mixdog 0.9.68 → 0.9.70
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/package.json +8 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
- package/src/runtime/channels/lib/owned-runtime.mjs +8 -1
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +13 -1
- package/src/runtime/media/adapters/codex-image.mjs +109 -0
- package/src/runtime/media/adapters/gemini-image.mjs +68 -0
- package/src/runtime/media/adapters/gemini-video.mjs +119 -0
- package/src/runtime/media/adapters/xai-media.mjs +116 -0
- package/src/runtime/media/auth.mjs +51 -0
- package/src/runtime/media/index.mjs +17 -0
- package/src/runtime/media/jobs.mjs +174 -0
- package/src/runtime/media/lanes.mjs +230 -0
- package/src/runtime/media/store.mjs +164 -0
- package/src/runtime/media/upstream-error.mjs +39 -0
- package/src/session-runtime/channel-config-api.mjs +12 -1
- package/src/session-runtime/media-api.mjs +47 -0
- package/src/session-runtime/memory-daemon-probe.mjs +61 -0
- package/src/session-runtime/model-capabilities.mjs +6 -4
- package/src/session-runtime/model-route-api.mjs +4 -1
- package/src/session-runtime/notification-bus.mjs +82 -0
- package/src/session-runtime/provider-auth-api.mjs +16 -1
- package/src/session-runtime/provider-usage.mjs +3 -0
- package/src/session-runtime/remote-control.mjs +166 -0
- package/src/session-runtime/remote-transcript.mjs +126 -0
- package/src/session-runtime/remote-transition-queue.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +160 -706
- package/src/session-runtime/runtime-tunables.mjs +57 -0
- package/src/session-runtime/self-update.mjs +129 -0
- package/src/session-runtime/skills-api.mjs +77 -0
- package/src/session-runtime/tool-surface.mjs +83 -0
- package/src/session-runtime/warmup-schedulers.mjs +14 -1
- package/src/session-runtime/workflow-agents-api.mjs +235 -7
- package/src/session-runtime/workflow.mjs +84 -17
- package/src/standalone/agent-tool/worker-index.mjs +287 -0
- package/src/standalone/agent-tool.mjs +22 -346
- package/src/standalone/agent-watchdog-registry.mjs +101 -0
- package/src/standalone/channel-worker.mjs +7 -13
- package/src/tui/components/StatusLine.jsx +6 -5
- package/src/tui/dist/index.mjs +144 -95
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +31 -8
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- package/src/tui/engine.mjs +39 -1
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/solo/WORKFLOW.md +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
|
-
import httpMod from 'node:http';
|
|
5
4
|
import keychain from '../lib/keychain-cjs.cjs';
|
|
6
5
|
import './hitch-profile.mjs';
|
|
7
6
|
import { ensureStandaloneEnvironment } from '../standalone/seeds.mjs';
|
|
@@ -11,15 +10,13 @@ import { EXPLORE_TOOL, runExplore } from '../standalone/explore-tool.mjs';
|
|
|
11
10
|
import { createStandaloneChannelWorker } from '../standalone/channel-worker.mjs';
|
|
12
11
|
import { createStandaloneMemoryRuntime } from '../standalone/memory-runtime-proxy.mjs';
|
|
13
12
|
import { createStandaloneHookBus } from '../standalone/hook-bus.mjs';
|
|
13
|
+
import { createRemoteTransitionQueue } from './remote-transition-queue.mjs';
|
|
14
14
|
import { writeLastSessionCwd } from '../runtime/shared/user-cwd.mjs';
|
|
15
15
|
import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
|
|
16
16
|
import { createTranscriptWriter } from '../runtime/shared/transcript-writer.mjs';
|
|
17
17
|
import { mixdogHome } from '../runtime/shared/plugin-paths.mjs';
|
|
18
18
|
import { checkLatestVersion, localPackageVersion, isDevInstall } from '../runtime/shared/update-checker.mjs';
|
|
19
19
|
import { spawnStagedInstall, runStagedInstall, isStagedComplete } from '../runtime/shared/staged-update.mjs';
|
|
20
|
-
import {
|
|
21
|
-
modelVisibleToolCompletionMessage,
|
|
22
|
-
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
23
20
|
import {
|
|
24
21
|
channelNotificationModelContent,
|
|
25
22
|
shouldMirrorChannelNotificationToPending,
|
|
@@ -30,12 +27,6 @@ import {
|
|
|
30
27
|
} from '../runtime/shared/markdown-frontmatter.mjs';
|
|
31
28
|
import { setConfiguredShell } from '../runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs';
|
|
32
29
|
import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
33
|
-
import { markCompletionEntry } from '../runtime/agent/orchestrator/session/manager/pending-messages.mjs';
|
|
34
|
-
import {
|
|
35
|
-
recordDeliveredCompletion,
|
|
36
|
-
isDeliveredCompletion,
|
|
37
|
-
logDuplicateSkip,
|
|
38
|
-
} from '../runtime/agent/orchestrator/session/manager/delivered-completions.mjs';
|
|
39
30
|
import {
|
|
40
31
|
beginOAuthProviderLogin,
|
|
41
32
|
forgetProviderAuth,
|
|
@@ -236,15 +227,23 @@ import { createCwdPlugins } from './cwd-plugins.mjs';
|
|
|
236
227
|
import { createSettingsApi } from './settings-api.mjs';
|
|
237
228
|
import { createProviderModels } from './provider-models.mjs';
|
|
238
229
|
import { createProviderUsage } from './provider-usage.mjs';
|
|
239
|
-
import { envFlag
|
|
230
|
+
import { envFlag } from './env.mjs';
|
|
240
231
|
import { bootProfile, profiledImport } from './boot-profile.mjs';
|
|
241
232
|
import { createChannelConfigApi } from './channel-config-api.mjs';
|
|
233
|
+
import { createMediaApi } from './media-api.mjs';
|
|
242
234
|
import { createProviderAuthApi } from './provider-auth-api.mjs';
|
|
243
235
|
import { createContextStatus } from './context-status.mjs';
|
|
244
236
|
import { createLifecycleApi } from './lifecycle-api.mjs';
|
|
245
237
|
import { createResourceApi } from './resource-api.mjs';
|
|
246
238
|
import { createModelRouteApi } from './model-route-api.mjs';
|
|
247
239
|
import { createWorkflowAgentsApi } from './workflow-agents-api.mjs';
|
|
240
|
+
import { createSelfUpdateController } from './self-update.mjs';
|
|
241
|
+
import { createSkillsApi } from './skills-api.mjs';
|
|
242
|
+
import { createRemoteTranscript } from './remote-transcript.mjs';
|
|
243
|
+
import { createRemoteControl } from './remote-control.mjs';
|
|
244
|
+
import { createNotificationBus } from './notification-bus.mjs';
|
|
245
|
+
import { createToolSurface } from './tool-surface.mjs';
|
|
246
|
+
import { readRuntimeTunables } from './runtime-tunables.mjs';
|
|
248
247
|
import { createSessionTurnApi } from './session-turn-api.mjs';
|
|
249
248
|
import { providerInitCacheKey } from './provider-init-key.mjs';
|
|
250
249
|
import {
|
|
@@ -297,6 +296,7 @@ const {
|
|
|
297
296
|
workflowSummary,
|
|
298
297
|
activeWorkflowSummary,
|
|
299
298
|
loadAgentDefinition,
|
|
299
|
+
listCustomAgentIds,
|
|
300
300
|
workflowContextBlock,
|
|
301
301
|
activeWorkflowContext,
|
|
302
302
|
} = createWorkflowHelpers({
|
|
@@ -334,15 +334,10 @@ export async function createMixdogSessionRuntime({
|
|
|
334
334
|
// dead/replaced owner (clear, delete) hands the seat to the next current
|
|
335
335
|
// session that asks.
|
|
336
336
|
let remoteSessionId = null;
|
|
337
|
-
// Remote
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// One-shot: an 'acquired' verdict (or other rebind trigger) landed before a
|
|
342
|
-
// session/writer existed, so the rebind push could not fire. Set true when a
|
|
343
|
-
// push is deferred; the next session-create / turn-start flushes it exactly
|
|
344
|
-
// once so the daemon forwarder always ends bound to THIS session's transcript.
|
|
345
|
-
let _pendingRebind = false;
|
|
337
|
+
// Desktop/TUI Remote toggles are desired-state transitions. Serialize the
|
|
338
|
+
// daemon deactivate/detach and reconnect/activate chains so an OFF that is
|
|
339
|
+
// still stopping can never tear down the worker created by the following ON.
|
|
340
|
+
const remoteTransitions = createRemoteTransitionQueue();
|
|
346
341
|
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
347
342
|
// so the post-turn final-content append can skip an exact duplicate.
|
|
348
343
|
let _lastAppendedAssistant = '';
|
|
@@ -370,6 +365,9 @@ export async function createMixdogSessionRuntime({
|
|
|
370
365
|
clearTimeout(timeoutId);
|
|
371
366
|
keychainPrewarmWaitDone = true;
|
|
372
367
|
}
|
|
368
|
+
// Invoked here: the assignment used to store the async FUNCTION, so every
|
|
369
|
+
// `await awaitKeychainPrewarm()` resolved instantly (awaiting a function is
|
|
370
|
+
// a no-op) and callers silently skipped the wait they asked for.
|
|
373
371
|
})();
|
|
374
372
|
return keychainPrewarmWaitPromise;
|
|
375
373
|
}
|
|
@@ -545,50 +543,26 @@ export async function createMixdogSessionRuntime({
|
|
|
545
543
|
...extra,
|
|
546
544
|
};
|
|
547
545
|
}
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const providerWarmupEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
569
|
-
&& (
|
|
570
|
-
envFlag('MIXDOG_ENABLE_PROVIDER_WARMUP')
|
|
571
|
-
|| envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')
|
|
572
|
-
|| envPresent('MIXDOG_PROVIDER_WARMUP_DELAY_MS')
|
|
573
|
-
|| envPresent('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS')
|
|
574
|
-
);
|
|
575
|
-
// Boot-time model-catalog prefetch is intentionally decoupled from the
|
|
576
|
-
// heavier providerWarmupEnabled gate (which stays opt-in for provider
|
|
577
|
-
// *init* side effects). Fetching the model list in the background after a
|
|
578
|
-
// short delay is cheap, fire-and-forget, and unref'd, so it is ON by
|
|
579
|
-
// default — otherwise the FIRST `/model` open always paid a cold full
|
|
580
|
-
// network load. Operators can still disable it explicitly.
|
|
581
|
-
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
582
|
-
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
583
|
-
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
584
|
-
const modelCatalogWarmupEnabled = !envFlag('MIXDOG_DISABLE_MODEL_CATALOG_WARMUP');
|
|
585
|
-
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
586
|
-
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
587
|
-
// burned a worker (and felt like a freeze) before the user did anything.
|
|
588
|
-
// Instead prewarm ONCE on the first real turn, when a code lookup is actually
|
|
589
|
-
// imminent. Operators who want the old eager behavior can set
|
|
590
|
-
// MIXDOG_CODE_GRAPH_PREWARM_EAGER=1.
|
|
591
|
-
const codeGraphPrewarmLazy = codeGraphPrewarmEnabled && !envFlag('MIXDOG_CODE_GRAPH_PREWARM_EAGER');
|
|
546
|
+
// Env-tunable boot delays and feature gates: runtime-tunables.mjs.
|
|
547
|
+
const {
|
|
548
|
+
sessionPrewarmDelayMs,
|
|
549
|
+
providerSetupWarmupDelayMs,
|
|
550
|
+
modelCatalogWarmupDelayMs,
|
|
551
|
+
providerWarmupDelayMs,
|
|
552
|
+
providerModelWarmupDelayMs,
|
|
553
|
+
codeGraphPrewarmDelayMs,
|
|
554
|
+
statuslineUsageWarmupDelayMs,
|
|
555
|
+
statuslineUsageRefreshDelayMs,
|
|
556
|
+
channelStartDelayMs,
|
|
557
|
+
backgroundBusyRetryMs,
|
|
558
|
+
remoteAutoStartDelayMs,
|
|
559
|
+
sessionPrewarmEnabled,
|
|
560
|
+
providerWarmupEnabled,
|
|
561
|
+
modelPrefetchEnabled,
|
|
562
|
+
codeGraphPrewarmEnabled,
|
|
563
|
+
modelCatalogWarmupEnabled,
|
|
564
|
+
codeGraphPrewarmLazy,
|
|
565
|
+
} = readRuntimeTunables();
|
|
592
566
|
let codeGraphFirstTurnPrewarmDone = false;
|
|
593
567
|
const modelMetaByRoute = new Map();
|
|
594
568
|
const notificationListeners = new Set();
|
|
@@ -615,6 +589,9 @@ export async function createMixdogSessionRuntime({
|
|
|
615
589
|
};
|
|
616
590
|
const providerInitPromises = new Map();
|
|
617
591
|
let startupProviderCatalogRefreshStarted = false;
|
|
592
|
+
// True while the boot-time provider-catalog refresh is in flight: warming a
|
|
593
|
+
// model cache it is about to invalidate only burns the load twice.
|
|
594
|
+
let startupProviderCatalogRefreshPending = false;
|
|
618
595
|
let lastProjectMcpKey = null;
|
|
619
596
|
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
620
597
|
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
@@ -640,251 +617,40 @@ export async function createMixdogSessionRuntime({
|
|
|
640
617
|
setDesktopSession: (v) => { desktopSession = v; },
|
|
641
618
|
state: mcpState,
|
|
642
619
|
});
|
|
643
|
-
let preSessionToolSurface = null;
|
|
644
620
|
const hooksStartedAt = performance.now();
|
|
645
621
|
const hooks = createStandaloneHookBus({ dataDir: cfgMod.getPluginData() });
|
|
646
622
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
647
623
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
648
624
|
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
663
|
-
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
664
|
-
// Boot detects an available update and STAGES it in the background (a hidden,
|
|
665
|
-
// detached npm install into ~/.mixdog/data/staging/<ver>). The staged package
|
|
666
|
-
// is swapped into the global dir on the next clean launch (cli.mjs
|
|
667
|
-
// pre-import), never while a session is live — so npm never overwrites the
|
|
668
|
-
// .mjs files node currently holds (the old shutdown `npm install -g` did, and
|
|
669
|
-
// caused Windows TAR_ENTRY_ERROR / ENOENT). The live-session refcount that a
|
|
670
|
-
// concurrent launch consults to defer its swap is registered earlier, in
|
|
671
|
-
// cli.mjs (pre-import), so this process is visible before any runtime loads.
|
|
672
|
-
|
|
673
|
-
function autoUpdateEnabled() {
|
|
674
|
-
return config?.update?.auto !== false;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
async function checkForUpdateInternal({ force = false } = {}) {
|
|
678
|
-
if (updateProcessState.phase !== 'installing') updateProcessState.phase = 'checking';
|
|
679
|
-
try {
|
|
680
|
-
const result = await checkLatestVersion({ force, dataDir: cfgMod.getPluginData?.() || STANDALONE_DATA_DIR });
|
|
681
|
-
updateCheckState = {
|
|
682
|
-
currentVersion: result.currentVersion,
|
|
683
|
-
latestVersion: result.latestVersion,
|
|
684
|
-
updateAvailable: result.updateAvailable,
|
|
685
|
-
lastCheckedAt: result.lastCheckedAt,
|
|
686
|
-
};
|
|
687
|
-
} catch {
|
|
688
|
-
// checkLatestVersion() is already silent-safe; this catch is belt-and-
|
|
689
|
-
// braces so a boot-time call can never crash the runtime.
|
|
690
|
-
} finally {
|
|
691
|
-
if (updateProcessState.phase === 'checking') updateProcessState.phase = 'idle';
|
|
692
|
-
}
|
|
693
|
-
return updateCheckState;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async function runUpdateNowInternal() {
|
|
697
|
-
if (updateProcessState.phase === 'installing') {
|
|
698
|
-
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
699
|
-
}
|
|
700
|
-
if (isDevInstall()) {
|
|
701
|
-
updateProcessState = { phase: 'failed', version: null, error: 'dev install — update skipped' };
|
|
702
|
-
return updateProcessState;
|
|
703
|
-
}
|
|
704
|
-
const ver = updateCheckState.latestVersion;
|
|
705
|
-
if (!ver || !updateCheckState.updateAvailable) {
|
|
706
|
-
updateProcessState = { phase: 'idle', version: null, error: null };
|
|
707
|
-
return { ...updateProcessState, error: 'no update available' };
|
|
708
|
-
}
|
|
709
|
-
// "Update now" stages the new version (verified, self-contained) rather than
|
|
710
|
-
// installing over the live global dir; the swap applies on the next launch.
|
|
711
|
-
// phase 'installed' here means "staged & ready — restart to apply".
|
|
712
|
-
updateProcessState = { phase: 'installing', version: ver, error: null };
|
|
713
|
-
try {
|
|
714
|
-
const result = await runStagedInstall(ver);
|
|
715
|
-
if (result?.ok) {
|
|
716
|
-
updateProcessState = { phase: 'installed', version: result.version || ver, error: null };
|
|
717
|
-
} else {
|
|
718
|
-
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
719
|
-
}
|
|
720
|
-
} catch (err) {
|
|
721
|
-
updateProcessState = { phase: 'failed', version: null, error: err?.message || String(err) };
|
|
722
|
-
}
|
|
723
|
-
return updateProcessState;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
727
|
-
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
728
|
-
// slow/hanging registry request can never delay session boot. The check
|
|
729
|
-
// ALWAYS runs (populates updateCheckState for the maintenance picker), but
|
|
730
|
-
// when an update is available it kicks off a hidden BACKGROUND staging
|
|
731
|
-
// install (spawnStagedInstall) into ~/.mixdog/data/staging/<ver>. The actual
|
|
732
|
-
// swap into the global dir happens on the next clean launch (cli.mjs
|
|
733
|
-
// pre-import), so npm never overwrites files this live process holds.
|
|
734
|
-
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
735
|
-
// stale-visible: it kept reporting an older "latest" than the installed
|
|
736
|
-
// version). checkLatestVersion() still falls back to the cache offline.
|
|
737
|
-
// isDevInstall() gate: a git checkout / clone (or non-node_modules install)
|
|
738
|
-
// must never self-update — staging + swap would fight the working tree.
|
|
739
|
-
const updateBootTimer = setTimeout(() => {
|
|
740
|
-
void (async () => {
|
|
741
|
-
await checkForUpdateInternal({ force: true });
|
|
742
|
-
if (!(autoUpdateEnabled() && !isDevInstall() && updateCheckState.updateAvailable)) return;
|
|
743
|
-
const ver = updateCheckState.latestVersion;
|
|
744
|
-
if (!ver) return;
|
|
745
|
-
// The notice fires ONLY once staging has completed (a ready-to-apply
|
|
746
|
-
// package sits on disk) — never upfront — so the user sees no "update
|
|
747
|
-
// available / installs on quit" nag while the background stage runs
|
|
748
|
-
// silently. The wording lives in the notice surface (notification-plan):
|
|
749
|
-
// this emit only carries meta.version. TUI maps meta.kind 'update-notice'
|
|
750
|
-
// to a transient notice, never a model-visible message; tone 'info' =
|
|
751
|
-
// non-urgent, applies on the next launch.
|
|
752
|
-
const announceReady = () => {
|
|
753
|
-
emitRuntimeNotification('update ready', { kind: 'update-notice', version: ver, tone: 'info' });
|
|
754
|
-
};
|
|
755
|
-
// Already staged in a prior session → announce immediately.
|
|
756
|
-
if (isStagedComplete(ver)) { announceReady(); return; }
|
|
757
|
-
try { spawnStagedInstall(ver); } catch { /* best-effort background stage */ }
|
|
758
|
-
// Poll for staging completion, then announce once. The interval is
|
|
759
|
-
// unref'd so it never holds the process open, and gives up silently
|
|
760
|
-
// after the cap — the next launch retries.
|
|
761
|
-
const POLL_MS = 3_000;
|
|
762
|
-
const MAX_MS = 10 * 60 * 1000;
|
|
763
|
-
const startedAt = Date.now();
|
|
764
|
-
const poll = setInterval(() => {
|
|
765
|
-
if (isStagedComplete(ver)) {
|
|
766
|
-
clearInterval(poll);
|
|
767
|
-
announceReady();
|
|
768
|
-
} else if (Date.now() - startedAt > MAX_MS) {
|
|
769
|
-
clearInterval(poll);
|
|
770
|
-
}
|
|
771
|
-
}, POLL_MS);
|
|
772
|
-
poll.unref?.();
|
|
773
|
-
})().catch(() => {});
|
|
774
|
-
}, 0);
|
|
775
|
-
updateBootTimer.unref?.();
|
|
776
|
-
|
|
777
|
-
function emitRuntimeNotification(content, meta = {}) {
|
|
778
|
-
const text = String(content || '').trim();
|
|
779
|
-
if (!text) return { handled: false, modelVisibleDelivered: false };
|
|
780
|
-
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
781
|
-
let handled = false;
|
|
782
|
-
for (const listener of [...notificationListeners]) {
|
|
783
|
-
try {
|
|
784
|
-
if (listener(event) === true) handled = true;
|
|
785
|
-
} catch {}
|
|
786
|
-
}
|
|
787
|
-
// EXPLICIT model-visible-delivery ack: only the TUI execution-ui path sets
|
|
788
|
-
// event.modelVisibleDelivered when it enqueues the model-visible completion
|
|
789
|
-
// body into the active loop. A generic display-only / API listener that
|
|
790
|
-
// returns true does NOT set it, so `handled` alone must never be read as
|
|
791
|
-
// "the model saw the body".
|
|
792
|
-
const modelVisibleDelivered = event.modelVisibleDelivered === true;
|
|
793
|
-
return { handled, modelVisibleDelivered };
|
|
794
|
-
}
|
|
625
|
+
// Self-update: registry check + background staging live in self-update.mjs;
|
|
626
|
+
// the facade only wires config/data-dir/notification access into it. The
|
|
627
|
+
// boot check is deferred past this constructor so a hanging registry request
|
|
628
|
+
// can never delay session boot.
|
|
629
|
+
const selfUpdate = createSelfUpdateController({
|
|
630
|
+
getConfig: () => config,
|
|
631
|
+
getDataDir: () => cfgMod.getPluginData?.() || STANDALONE_DATA_DIR,
|
|
632
|
+
emitNotification: (...a) => emitRuntimeNotification(...a),
|
|
633
|
+
});
|
|
634
|
+
const autoUpdateEnabled = () => selfUpdate.autoUpdateEnabled();
|
|
635
|
+
const checkForUpdateInternal = (...a) => selfUpdate.checkForUpdate(...a);
|
|
636
|
+
const runUpdateNowInternal = (...a) => selfUpdate.runUpdateNow();
|
|
637
|
+
selfUpdate.startBootCheck();
|
|
795
638
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
// on another path (background reconcile/fallback/notify) within THIS
|
|
803
|
-
// process is skipped instead of double-injecting the same body next turn.
|
|
804
|
-
if (modelVisibleDelivered) {
|
|
805
|
-
try {
|
|
806
|
-
const deliveredVisible = modelVisibleToolCompletionMessage(text, meta);
|
|
807
|
-
recordDeliveredCompletion({ executionId: meta?.execution_id, text: deliveredVisible });
|
|
808
|
-
} catch {}
|
|
809
|
-
}
|
|
810
|
-
// TUI sessions consume raw execution notifications for UI/task cards via
|
|
811
|
-
// onNotification, but those raw envelopes are internal-only in pending
|
|
812
|
-
// drain. The TUI execution-ui path injects the model-visible twin of a
|
|
813
|
-
// terminal completion into the active loop and acks with
|
|
814
|
-
// modelVisibleDelivered, so mirroring it into the pending queue would
|
|
815
|
-
// double-inject the same completion — skip only on that EXPLICIT ack. A
|
|
816
|
-
// generic display-only / API listener returning true is NOT an ack: the
|
|
817
|
-
// mirror stays as the sole model-visible delivery.
|
|
818
|
-
if (shouldMirrorCompletionToPendingQueue({
|
|
819
|
-
callerSessionId,
|
|
820
|
-
modelVisibleDelivered,
|
|
821
|
-
hasEnqueue: typeof mgr.enqueuePendingMessage === 'function',
|
|
822
|
-
text,
|
|
823
|
-
meta,
|
|
824
|
-
})) {
|
|
825
|
-
try {
|
|
826
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
827
|
-
// Terminal completion (gated by shouldPersistModelVisibleToolCompletion)
|
|
828
|
-
// → tag so drain discards it on resume rather than replaying out-of-order.
|
|
829
|
-
if (visible) {
|
|
830
|
-
if (isDeliveredCompletion({ executionId: meta?.execution_id, text: visible })) {
|
|
831
|
-
// Already delivered+ACKed on the TUI path → suppress the mirror but
|
|
832
|
-
// report DELIVERED so the caller marks it notified and never retries.
|
|
833
|
-
logDuplicateSkip('mirror', { executionId: meta?.execution_id, text: visible });
|
|
834
|
-
enqueued = true;
|
|
835
|
-
} else {
|
|
836
|
-
enqueued = mgr.enqueuePendingMessage(callerSessionId, markCompletionEntry(visible)) > 0;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
} catch {}
|
|
840
|
-
}
|
|
841
|
-
// Headless/API listeners may exist but not consume the event; preserve
|
|
842
|
-
// the old fallback for non-terminal notifications only when unhandled.
|
|
843
|
-
if (!enqueued && !handledByRuntimeListener && callerSessionId
|
|
844
|
-
&& typeof mgr.enqueuePendingMessage === 'function') {
|
|
845
|
-
try {
|
|
846
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
847
|
-
// modelVisibleToolCompletionMessage only returns non-empty for a
|
|
848
|
-
// persistable terminal completion, so this fallback is a completion
|
|
849
|
-
// too → tag it (genuine non-completion notifications yield '' above).
|
|
850
|
-
if (visible) {
|
|
851
|
-
if (isDeliveredCompletion({ executionId: meta?.execution_id, text: visible })) {
|
|
852
|
-
// Already delivered+ACKed on the TUI path → suppress the fallback
|
|
853
|
-
// enqueue but report DELIVERED so the caller stops retrying.
|
|
854
|
-
logDuplicateSkip('fallback', { executionId: meta?.execution_id, text: visible });
|
|
855
|
-
enqueued = true;
|
|
856
|
-
} else {
|
|
857
|
-
enqueued = mgr.enqueuePendingMessage(callerSessionId, markCompletionEntry(visible)) > 0;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
} catch {}
|
|
861
|
-
}
|
|
862
|
-
return enqueued || handledByRuntimeListener;
|
|
863
|
-
};
|
|
864
|
-
}
|
|
639
|
+
// Notification fan-out (listener broadcast + pending-queue mirroring of
|
|
640
|
+
// terminal completions) lives in notification-bus.mjs.
|
|
641
|
+
const { emitRuntimeNotification, notifyFnForSession } = createNotificationBus({
|
|
642
|
+
listeners: notificationListeners,
|
|
643
|
+
mgr,
|
|
644
|
+
});
|
|
865
645
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (cwdNorm && path.startsWith(`${cwdNorm}/.mixdog/skills/`)) return 'project';
|
|
875
|
-
return 'skill';
|
|
876
|
-
};
|
|
877
|
-
return {
|
|
878
|
-
cwd: currentCwd,
|
|
879
|
-
count: skills.length,
|
|
880
|
-
skills: skills.map((skill) => ({
|
|
881
|
-
name: skill.name,
|
|
882
|
-
description: skill.description || '',
|
|
883
|
-
filePath: skill.filePath || null,
|
|
884
|
-
source: sourceForSkill(skill.filePath),
|
|
885
|
-
})),
|
|
886
|
-
};
|
|
887
|
-
}
|
|
646
|
+
// Skill listing/loading/creation lives in skills-api.mjs; the facade only
|
|
647
|
+
// supplies the mutable cwd and the context module.
|
|
648
|
+
const {
|
|
649
|
+
skillsStatus,
|
|
650
|
+
skillContent,
|
|
651
|
+
skillToolContent,
|
|
652
|
+
addProjectSkill,
|
|
653
|
+
} = createSkillsApi({ contextMod, getCwd: () => currentCwd });
|
|
888
654
|
|
|
889
655
|
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
890
656
|
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
@@ -932,52 +698,6 @@ export async function createMixdogSessionRuntime({
|
|
|
932
698
|
STANDALONE_DATA_DIR,
|
|
933
699
|
});
|
|
934
700
|
|
|
935
|
-
function skillContent(name) {
|
|
936
|
-
const res = typeof contextMod.loadSkillResource === 'function'
|
|
937
|
-
? contextMod.loadSkillResource(name, currentCwd)
|
|
938
|
-
: null;
|
|
939
|
-
if (!res) throw new Error(`skill not found: ${name}`);
|
|
940
|
-
return { name, content: res.content, dir: res.dir };
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function skillToolContent(name) {
|
|
944
|
-
if (typeof contextMod.isSkillDisabled === 'function' && contextMod.isSkillDisabled(name)) {
|
|
945
|
-
const label = String(name || '').trim() || 'skill';
|
|
946
|
-
return `Error: skill "${label}" is disabled`;
|
|
947
|
-
}
|
|
948
|
-
const skill = skillContent(name);
|
|
949
|
-
// Return the general tool envelope so the main/Lead session behaves the
|
|
950
|
-
// same as agent-loop sessions: the model-visible tool_result is the short
|
|
951
|
-
// stub (`Loaded skill: <name>`) and the full SKILL.md body is delivered
|
|
952
|
-
// ONCE as a separate injected role:'user' message (newMessages). The
|
|
953
|
-
// envelope passes through internal-tools._normalize untouched (it preserves
|
|
954
|
-
// __toolEnvelope objects), and the agent loop's central normalizeToolEnvelope
|
|
955
|
-
// splits it into stub + injected user body.
|
|
956
|
-
return contextMod.buildSkillToolEnvelope(skill.name, skill.content, skill.dir);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
function addProjectSkill(input = {}) {
|
|
960
|
-
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
961
|
-
if (!name) throw new Error('skill name is required');
|
|
962
|
-
const dir = join(currentCwd, '.mixdog', 'skills', name);
|
|
963
|
-
const filePath = join(dir, 'SKILL.md');
|
|
964
|
-
if (existsSync(filePath)) throw new Error(`skill already exists: ${name}`);
|
|
965
|
-
const description = clean(input.description) || 'Project skill.';
|
|
966
|
-
mkdirSync(dir, { recursive: true });
|
|
967
|
-
writeFileSync(filePath, [
|
|
968
|
-
'---',
|
|
969
|
-
`name: ${name}`,
|
|
970
|
-
`description: ${description}`,
|
|
971
|
-
'---',
|
|
972
|
-
'',
|
|
973
|
-
'# Instructions',
|
|
974
|
-
'',
|
|
975
|
-
'Describe when and how to use this skill.',
|
|
976
|
-
'',
|
|
977
|
-
].join('\n'), 'utf8');
|
|
978
|
-
return { name, filePath };
|
|
979
|
-
}
|
|
980
|
-
|
|
981
701
|
const agentToolStartedAt = performance.now();
|
|
982
702
|
const agentTool = createStandaloneAgent({
|
|
983
703
|
cfgMod,
|
|
@@ -1082,25 +802,26 @@ export async function createMixdogSessionRuntime({
|
|
|
1082
802
|
// schema-visible tool that policy always rejects is a guaranteed error turn
|
|
1083
803
|
// (user-reported in Solo). Names derive from the live agent tool defs.
|
|
1084
804
|
const agentToolNames = new Set(agentTool.tools.map((tool) => String(tool?.name || '')).filter(Boolean));
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
805
|
+
// Lead tool surface (workflow-gated agent tool, pre-session preview, deferred
|
|
806
|
+
// replay) lives in tool-surface.mjs.
|
|
807
|
+
const {
|
|
808
|
+
modelStandaloneTools,
|
|
809
|
+
invalidatePreSessionToolSurface,
|
|
810
|
+
activeToolSurface,
|
|
811
|
+
applyPreSessionToolSelection,
|
|
812
|
+
} = createToolSurface({
|
|
813
|
+
mgr,
|
|
814
|
+
mode,
|
|
815
|
+
standaloneTools,
|
|
816
|
+
agentToolNames,
|
|
817
|
+
getSession: () => session,
|
|
818
|
+
getRoute: () => route,
|
|
819
|
+
getConfig: () => config,
|
|
820
|
+
cfgMod,
|
|
821
|
+
loadWorkflowPack,
|
|
822
|
+
activeWorkflowId,
|
|
823
|
+
dataDir: STANDALONE_DATA_DIR,
|
|
824
|
+
});
|
|
1104
825
|
|
|
1105
826
|
const { contextStatus: computeContextStatus, invalidateContextStatusCache } = createContextStatus({
|
|
1106
827
|
getSession: () => session,
|
|
@@ -1108,34 +829,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1108
829
|
getCurrentCwd: () => currentCwd,
|
|
1109
830
|
getMode: () => mode,
|
|
1110
831
|
});
|
|
1111
|
-
|
|
1112
|
-
function buildPreSessionToolSurface() {
|
|
1113
|
-
const previewTools = typeof mgr.previewSessionTools === 'function'
|
|
1114
|
-
? mgr.previewSessionTools(toolSpecForMode(mode), [])
|
|
1115
|
-
: [];
|
|
1116
|
-
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
1117
|
-
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
1118
|
-
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), modelStandaloneTools(), { provider: route.provider });
|
|
1119
|
-
return surface;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function activeToolSurface() {
|
|
1123
|
-
if (session) return session;
|
|
1124
|
-
preSessionToolSurface ??= buildPreSessionToolSurface();
|
|
1125
|
-
return preSessionToolSurface;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
function applyPreSessionToolSelection() {
|
|
1129
|
-
if (!session || !preSessionToolSurface) return;
|
|
1130
|
-
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
1131
|
-
? preSessionToolSurface.deferredSelectedTools
|
|
1132
|
-
: [];
|
|
1133
|
-
const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
|
|
1134
|
-
? preSessionToolSurface.deferredDiscoveredTools
|
|
1135
|
-
: [];
|
|
1136
|
-
const replay = [...new Set([...selected, ...discovered])];
|
|
1137
|
-
if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
|
|
1138
|
-
}
|
|
1139
832
|
internalTools.setInternalToolsProvider({
|
|
1140
833
|
tools: [...standaloneTools, ...searchRuntimeTools.filter((tool) => tool?.public === false)],
|
|
1141
834
|
executor: async (name, args, callerCtx = {}) => {
|
|
@@ -1322,20 +1015,28 @@ export async function createMixdogSessionRuntime({
|
|
|
1322
1015
|
}
|
|
1323
1016
|
if (!startupProviderCatalogRefreshStarted && !closeRequested) {
|
|
1324
1017
|
startupProviderCatalogRefreshStarted = true;
|
|
1018
|
+
startupProviderCatalogRefreshPending = true;
|
|
1325
1019
|
try {
|
|
1326
1020
|
void Promise.resolve(reg.refreshProviderCatalogsOnStartup())
|
|
1327
1021
|
.then(() => {
|
|
1328
1022
|
// Fresh catalog rows invalidate model-derived caches, but the
|
|
1329
1023
|
// already initialized provider registry remains valid.
|
|
1330
1024
|
invalidateProviderCaches({ preserveProviderInit: true });
|
|
1331
|
-
|
|
1025
|
+
startupProviderCatalogRefreshPending = false;
|
|
1026
|
+
// Secrets-aware: a no-secrets rewarm bumps the load sequence and
|
|
1027
|
+
// is never adopted, so it discarded the in-flight authoritative
|
|
1028
|
+
// load and left the picker cache empty — every later consumer then
|
|
1029
|
+
// paid a full catalog load (measured ~560ms each).
|
|
1030
|
+
warmProviderModelCache({ loadSecrets: true });
|
|
1332
1031
|
bootProfile('provider-catalogs:refresh-ready');
|
|
1333
1032
|
})
|
|
1334
1033
|
.catch((error) => {
|
|
1034
|
+
startupProviderCatalogRefreshPending = false;
|
|
1335
1035
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1336
1036
|
});
|
|
1337
1037
|
bootProfile('provider-catalogs:refresh-started');
|
|
1338
1038
|
} catch (error) {
|
|
1039
|
+
startupProviderCatalogRefreshPending = false;
|
|
1339
1040
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1340
1041
|
}
|
|
1341
1042
|
}
|
|
@@ -1371,6 +1072,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1371
1072
|
const {
|
|
1372
1073
|
refreshStatuslineUsageSnapshot,
|
|
1373
1074
|
cachedProviderSetup,
|
|
1075
|
+
hasProviderSetupCached,
|
|
1374
1076
|
getUsageDashboard,
|
|
1375
1077
|
} = createProviderUsage({
|
|
1376
1078
|
caches: providerUsageCaches,
|
|
@@ -1665,6 +1367,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1665
1367
|
cachedProviderSetup,
|
|
1666
1368
|
warmCatalogsInBackground,
|
|
1667
1369
|
isFirstTurnCompleted: () => firstTurnCompleted,
|
|
1370
|
+
isCatalogRefreshPending: () => startupProviderCatalogRefreshPending,
|
|
1668
1371
|
envFlag,
|
|
1669
1372
|
delays: {
|
|
1670
1373
|
providerWarmupDelayMs,
|
|
@@ -1717,313 +1420,59 @@ export async function createMixdogSessionRuntime({
|
|
|
1717
1420
|
state: prewarmState,
|
|
1718
1421
|
});
|
|
1719
1422
|
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
1739
|
-
if (_twKey !== twKey) {
|
|
1740
|
-
try {
|
|
1741
|
-
_transcriptWriter = createTranscriptWriter({
|
|
1742
|
-
mixdogHome: mixdogHome(),
|
|
1743
|
-
sessionId: session.id,
|
|
1744
|
-
cwd: currentCwd,
|
|
1745
|
-
pid: process.pid,
|
|
1746
|
-
});
|
|
1747
|
-
_transcriptWriter.writeSessionRecord();
|
|
1748
|
-
_twKey = twKey;
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
1751
|
-
_transcriptWriter = null;
|
|
1752
|
-
_twKey = '';
|
|
1753
|
-
return false;
|
|
1754
|
-
}
|
|
1755
|
-
} else {
|
|
1756
|
-
// Same binding — refresh updatedAt so worker-side discovery keeps
|
|
1757
|
-
// ranking this session as the live parent-chain candidate.
|
|
1758
|
-
try { _transcriptWriter?.writeSessionRecord(); } catch {}
|
|
1759
|
-
}
|
|
1760
|
-
try { _transcriptWriter?.ensureTranscriptFile(); }
|
|
1761
|
-
catch (error) { process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`); }
|
|
1762
|
-
return _transcriptWriter != null;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
// Push the CURRENT transcript path to the channel worker so outbound
|
|
1766
|
-
// forwarding repoints immediately at the moments the binding can go stale
|
|
1767
|
-
// (auto-acquire, newSession/resume, clear) instead of waiting for the next
|
|
1768
|
-
// inbound parent-chain steal. Best-effort: ensures the writer, then fires
|
|
1769
|
-
// the dedicated idempotent worker op — a missing/not-ready worker or a bind
|
|
1770
|
-
// failure must never throw into the lead paths that call this.
|
|
1771
|
-
function pushTranscriptRebind() {
|
|
1772
|
-
if (!remoteEnabled) return;
|
|
1773
|
-
// Writer not bindable yet (e.g. 'acquired' before the session exists in
|
|
1774
|
-
// lazy mode): defer instead of silently dropping the push. flushPending-
|
|
1775
|
-
// TranscriptRebind() re-fires this exactly once when the writer is ready.
|
|
1776
|
-
if (!ensureRemoteTranscriptWriter()) { _pendingRebind = true; return; }
|
|
1777
|
-
const transcriptPath = _transcriptWriter?.transcriptPath;
|
|
1778
|
-
if (!transcriptPath || !channelsEnabled()) { _pendingRebind = true; return; }
|
|
1779
|
-
_pendingRebind = false;
|
|
1780
|
-
executeTranscriptRebind(transcriptPath, 1);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
// Fire the idempotent worker op with bounded retry. A rejected/throwing
|
|
1784
|
-
// channels.execute retries a few times with short backoff; the final failure
|
|
1785
|
-
// surfaces one stderr line (not only the env-gated bootProfile) so a lost
|
|
1786
|
-
// rebind is diagnosable by default. Best-effort throughout — never throws
|
|
1787
|
-
// into the lead paths that call pushTranscriptRebind().
|
|
1788
|
-
function executeTranscriptRebind(transcriptPath, attempt) {
|
|
1789
|
-
const maxAttempts = 3;
|
|
1790
|
-
const onError = (error) => {
|
|
1791
|
-
const detail = error?.message || String(error);
|
|
1792
|
-
bootProfile('channels:rebind-push-failed', { attempt, error: detail });
|
|
1793
|
-
if (attempt < maxAttempts && remoteEnabled && !closeRequested) {
|
|
1794
|
-
const timer = setTimeout(() => {
|
|
1795
|
-
// Abort the retry chain silently if remote was dropped or the writer
|
|
1796
|
-
// moved on (supersede→re-acquire, newSession/clear): re-firing the
|
|
1797
|
-
// captured path would rebind forwarding back to a stale transcript.
|
|
1798
|
-
if (!remoteEnabled || !channelsEnabled()) return;
|
|
1799
|
-
if (_transcriptWriter?.transcriptPath !== transcriptPath) return;
|
|
1800
|
-
executeTranscriptRebind(transcriptPath, attempt + 1);
|
|
1801
|
-
}, 150 * attempt);
|
|
1802
|
-
timer.unref?.();
|
|
1803
|
-
} else {
|
|
1804
|
-
process.stderr.write(`mixdog: channels: rebind_current_transcript failed after ${attempt} attempt(s): ${detail}\n`);
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
try {
|
|
1808
|
-
void channels.execute('rebind_current_transcript', { transcriptPath }).catch(onError);
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
onError(error);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
// Re-fire a deferred rebind exactly once, after a session/writer becomes
|
|
1815
|
-
// available (session create or turn start). No-op unless a push was deferred,
|
|
1816
|
-
// so no unconditional rebind fires per turn for already-bound sessions.
|
|
1817
|
-
function flushPendingTranscriptRebind() {
|
|
1818
|
-
if (!_pendingRebind || !remoteEnabled) return;
|
|
1819
|
-
pushTranscriptRebind();
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
1823
|
-
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
1824
|
-
// boots the channel worker and contends for channel ownership.
|
|
1825
|
-
// startRemote() is FORCE-TAKEOVER: it always (re)claims the bridge seat and
|
|
1826
|
-
// rebinds output forwarding to this session, even when the worker is
|
|
1827
|
-
// already running (e.g. `/remote` re-issued after another session took the
|
|
1828
|
-
// seat, or to re-pin forwarding onto the current transcript).
|
|
1829
|
-
function startRemote(options = {}) {
|
|
1830
|
-
const intent = options?.intent === 'auto' ? 'auto' : 'explicit';
|
|
1831
|
-
// Claim intent reaches the worker's boot claim via MIXDOG_REMOTE_INTENT
|
|
1832
|
-
// (last-wins for explicit, claim-if-vacant for auto). It is set transiently
|
|
1833
|
-
// around the worker fork below (see invokeChannelStart) and restored right
|
|
1834
|
-
// after — NOT here on the shared process.env — so unrelated children forked
|
|
1835
|
-
// during the boot window never inherit a stale intent.
|
|
1836
|
-
if (intent === 'auto') {
|
|
1837
|
-
// Auto-start (config/delayed): do NOT flip this session to remote up
|
|
1838
|
-
// front. Boot the worker to ATTEMPT a claim-if-vacant; remoteEnabled is
|
|
1839
|
-
// set only when the worker reports it actually acquired the seat (the
|
|
1840
|
-
// 'acquired' notification). A live owner already holding the seat makes
|
|
1841
|
-
// the worker back off silently and this session stays non-remote.
|
|
1842
|
-
remoteClaimPending = true;
|
|
1843
|
-
} else {
|
|
1844
|
-
remoteEnabled = true;
|
|
1845
|
-
// Explicit toggle: the CURRENT session owns the relay (lazy create
|
|
1846
|
-
// adopts inside ensureRemoteTranscriptWriter when no session exists yet).
|
|
1847
|
-
remoteSessionId = session?.id || null;
|
|
1848
|
-
}
|
|
1849
|
-
// Boot the memory daemon eagerly. The channels worker forwards
|
|
1850
|
-
// transcript ingests/entries to the memory HTTP service, whose port is
|
|
1851
|
-
// published to active-instance.json by getMemoryModule().init(). Without
|
|
1852
|
-
// this, memory only starts on the first turn's getMemoryModule() call —
|
|
1853
|
-
// so early channel traffic finds no memory_port and gets buffered (or,
|
|
1854
|
-
// pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
|
|
1855
|
-
// racing to be live by the time the worker sends its first ingest.
|
|
1856
|
-
//
|
|
1857
|
-
// Not fire-and-forget: init() only resolves the module handle, so a bare
|
|
1858
|
-
// getMemoryModule() could return before /health reports ok — leaving early
|
|
1859
|
-
// ingests to hit a not-yet-listening port. Await the proxy start() and then
|
|
1860
|
-
// poll /health with a bounded retry so the daemon is provably reachable
|
|
1861
|
-
// (or logged failed) rather than assumed-up. Still non-blocking to the
|
|
1862
|
-
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1863
|
-
void (async () => {
|
|
1864
|
-
try {
|
|
1865
|
-
// Yield one event-loop tick before the heavy chain below (module
|
|
1866
|
-
// resolve, daemon fork/health-poll) starts, so Ink's next render
|
|
1867
|
-
// (scheduled via setImmediate/timers) and any queued keypress
|
|
1868
|
-
// events get a turn first instead of being starved by this
|
|
1869
|
-
// detached chain's synchronous setup work.
|
|
1870
|
-
await new Promise((r) => setImmediate(r));
|
|
1871
|
-
const mod = await getMemoryModule();
|
|
1872
|
-
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1873
|
-
const port = started?.port;
|
|
1874
|
-
if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
|
|
1875
|
-
for (let i = 0; i < 30; i++) {
|
|
1876
|
-
try {
|
|
1877
|
-
const ok = await new Promise((res) => {
|
|
1878
|
-
const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
|
|
1879
|
-
let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
|
|
1880
|
-
try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
|
|
1881
|
-
});
|
|
1882
|
-
});
|
|
1883
|
-
req.on('error', () => res(false));
|
|
1884
|
-
req.on('timeout', () => { req.destroy(); res(false); });
|
|
1885
|
-
req.end();
|
|
1886
|
-
});
|
|
1887
|
-
if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
|
|
1888
|
-
} catch {}
|
|
1889
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1890
|
-
}
|
|
1891
|
-
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
1892
|
-
} catch (error) {
|
|
1893
|
-
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
1894
|
-
}
|
|
1895
|
-
})();
|
|
1896
|
-
// Publish this session's record + transcript file BEFORE the worker's
|
|
1897
|
-
// activate-time discovery polls, so output forwarding binds to this
|
|
1898
|
-
// terminal session immediately instead of waiting for the first turn.
|
|
1899
|
-
// No-op when the session has not been created yet (lazy mode); that
|
|
1900
|
-
// case is covered by the turn-start rebind in ask().
|
|
1901
|
-
ensureRemoteTranscriptWriter();
|
|
1902
|
-
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
1903
|
-
bootProfile('channels:start-skipped');
|
|
1904
|
-
return true;
|
|
1905
|
-
}
|
|
1906
|
-
if (closeRequested) return true;
|
|
1907
|
-
if (prewarmTimers.channelStartTimer) {
|
|
1908
|
-
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1909
|
-
prewarmTimers.channelStartTimer = null;
|
|
1910
|
-
}
|
|
1911
|
-
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1912
|
-
void (async () => {
|
|
1913
|
-
// Channels-module toggle gates MESSAGING only: automation (schedules/
|
|
1914
|
-
// webhooks) still boots the worker — its backend runs headless when
|
|
1915
|
-
// messaging is off or unconfigured (see channels/lib/config.mjs).
|
|
1916
|
-
if (!channelsEnabled() && !(await hasActiveAutomation().catch(() => false))) {
|
|
1917
|
-
bootProfile('channels:start-disabled');
|
|
1918
|
-
remoteClaimPending = false;
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
// A backend switch may still be pending or writing asynchronously. Drain
|
|
1922
|
-
// it before the worker reads config; never race it with a sync lock wait.
|
|
1923
|
-
try { await flushBackendSave(); } catch {}
|
|
1924
|
-
// Yield before the createCurrentSession/transcript/fork chain below —
|
|
1925
|
-
// same rationale as the memory-eager-init yield above: this detached
|
|
1926
|
-
// chain runs synchronous config/fs work (createCurrentSession, backend
|
|
1927
|
-
// flush, transcript writer) back-to-back, and without a tick break it
|
|
1928
|
-
// can run ahead of Ink's queued render/input handling.
|
|
1929
|
-
await new Promise((r) => setImmediate(r));
|
|
1930
|
-
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1931
|
-
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1932
|
-
// runs transcript discovery inside its own start(), so publishing the
|
|
1933
|
-
// session record/file first lets that very first discovery pass bind
|
|
1934
|
-
// output forwarding to THIS terminal instead of a persisted/stale
|
|
1935
|
-
// neighbour. Lazy mode means the session may not exist yet at /remote
|
|
1936
|
-
// time; create it here (idempotent — reuses a live session, joins an
|
|
1937
|
-
// in-flight create). On create failure we still claim: that matches
|
|
1938
|
-
// the pre-eager behavior (bind resolves on the first turn's rebind).
|
|
1939
|
-
try { await createCurrentSession('remote-start'); }
|
|
1940
|
-
catch (error) { bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) }); }
|
|
1941
|
-
ensureRemoteTranscriptWriter();
|
|
1942
|
-
// Re-check after the awaits above: stopRemote()/superseded or runtime
|
|
1943
|
-
// close may have landed mid-chain — do not boot/claim for a session
|
|
1944
|
-
// that already turned remote off.
|
|
1945
|
-
if ((!remoteEnabled && !remoteClaimPending) || closeRequested) { remoteClaimPending = false; return; }
|
|
1946
|
-
// Set the fork-inherited intent immediately before the worker fork (the
|
|
1947
|
-
// fork reads process.env synchronously inside invokeChannelStart) and
|
|
1948
|
-
// restore the prior value the instant it resolves, so the pollution
|
|
1949
|
-
// window is just this fork rather than the whole boot chain.
|
|
1950
|
-
const _prevIntent = process.env.MIXDOG_REMOTE_INTENT;
|
|
1951
|
-
try {
|
|
1952
|
-
process.env.MIXDOG_REMOTE_INTENT = intent;
|
|
1953
|
-
await invokeChannelStart();
|
|
1954
|
-
} finally {
|
|
1955
|
-
if (_prevIntent === undefined) delete process.env.MIXDOG_REMOTE_INTENT;
|
|
1956
|
-
else process.env.MIXDOG_REMOTE_INTENT = _prevIntent;
|
|
1957
|
-
}
|
|
1958
|
-
if ((!remoteEnabled && !remoteClaimPending) || closeRequested) { remoteClaimPending = false; return; }
|
|
1959
|
-
// Explicit claims are unconditional. Auto-start dispatches the same
|
|
1960
|
-
// activation as claim-if-vacant so it can promote an existing
|
|
1961
|
-
// automation-only daemon without stealing a live remote owner.
|
|
1962
|
-
await channels.execute('activate_channel_bridge', {
|
|
1963
|
-
active: true,
|
|
1964
|
-
claimIfVacant: intent === 'auto',
|
|
1965
|
-
sessionId: session?.id || remoteSessionId || null,
|
|
1966
|
-
transcriptPath: _transcriptWriter?.transcriptPath || null,
|
|
1967
|
-
});
|
|
1968
|
-
// Claim attempt dispatched; the worker's acquire/supersede notification
|
|
1969
|
-
// now owns the remoteEnabled transition. Drop the transient marker.
|
|
1970
|
-
remoteClaimPending = false;
|
|
1971
|
-
})().catch((error) => {
|
|
1972
|
-
remoteClaimPending = false;
|
|
1973
|
-
if (remoteEnabled) {
|
|
1974
|
-
remoteEnabled = false;
|
|
1975
|
-
remoteSessionId = null;
|
|
1976
|
-
channels.stop('remote-claim-failed').catch(() => {});
|
|
1977
|
-
emitRemoteStateChange(false, 'claim-failed');
|
|
1978
|
-
}
|
|
1979
|
-
bootProfile('channels:claim-failed', { error: error?.message || String(error) });
|
|
1980
|
-
});
|
|
1981
|
-
return true;
|
|
1982
|
-
}
|
|
1423
|
+
// Remote transcript binding + worker rebind pushes live in
|
|
1424
|
+
// remote-transcript.mjs; the facade injects the mutable session/cwd/remote
|
|
1425
|
+
// state it needs.
|
|
1426
|
+
const remoteTranscript = createRemoteTranscript({
|
|
1427
|
+
getSession: () => session,
|
|
1428
|
+
getCwd: () => currentCwd,
|
|
1429
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1430
|
+
getRemoteSessionId: () => remoteSessionId,
|
|
1431
|
+
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1432
|
+
isCloseRequested: () => closeRequested,
|
|
1433
|
+
channelsEnabled,
|
|
1434
|
+
channels,
|
|
1435
|
+
mgr,
|
|
1436
|
+
bootProfile,
|
|
1437
|
+
});
|
|
1438
|
+
const ensureRemoteTranscriptWriter = () => remoteTranscript.ensureRemoteTranscriptWriter();
|
|
1439
|
+
const pushTranscriptRebind = () => remoteTranscript.pushTranscriptRebind();
|
|
1440
|
+
const flushPendingTranscriptRebind = () => remoteTranscript.flushPendingTranscriptRebind();
|
|
1983
1441
|
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
remoteSessionId =
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
await channels.execute('activate_channel_bridge', {
|
|
2000
|
-
active: false,
|
|
2001
|
-
sessionId: releasingSessionId,
|
|
2002
|
-
});
|
|
2003
|
-
} catch (error) {
|
|
2004
|
-
bootProfile('channels:release-failed', { error: error?.message || String(error) });
|
|
2005
|
-
emitRemoteStateChange(false, 'release-failed');
|
|
2006
|
-
} finally {
|
|
2007
|
-
await channels.stop(reason || 'remote-disabled').catch(() => {});
|
|
1442
|
+
// Remote (channel relay) claim/release/stop live in remote-control.mjs; the
|
|
1443
|
+
// facade injects the mutable session + remote state they mutate.
|
|
1444
|
+
const remoteControl = createRemoteControl({
|
|
1445
|
+
getSession: () => session,
|
|
1446
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1447
|
+
setRemoteEnabled: (next) => { remoteEnabled = next; },
|
|
1448
|
+
getRemoteSessionId: () => remoteSessionId,
|
|
1449
|
+
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1450
|
+
isRemoteClaimPending: () => remoteClaimPending,
|
|
1451
|
+
setRemoteClaimPending: (next) => { remoteClaimPending = next; },
|
|
1452
|
+
isCloseRequested: () => closeRequested,
|
|
1453
|
+
clearChannelStartTimer: () => {
|
|
1454
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1455
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1456
|
+
prewarmTimers.channelStartTimer = null;
|
|
2008
1457
|
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
1458
|
+
},
|
|
1459
|
+
remoteTransitions,
|
|
1460
|
+
channels,
|
|
1461
|
+
channelsEnabled,
|
|
1462
|
+
hasActiveAutomation,
|
|
1463
|
+
flushBackendSave,
|
|
1464
|
+
invokeChannelStart,
|
|
1465
|
+
createCurrentSession,
|
|
1466
|
+
ensureRemoteTranscriptWriter,
|
|
1467
|
+
getTranscriptPath: () => remoteTranscript.transcriptWriter?.transcriptPath || null,
|
|
1468
|
+
emitRemoteStateChange,
|
|
1469
|
+
getMemoryModule,
|
|
1470
|
+
bootProfile,
|
|
1471
|
+
envFlag,
|
|
1472
|
+
});
|
|
1473
|
+
const startRemote = (options) => remoteControl.startRemote(options);
|
|
1474
|
+
const releaseRemote = (reason) => remoteControl.releaseRemote(reason);
|
|
1475
|
+
const stopRemote = (reason) => remoteControl.stopRemote(reason);
|
|
2027
1476
|
|
|
2028
1477
|
function isRemoteEnabled() {
|
|
2029
1478
|
return remoteEnabled;
|
|
@@ -2101,7 +1550,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2101
1550
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
2102
1551
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
2103
1552
|
// still start immediately (user-initiated, UI already painted).
|
|
2104
|
-
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2105
1553
|
if (remoteEnabled || remoteAutoStartRequested) {
|
|
2106
1554
|
const remoteStartIntent = remoteEnabled ? 'explicit' : 'auto';
|
|
2107
1555
|
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
@@ -2166,8 +1614,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2166
1614
|
recapEnabledFn,
|
|
2167
1615
|
channelsEnabled,
|
|
2168
1616
|
autoUpdateEnabled,
|
|
2169
|
-
getUpdateCheckState: () =>
|
|
2170
|
-
getUpdateProcessState: () =>
|
|
1617
|
+
getUpdateCheckState: () => selfUpdate.getCheckState(),
|
|
1618
|
+
getUpdateProcessState: () => selfUpdate.getProcessState(),
|
|
2171
1619
|
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
2172
1620
|
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
2173
1621
|
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
@@ -2196,6 +1644,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2196
1644
|
// Automation saved mid-session boots the worker (claim-if-vacant) even
|
|
2197
1645
|
// though the boot-time autostart window has already passed.
|
|
2198
1646
|
ensureAutomationRuntime: () => scheduleChannelStart(0),
|
|
1647
|
+
awaitKeychainPrewarm,
|
|
2199
1648
|
});
|
|
2200
1649
|
const providerAuthApi = createProviderAuthApi({
|
|
2201
1650
|
cfgMod,
|
|
@@ -2204,6 +1653,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2204
1653
|
displayConfig,
|
|
2205
1654
|
reloadFullConfig,
|
|
2206
1655
|
awaitKeychainPrewarm,
|
|
1656
|
+
isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
|
|
1657
|
+
hasProviderSetupCached,
|
|
2207
1658
|
invalidateProviderCaches,
|
|
2208
1659
|
warmProviderModelCache,
|
|
2209
1660
|
refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
|
|
@@ -2211,6 +1662,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2211
1662
|
getUsageDashboard,
|
|
2212
1663
|
collectProviderModels,
|
|
2213
1664
|
});
|
|
1665
|
+
const mediaApi = createMediaApi();
|
|
2214
1666
|
const lifecycleApi = createLifecycleApi({
|
|
2215
1667
|
getSession: () => session,
|
|
2216
1668
|
setSession: (v) => { session = v; },
|
|
@@ -2327,6 +1779,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2327
1779
|
displayConfig,
|
|
2328
1780
|
agentRouteFromConfig,
|
|
2329
1781
|
loadAgentDefinition,
|
|
1782
|
+
listCustomAgentIds,
|
|
2330
1783
|
activeWorkflowId,
|
|
2331
1784
|
listWorkflowPacks,
|
|
2332
1785
|
loadWorkflowPack,
|
|
@@ -2355,8 +1808,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2355
1808
|
getCloseRequested: () => closeRequested,
|
|
2356
1809
|
getPendingSessionReset: () => pendingSessionReset,
|
|
2357
1810
|
setPendingSessionReset: (v) => { pendingSessionReset = v; },
|
|
2358
|
-
getTranscriptWriter: () =>
|
|
2359
|
-
getTwKey: () =>
|
|
1811
|
+
getTranscriptWriter: () => remoteTranscript.transcriptWriter,
|
|
1812
|
+
getTwKey: () => remoteTranscript.transcriptKey,
|
|
2360
1813
|
getLastAppendedAssistant: () => _lastAppendedAssistant,
|
|
2361
1814
|
setLastAppendedAssistant: (v) => { _lastAppendedAssistant = v; },
|
|
2362
1815
|
scheduleCodeGraphPrewarm,
|
|
@@ -2392,6 +1845,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2392
1845
|
...settingsApi,
|
|
2393
1846
|
...channelConfigApi,
|
|
2394
1847
|
...providerAuthApi,
|
|
1848
|
+
...mediaApi,
|
|
2395
1849
|
// Turn-scoped worktree diff (shadow snapshot): everything changed since
|
|
2396
1850
|
// the current turn's base tree, regardless of which agent/process wrote it.
|
|
2397
1851
|
getTurnReviewDiff: () => getTurnSnapshotReviewDiff(currentCwd, session?.id),
|