mixdog 0.9.68 → 0.9.69
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 +6 -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/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +13 -1
- package/src/session-runtime/memory-daemon-probe.mjs +61 -0
- package/src/session-runtime/model-capabilities.mjs +6 -4
- 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 +143 -705
- 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/workflow-agents-api.mjs +206 -3
- 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 +124 -94
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +17 -8
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- 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,7 +227,7 @@ 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';
|
|
242
233
|
import { createProviderAuthApi } from './provider-auth-api.mjs';
|
|
@@ -245,6 +236,13 @@ import { createLifecycleApi } from './lifecycle-api.mjs';
|
|
|
245
236
|
import { createResourceApi } from './resource-api.mjs';
|
|
246
237
|
import { createModelRouteApi } from './model-route-api.mjs';
|
|
247
238
|
import { createWorkflowAgentsApi } from './workflow-agents-api.mjs';
|
|
239
|
+
import { createSelfUpdateController } from './self-update.mjs';
|
|
240
|
+
import { createSkillsApi } from './skills-api.mjs';
|
|
241
|
+
import { createRemoteTranscript } from './remote-transcript.mjs';
|
|
242
|
+
import { createRemoteControl } from './remote-control.mjs';
|
|
243
|
+
import { createNotificationBus } from './notification-bus.mjs';
|
|
244
|
+
import { createToolSurface } from './tool-surface.mjs';
|
|
245
|
+
import { readRuntimeTunables } from './runtime-tunables.mjs';
|
|
248
246
|
import { createSessionTurnApi } from './session-turn-api.mjs';
|
|
249
247
|
import { providerInitCacheKey } from './provider-init-key.mjs';
|
|
250
248
|
import {
|
|
@@ -297,6 +295,7 @@ const {
|
|
|
297
295
|
workflowSummary,
|
|
298
296
|
activeWorkflowSummary,
|
|
299
297
|
loadAgentDefinition,
|
|
298
|
+
listCustomAgentIds,
|
|
300
299
|
workflowContextBlock,
|
|
301
300
|
activeWorkflowContext,
|
|
302
301
|
} = createWorkflowHelpers({
|
|
@@ -334,15 +333,10 @@ export async function createMixdogSessionRuntime({
|
|
|
334
333
|
// dead/replaced owner (clear, delete) hands the seat to the next current
|
|
335
334
|
// session that asks.
|
|
336
335
|
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;
|
|
336
|
+
// Desktop/TUI Remote toggles are desired-state transitions. Serialize the
|
|
337
|
+
// daemon deactivate/detach and reconnect/activate chains so an OFF that is
|
|
338
|
+
// still stopping can never tear down the worker created by the following ON.
|
|
339
|
+
const remoteTransitions = createRemoteTransitionQueue();
|
|
346
340
|
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
347
341
|
// so the post-turn final-content append can skip an exact duplicate.
|
|
348
342
|
let _lastAppendedAssistant = '';
|
|
@@ -370,6 +364,9 @@ export async function createMixdogSessionRuntime({
|
|
|
370
364
|
clearTimeout(timeoutId);
|
|
371
365
|
keychainPrewarmWaitDone = true;
|
|
372
366
|
}
|
|
367
|
+
// Invoked here: the assignment used to store the async FUNCTION, so every
|
|
368
|
+
// `await awaitKeychainPrewarm()` resolved instantly (awaiting a function is
|
|
369
|
+
// a no-op) and callers silently skipped the wait they asked for.
|
|
373
370
|
})();
|
|
374
371
|
return keychainPrewarmWaitPromise;
|
|
375
372
|
}
|
|
@@ -545,50 +542,26 @@ export async function createMixdogSessionRuntime({
|
|
|
545
542
|
...extra,
|
|
546
543
|
};
|
|
547
544
|
}
|
|
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');
|
|
545
|
+
// Env-tunable boot delays and feature gates: runtime-tunables.mjs.
|
|
546
|
+
const {
|
|
547
|
+
sessionPrewarmDelayMs,
|
|
548
|
+
providerSetupWarmupDelayMs,
|
|
549
|
+
modelCatalogWarmupDelayMs,
|
|
550
|
+
providerWarmupDelayMs,
|
|
551
|
+
providerModelWarmupDelayMs,
|
|
552
|
+
codeGraphPrewarmDelayMs,
|
|
553
|
+
statuslineUsageWarmupDelayMs,
|
|
554
|
+
statuslineUsageRefreshDelayMs,
|
|
555
|
+
channelStartDelayMs,
|
|
556
|
+
backgroundBusyRetryMs,
|
|
557
|
+
remoteAutoStartDelayMs,
|
|
558
|
+
sessionPrewarmEnabled,
|
|
559
|
+
providerWarmupEnabled,
|
|
560
|
+
modelPrefetchEnabled,
|
|
561
|
+
codeGraphPrewarmEnabled,
|
|
562
|
+
modelCatalogWarmupEnabled,
|
|
563
|
+
codeGraphPrewarmLazy,
|
|
564
|
+
} = readRuntimeTunables();
|
|
592
565
|
let codeGraphFirstTurnPrewarmDone = false;
|
|
593
566
|
const modelMetaByRoute = new Map();
|
|
594
567
|
const notificationListeners = new Set();
|
|
@@ -640,251 +613,40 @@ export async function createMixdogSessionRuntime({
|
|
|
640
613
|
setDesktopSession: (v) => { desktopSession = v; },
|
|
641
614
|
state: mcpState,
|
|
642
615
|
});
|
|
643
|
-
let preSessionToolSurface = null;
|
|
644
616
|
const hooksStartedAt = performance.now();
|
|
645
617
|
const hooks = createStandaloneHookBus({ dataDir: cfgMod.getPluginData() });
|
|
646
618
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
647
619
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
648
620
|
|
|
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
|
-
}
|
|
621
|
+
// Self-update: registry check + background staging live in self-update.mjs;
|
|
622
|
+
// the facade only wires config/data-dir/notification access into it. The
|
|
623
|
+
// boot check is deferred past this constructor so a hanging registry request
|
|
624
|
+
// can never delay session boot.
|
|
625
|
+
const selfUpdate = createSelfUpdateController({
|
|
626
|
+
getConfig: () => config,
|
|
627
|
+
getDataDir: () => cfgMod.getPluginData?.() || STANDALONE_DATA_DIR,
|
|
628
|
+
emitNotification: (...a) => emitRuntimeNotification(...a),
|
|
629
|
+
});
|
|
630
|
+
const autoUpdateEnabled = () => selfUpdate.autoUpdateEnabled();
|
|
631
|
+
const checkForUpdateInternal = (...a) => selfUpdate.checkForUpdate(...a);
|
|
632
|
+
const runUpdateNowInternal = (...a) => selfUpdate.runUpdateNow();
|
|
633
|
+
selfUpdate.startBootCheck();
|
|
795
634
|
|
|
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
|
-
}
|
|
635
|
+
// Notification fan-out (listener broadcast + pending-queue mirroring of
|
|
636
|
+
// terminal completions) lives in notification-bus.mjs.
|
|
637
|
+
const { emitRuntimeNotification, notifyFnForSession } = createNotificationBus({
|
|
638
|
+
listeners: notificationListeners,
|
|
639
|
+
mgr,
|
|
640
|
+
});
|
|
865
641
|
|
|
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
|
-
}
|
|
642
|
+
// Skill listing/loading/creation lives in skills-api.mjs; the facade only
|
|
643
|
+
// supplies the mutable cwd and the context module.
|
|
644
|
+
const {
|
|
645
|
+
skillsStatus,
|
|
646
|
+
skillContent,
|
|
647
|
+
skillToolContent,
|
|
648
|
+
addProjectSkill,
|
|
649
|
+
} = createSkillsApi({ contextMod, getCwd: () => currentCwd });
|
|
888
650
|
|
|
889
651
|
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
890
652
|
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
@@ -932,52 +694,6 @@ export async function createMixdogSessionRuntime({
|
|
|
932
694
|
STANDALONE_DATA_DIR,
|
|
933
695
|
});
|
|
934
696
|
|
|
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
697
|
const agentToolStartedAt = performance.now();
|
|
982
698
|
const agentTool = createStandaloneAgent({
|
|
983
699
|
cfgMod,
|
|
@@ -1082,25 +798,26 @@ export async function createMixdogSessionRuntime({
|
|
|
1082
798
|
// schema-visible tool that policy always rejects is a guaranteed error turn
|
|
1083
799
|
// (user-reported in Solo). Names derive from the live agent tool defs.
|
|
1084
800
|
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
|
-
|
|
801
|
+
// Lead tool surface (workflow-gated agent tool, pre-session preview, deferred
|
|
802
|
+
// replay) lives in tool-surface.mjs.
|
|
803
|
+
const {
|
|
804
|
+
modelStandaloneTools,
|
|
805
|
+
invalidatePreSessionToolSurface,
|
|
806
|
+
activeToolSurface,
|
|
807
|
+
applyPreSessionToolSelection,
|
|
808
|
+
} = createToolSurface({
|
|
809
|
+
mgr,
|
|
810
|
+
mode,
|
|
811
|
+
standaloneTools,
|
|
812
|
+
agentToolNames,
|
|
813
|
+
getSession: () => session,
|
|
814
|
+
getRoute: () => route,
|
|
815
|
+
getConfig: () => config,
|
|
816
|
+
cfgMod,
|
|
817
|
+
loadWorkflowPack,
|
|
818
|
+
activeWorkflowId,
|
|
819
|
+
dataDir: STANDALONE_DATA_DIR,
|
|
820
|
+
});
|
|
1104
821
|
|
|
1105
822
|
const { contextStatus: computeContextStatus, invalidateContextStatusCache } = createContextStatus({
|
|
1106
823
|
getSession: () => session,
|
|
@@ -1108,34 +825,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1108
825
|
getCurrentCwd: () => currentCwd,
|
|
1109
826
|
getMode: () => mode,
|
|
1110
827
|
});
|
|
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
828
|
internalTools.setInternalToolsProvider({
|
|
1140
829
|
tools: [...standaloneTools, ...searchRuntimeTools.filter((tool) => tool?.public === false)],
|
|
1141
830
|
executor: async (name, args, callerCtx = {}) => {
|
|
@@ -1371,6 +1060,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1371
1060
|
const {
|
|
1372
1061
|
refreshStatuslineUsageSnapshot,
|
|
1373
1062
|
cachedProviderSetup,
|
|
1063
|
+
hasProviderSetupCached,
|
|
1374
1064
|
getUsageDashboard,
|
|
1375
1065
|
} = createProviderUsage({
|
|
1376
1066
|
caches: providerUsageCaches,
|
|
@@ -1717,313 +1407,59 @@ export async function createMixdogSessionRuntime({
|
|
|
1717
1407
|
state: prewarmState,
|
|
1718
1408
|
});
|
|
1719
1409
|
|
|
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
|
-
}
|
|
1410
|
+
// Remote transcript binding + worker rebind pushes live in
|
|
1411
|
+
// remote-transcript.mjs; the facade injects the mutable session/cwd/remote
|
|
1412
|
+
// state it needs.
|
|
1413
|
+
const remoteTranscript = createRemoteTranscript({
|
|
1414
|
+
getSession: () => session,
|
|
1415
|
+
getCwd: () => currentCwd,
|
|
1416
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1417
|
+
getRemoteSessionId: () => remoteSessionId,
|
|
1418
|
+
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1419
|
+
isCloseRequested: () => closeRequested,
|
|
1420
|
+
channelsEnabled,
|
|
1421
|
+
channels,
|
|
1422
|
+
mgr,
|
|
1423
|
+
bootProfile,
|
|
1424
|
+
});
|
|
1425
|
+
const ensureRemoteTranscriptWriter = () => remoteTranscript.ensureRemoteTranscriptWriter();
|
|
1426
|
+
const pushTranscriptRebind = () => remoteTranscript.pushTranscriptRebind();
|
|
1427
|
+
const flushPendingTranscriptRebind = () => remoteTranscript.flushPendingTranscriptRebind();
|
|
1983
1428
|
|
|
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(() => {});
|
|
1429
|
+
// Remote (channel relay) claim/release/stop live in remote-control.mjs; the
|
|
1430
|
+
// facade injects the mutable session + remote state they mutate.
|
|
1431
|
+
const remoteControl = createRemoteControl({
|
|
1432
|
+
getSession: () => session,
|
|
1433
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1434
|
+
setRemoteEnabled: (next) => { remoteEnabled = next; },
|
|
1435
|
+
getRemoteSessionId: () => remoteSessionId,
|
|
1436
|
+
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1437
|
+
isRemoteClaimPending: () => remoteClaimPending,
|
|
1438
|
+
setRemoteClaimPending: (next) => { remoteClaimPending = next; },
|
|
1439
|
+
isCloseRequested: () => closeRequested,
|
|
1440
|
+
clearChannelStartTimer: () => {
|
|
1441
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1442
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1443
|
+
prewarmTimers.channelStartTimer = null;
|
|
2008
1444
|
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
1445
|
+
},
|
|
1446
|
+
remoteTransitions,
|
|
1447
|
+
channels,
|
|
1448
|
+
channelsEnabled,
|
|
1449
|
+
hasActiveAutomation,
|
|
1450
|
+
flushBackendSave,
|
|
1451
|
+
invokeChannelStart,
|
|
1452
|
+
createCurrentSession,
|
|
1453
|
+
ensureRemoteTranscriptWriter,
|
|
1454
|
+
getTranscriptPath: () => remoteTranscript.transcriptWriter?.transcriptPath || null,
|
|
1455
|
+
emitRemoteStateChange,
|
|
1456
|
+
getMemoryModule,
|
|
1457
|
+
bootProfile,
|
|
1458
|
+
envFlag,
|
|
1459
|
+
});
|
|
1460
|
+
const startRemote = (options) => remoteControl.startRemote(options);
|
|
1461
|
+
const releaseRemote = (reason) => remoteControl.releaseRemote(reason);
|
|
1462
|
+
const stopRemote = (reason) => remoteControl.stopRemote(reason);
|
|
2027
1463
|
|
|
2028
1464
|
function isRemoteEnabled() {
|
|
2029
1465
|
return remoteEnabled;
|
|
@@ -2101,7 +1537,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2101
1537
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
2102
1538
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
2103
1539
|
// 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
1540
|
if (remoteEnabled || remoteAutoStartRequested) {
|
|
2106
1541
|
const remoteStartIntent = remoteEnabled ? 'explicit' : 'auto';
|
|
2107
1542
|
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
@@ -2166,8 +1601,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2166
1601
|
recapEnabledFn,
|
|
2167
1602
|
channelsEnabled,
|
|
2168
1603
|
autoUpdateEnabled,
|
|
2169
|
-
getUpdateCheckState: () =>
|
|
2170
|
-
getUpdateProcessState: () =>
|
|
1604
|
+
getUpdateCheckState: () => selfUpdate.getCheckState(),
|
|
1605
|
+
getUpdateProcessState: () => selfUpdate.getProcessState(),
|
|
2171
1606
|
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
2172
1607
|
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
2173
1608
|
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
@@ -2204,6 +1639,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2204
1639
|
displayConfig,
|
|
2205
1640
|
reloadFullConfig,
|
|
2206
1641
|
awaitKeychainPrewarm,
|
|
1642
|
+
isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
|
|
1643
|
+
hasProviderSetupCached,
|
|
2207
1644
|
invalidateProviderCaches,
|
|
2208
1645
|
warmProviderModelCache,
|
|
2209
1646
|
refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
|
|
@@ -2327,6 +1764,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2327
1764
|
displayConfig,
|
|
2328
1765
|
agentRouteFromConfig,
|
|
2329
1766
|
loadAgentDefinition,
|
|
1767
|
+
listCustomAgentIds,
|
|
2330
1768
|
activeWorkflowId,
|
|
2331
1769
|
listWorkflowPacks,
|
|
2332
1770
|
loadWorkflowPack,
|
|
@@ -2355,8 +1793,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2355
1793
|
getCloseRequested: () => closeRequested,
|
|
2356
1794
|
getPendingSessionReset: () => pendingSessionReset,
|
|
2357
1795
|
setPendingSessionReset: (v) => { pendingSessionReset = v; },
|
|
2358
|
-
getTranscriptWriter: () =>
|
|
2359
|
-
getTwKey: () =>
|
|
1796
|
+
getTranscriptWriter: () => remoteTranscript.transcriptWriter,
|
|
1797
|
+
getTwKey: () => remoteTranscript.transcriptKey,
|
|
2360
1798
|
getLastAppendedAssistant: () => _lastAppendedAssistant,
|
|
2361
1799
|
setLastAppendedAssistant: (v) => { _lastAppendedAssistant = v; },
|
|
2362
1800
|
scheduleCodeGraphPrewarm,
|