mixdog 0.9.67 → 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/session-lifecycle.mjs +3 -0
- 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-summary-index.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
- 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/config.mjs +6 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
- package/src/runtime/channels/lib/scheduler.mjs +7 -15
- package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +36 -46
- package/src/runtime/channels/lib/worker-main.mjs +45 -92
- package/src/runtime/shared/automation-attachments.mjs +72 -0
- package/src/runtime/shared/automation-workflow.mjs +41 -0
- package/src/runtime/shared/schedule-session-run.mjs +10 -1
- package/src/runtime/shared/schedules-db.mjs +18 -3
- package/src/runtime/shared/webhook-session-run.mjs +57 -0
- package/src/runtime/shared/webhooks-db.mjs +19 -3
- package/src/session-runtime/channel-config-api.mjs +8 -1
- package/src/session-runtime/lifecycle-api.mjs +7 -0
- 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 +184 -660
- 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-admin.mjs +36 -1
- package/src/standalone/channel-daemon-client.mjs +5 -1
- package/src/standalone/channel-daemon-transport.mjs +112 -20
- package/src/standalone/channel-daemon.mjs +6 -4
- package/src/standalone/channel-worker.mjs +7 -13
- package/src/tui/App.jsx +2 -32
- package/src/tui/app/channel-pickers.mjs +2 -143
- package/src/tui/app/slash-commands.mjs +1 -3
- package/src/tui/app/slash-dispatch.mjs +3 -3
- package/src/tui/components/StatusLine.jsx +6 -5
- package/src/tui/dist/index.mjs +162 -259
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +45 -10
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- package/src/tui/engine.mjs +14 -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,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({
|
|
@@ -329,15 +328,15 @@ export async function createMixdogSessionRuntime({
|
|
|
329
328
|
// past its remoteEnabled guards without prematurely showing this session as
|
|
330
329
|
// remote (single-holder: a live owner must not be stolen by autoStart).
|
|
331
330
|
let remoteClaimPending = false;
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
331
|
+
// SESSION-SCOPED remote (user decision): the session that toggled remote
|
|
332
|
+
// OWNS the channel relay. Other sessions' turns never rebind or relay; a
|
|
333
|
+
// dead/replaced owner (clear, delete) hands the seat to the next current
|
|
334
|
+
// session that asks.
|
|
335
|
+
let remoteSessionId = null;
|
|
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();
|
|
341
340
|
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
342
341
|
// so the post-turn final-content append can skip an exact duplicate.
|
|
343
342
|
let _lastAppendedAssistant = '';
|
|
@@ -365,6 +364,9 @@ export async function createMixdogSessionRuntime({
|
|
|
365
364
|
clearTimeout(timeoutId);
|
|
366
365
|
keychainPrewarmWaitDone = true;
|
|
367
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.
|
|
368
370
|
})();
|
|
369
371
|
return keychainPrewarmWaitPromise;
|
|
370
372
|
}
|
|
@@ -540,50 +542,26 @@ export async function createMixdogSessionRuntime({
|
|
|
540
542
|
...extra,
|
|
541
543
|
};
|
|
542
544
|
}
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const providerWarmupEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
564
|
-
&& (
|
|
565
|
-
envFlag('MIXDOG_ENABLE_PROVIDER_WARMUP')
|
|
566
|
-
|| envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')
|
|
567
|
-
|| envPresent('MIXDOG_PROVIDER_WARMUP_DELAY_MS')
|
|
568
|
-
|| envPresent('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS')
|
|
569
|
-
);
|
|
570
|
-
// Boot-time model-catalog prefetch is intentionally decoupled from the
|
|
571
|
-
// heavier providerWarmupEnabled gate (which stays opt-in for provider
|
|
572
|
-
// *init* side effects). Fetching the model list in the background after a
|
|
573
|
-
// short delay is cheap, fire-and-forget, and unref'd, so it is ON by
|
|
574
|
-
// default — otherwise the FIRST `/model` open always paid a cold full
|
|
575
|
-
// network load. Operators can still disable it explicitly.
|
|
576
|
-
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
577
|
-
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
578
|
-
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
579
|
-
const modelCatalogWarmupEnabled = !envFlag('MIXDOG_DISABLE_MODEL_CATALOG_WARMUP');
|
|
580
|
-
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
581
|
-
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
582
|
-
// burned a worker (and felt like a freeze) before the user did anything.
|
|
583
|
-
// Instead prewarm ONCE on the first real turn, when a code lookup is actually
|
|
584
|
-
// imminent. Operators who want the old eager behavior can set
|
|
585
|
-
// MIXDOG_CODE_GRAPH_PREWARM_EAGER=1.
|
|
586
|
-
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();
|
|
587
565
|
let codeGraphFirstTurnPrewarmDone = false;
|
|
588
566
|
const modelMetaByRoute = new Map();
|
|
589
567
|
const notificationListeners = new Set();
|
|
@@ -635,251 +613,40 @@ export async function createMixdogSessionRuntime({
|
|
|
635
613
|
setDesktopSession: (v) => { desktopSession = v; },
|
|
636
614
|
state: mcpState,
|
|
637
615
|
});
|
|
638
|
-
let preSessionToolSurface = null;
|
|
639
616
|
const hooksStartedAt = performance.now();
|
|
640
617
|
const hooks = createStandaloneHookBus({ dataDir: cfgMod.getPluginData() });
|
|
641
618
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
642
619
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
643
620
|
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
658
|
-
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
659
|
-
// Boot detects an available update and STAGES it in the background (a hidden,
|
|
660
|
-
// detached npm install into ~/.mixdog/data/staging/<ver>). The staged package
|
|
661
|
-
// is swapped into the global dir on the next clean launch (cli.mjs
|
|
662
|
-
// pre-import), never while a session is live — so npm never overwrites the
|
|
663
|
-
// .mjs files node currently holds (the old shutdown `npm install -g` did, and
|
|
664
|
-
// caused Windows TAR_ENTRY_ERROR / ENOENT). The live-session refcount that a
|
|
665
|
-
// concurrent launch consults to defer its swap is registered earlier, in
|
|
666
|
-
// cli.mjs (pre-import), so this process is visible before any runtime loads.
|
|
667
|
-
|
|
668
|
-
function autoUpdateEnabled() {
|
|
669
|
-
return config?.update?.auto !== false;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
async function checkForUpdateInternal({ force = false } = {}) {
|
|
673
|
-
if (updateProcessState.phase !== 'installing') updateProcessState.phase = 'checking';
|
|
674
|
-
try {
|
|
675
|
-
const result = await checkLatestVersion({ force, dataDir: cfgMod.getPluginData?.() || STANDALONE_DATA_DIR });
|
|
676
|
-
updateCheckState = {
|
|
677
|
-
currentVersion: result.currentVersion,
|
|
678
|
-
latestVersion: result.latestVersion,
|
|
679
|
-
updateAvailable: result.updateAvailable,
|
|
680
|
-
lastCheckedAt: result.lastCheckedAt,
|
|
681
|
-
};
|
|
682
|
-
} catch {
|
|
683
|
-
// checkLatestVersion() is already silent-safe; this catch is belt-and-
|
|
684
|
-
// braces so a boot-time call can never crash the runtime.
|
|
685
|
-
} finally {
|
|
686
|
-
if (updateProcessState.phase === 'checking') updateProcessState.phase = 'idle';
|
|
687
|
-
}
|
|
688
|
-
return updateCheckState;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async function runUpdateNowInternal() {
|
|
692
|
-
if (updateProcessState.phase === 'installing') {
|
|
693
|
-
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
694
|
-
}
|
|
695
|
-
if (isDevInstall()) {
|
|
696
|
-
updateProcessState = { phase: 'failed', version: null, error: 'dev install — update skipped' };
|
|
697
|
-
return updateProcessState;
|
|
698
|
-
}
|
|
699
|
-
const ver = updateCheckState.latestVersion;
|
|
700
|
-
if (!ver || !updateCheckState.updateAvailable) {
|
|
701
|
-
updateProcessState = { phase: 'idle', version: null, error: null };
|
|
702
|
-
return { ...updateProcessState, error: 'no update available' };
|
|
703
|
-
}
|
|
704
|
-
// "Update now" stages the new version (verified, self-contained) rather than
|
|
705
|
-
// installing over the live global dir; the swap applies on the next launch.
|
|
706
|
-
// phase 'installed' here means "staged & ready — restart to apply".
|
|
707
|
-
updateProcessState = { phase: 'installing', version: ver, error: null };
|
|
708
|
-
try {
|
|
709
|
-
const result = await runStagedInstall(ver);
|
|
710
|
-
if (result?.ok) {
|
|
711
|
-
updateProcessState = { phase: 'installed', version: result.version || ver, error: null };
|
|
712
|
-
} else {
|
|
713
|
-
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
714
|
-
}
|
|
715
|
-
} catch (err) {
|
|
716
|
-
updateProcessState = { phase: 'failed', version: null, error: err?.message || String(err) };
|
|
717
|
-
}
|
|
718
|
-
return updateProcessState;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
722
|
-
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
723
|
-
// slow/hanging registry request can never delay session boot. The check
|
|
724
|
-
// ALWAYS runs (populates updateCheckState for the maintenance picker), but
|
|
725
|
-
// when an update is available it kicks off a hidden BACKGROUND staging
|
|
726
|
-
// install (spawnStagedInstall) into ~/.mixdog/data/staging/<ver>. The actual
|
|
727
|
-
// swap into the global dir happens on the next clean launch (cli.mjs
|
|
728
|
-
// pre-import), so npm never overwrites files this live process holds.
|
|
729
|
-
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
730
|
-
// stale-visible: it kept reporting an older "latest" than the installed
|
|
731
|
-
// version). checkLatestVersion() still falls back to the cache offline.
|
|
732
|
-
// isDevInstall() gate: a git checkout / clone (or non-node_modules install)
|
|
733
|
-
// must never self-update — staging + swap would fight the working tree.
|
|
734
|
-
const updateBootTimer = setTimeout(() => {
|
|
735
|
-
void (async () => {
|
|
736
|
-
await checkForUpdateInternal({ force: true });
|
|
737
|
-
if (!(autoUpdateEnabled() && !isDevInstall() && updateCheckState.updateAvailable)) return;
|
|
738
|
-
const ver = updateCheckState.latestVersion;
|
|
739
|
-
if (!ver) return;
|
|
740
|
-
// The notice fires ONLY once staging has completed (a ready-to-apply
|
|
741
|
-
// package sits on disk) — never upfront — so the user sees no "update
|
|
742
|
-
// available / installs on quit" nag while the background stage runs
|
|
743
|
-
// silently. The wording lives in the notice surface (notification-plan):
|
|
744
|
-
// this emit only carries meta.version. TUI maps meta.kind 'update-notice'
|
|
745
|
-
// to a transient notice, never a model-visible message; tone 'info' =
|
|
746
|
-
// non-urgent, applies on the next launch.
|
|
747
|
-
const announceReady = () => {
|
|
748
|
-
emitRuntimeNotification('update ready', { kind: 'update-notice', version: ver, tone: 'info' });
|
|
749
|
-
};
|
|
750
|
-
// Already staged in a prior session → announce immediately.
|
|
751
|
-
if (isStagedComplete(ver)) { announceReady(); return; }
|
|
752
|
-
try { spawnStagedInstall(ver); } catch { /* best-effort background stage */ }
|
|
753
|
-
// Poll for staging completion, then announce once. The interval is
|
|
754
|
-
// unref'd so it never holds the process open, and gives up silently
|
|
755
|
-
// after the cap — the next launch retries.
|
|
756
|
-
const POLL_MS = 3_000;
|
|
757
|
-
const MAX_MS = 10 * 60 * 1000;
|
|
758
|
-
const startedAt = Date.now();
|
|
759
|
-
const poll = setInterval(() => {
|
|
760
|
-
if (isStagedComplete(ver)) {
|
|
761
|
-
clearInterval(poll);
|
|
762
|
-
announceReady();
|
|
763
|
-
} else if (Date.now() - startedAt > MAX_MS) {
|
|
764
|
-
clearInterval(poll);
|
|
765
|
-
}
|
|
766
|
-
}, POLL_MS);
|
|
767
|
-
poll.unref?.();
|
|
768
|
-
})().catch(() => {});
|
|
769
|
-
}, 0);
|
|
770
|
-
updateBootTimer.unref?.();
|
|
771
|
-
|
|
772
|
-
function emitRuntimeNotification(content, meta = {}) {
|
|
773
|
-
const text = String(content || '').trim();
|
|
774
|
-
if (!text) return { handled: false, modelVisibleDelivered: false };
|
|
775
|
-
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
776
|
-
let handled = false;
|
|
777
|
-
for (const listener of [...notificationListeners]) {
|
|
778
|
-
try {
|
|
779
|
-
if (listener(event) === true) handled = true;
|
|
780
|
-
} catch {}
|
|
781
|
-
}
|
|
782
|
-
// EXPLICIT model-visible-delivery ack: only the TUI execution-ui path sets
|
|
783
|
-
// event.modelVisibleDelivered when it enqueues the model-visible completion
|
|
784
|
-
// body into the active loop. A generic display-only / API listener that
|
|
785
|
-
// returns true does NOT set it, so `handled` alone must never be read as
|
|
786
|
-
// "the model saw the body".
|
|
787
|
-
const modelVisibleDelivered = event.modelVisibleDelivered === true;
|
|
788
|
-
return { handled, modelVisibleDelivered };
|
|
789
|
-
}
|
|
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();
|
|
790
634
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// on another path (background reconcile/fallback/notify) within THIS
|
|
798
|
-
// process is skipped instead of double-injecting the same body next turn.
|
|
799
|
-
if (modelVisibleDelivered) {
|
|
800
|
-
try {
|
|
801
|
-
const deliveredVisible = modelVisibleToolCompletionMessage(text, meta);
|
|
802
|
-
recordDeliveredCompletion({ executionId: meta?.execution_id, text: deliveredVisible });
|
|
803
|
-
} catch {}
|
|
804
|
-
}
|
|
805
|
-
// TUI sessions consume raw execution notifications for UI/task cards via
|
|
806
|
-
// onNotification, but those raw envelopes are internal-only in pending
|
|
807
|
-
// drain. The TUI execution-ui path injects the model-visible twin of a
|
|
808
|
-
// terminal completion into the active loop and acks with
|
|
809
|
-
// modelVisibleDelivered, so mirroring it into the pending queue would
|
|
810
|
-
// double-inject the same completion — skip only on that EXPLICIT ack. A
|
|
811
|
-
// generic display-only / API listener returning true is NOT an ack: the
|
|
812
|
-
// mirror stays as the sole model-visible delivery.
|
|
813
|
-
if (shouldMirrorCompletionToPendingQueue({
|
|
814
|
-
callerSessionId,
|
|
815
|
-
modelVisibleDelivered,
|
|
816
|
-
hasEnqueue: typeof mgr.enqueuePendingMessage === 'function',
|
|
817
|
-
text,
|
|
818
|
-
meta,
|
|
819
|
-
})) {
|
|
820
|
-
try {
|
|
821
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
822
|
-
// Terminal completion (gated by shouldPersistModelVisibleToolCompletion)
|
|
823
|
-
// → tag so drain discards it on resume rather than replaying out-of-order.
|
|
824
|
-
if (visible) {
|
|
825
|
-
if (isDeliveredCompletion({ executionId: meta?.execution_id, text: visible })) {
|
|
826
|
-
// Already delivered+ACKed on the TUI path → suppress the mirror but
|
|
827
|
-
// report DELIVERED so the caller marks it notified and never retries.
|
|
828
|
-
logDuplicateSkip('mirror', { executionId: meta?.execution_id, text: visible });
|
|
829
|
-
enqueued = true;
|
|
830
|
-
} else {
|
|
831
|
-
enqueued = mgr.enqueuePendingMessage(callerSessionId, markCompletionEntry(visible)) > 0;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
} catch {}
|
|
835
|
-
}
|
|
836
|
-
// Headless/API listeners may exist but not consume the event; preserve
|
|
837
|
-
// the old fallback for non-terminal notifications only when unhandled.
|
|
838
|
-
if (!enqueued && !handledByRuntimeListener && callerSessionId
|
|
839
|
-
&& typeof mgr.enqueuePendingMessage === 'function') {
|
|
840
|
-
try {
|
|
841
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
842
|
-
// modelVisibleToolCompletionMessage only returns non-empty for a
|
|
843
|
-
// persistable terminal completion, so this fallback is a completion
|
|
844
|
-
// too → tag it (genuine non-completion notifications yield '' above).
|
|
845
|
-
if (visible) {
|
|
846
|
-
if (isDeliveredCompletion({ executionId: meta?.execution_id, text: visible })) {
|
|
847
|
-
// Already delivered+ACKed on the TUI path → suppress the fallback
|
|
848
|
-
// enqueue but report DELIVERED so the caller stops retrying.
|
|
849
|
-
logDuplicateSkip('fallback', { executionId: meta?.execution_id, text: visible });
|
|
850
|
-
enqueued = true;
|
|
851
|
-
} else {
|
|
852
|
-
enqueued = mgr.enqueuePendingMessage(callerSessionId, markCompletionEntry(visible)) > 0;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
} catch {}
|
|
856
|
-
}
|
|
857
|
-
return enqueued || handledByRuntimeListener;
|
|
858
|
-
};
|
|
859
|
-
}
|
|
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
|
+
});
|
|
860
641
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
if (cwdNorm && path.startsWith(`${cwdNorm}/.mixdog/skills/`)) return 'project';
|
|
870
|
-
return 'skill';
|
|
871
|
-
};
|
|
872
|
-
return {
|
|
873
|
-
cwd: currentCwd,
|
|
874
|
-
count: skills.length,
|
|
875
|
-
skills: skills.map((skill) => ({
|
|
876
|
-
name: skill.name,
|
|
877
|
-
description: skill.description || '',
|
|
878
|
-
filePath: skill.filePath || null,
|
|
879
|
-
source: sourceForSkill(skill.filePath),
|
|
880
|
-
})),
|
|
881
|
-
};
|
|
882
|
-
}
|
|
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 });
|
|
883
650
|
|
|
884
651
|
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
885
652
|
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
@@ -927,52 +694,6 @@ export async function createMixdogSessionRuntime({
|
|
|
927
694
|
STANDALONE_DATA_DIR,
|
|
928
695
|
});
|
|
929
696
|
|
|
930
|
-
function skillContent(name) {
|
|
931
|
-
const res = typeof contextMod.loadSkillResource === 'function'
|
|
932
|
-
? contextMod.loadSkillResource(name, currentCwd)
|
|
933
|
-
: null;
|
|
934
|
-
if (!res) throw new Error(`skill not found: ${name}`);
|
|
935
|
-
return { name, content: res.content, dir: res.dir };
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function skillToolContent(name) {
|
|
939
|
-
if (typeof contextMod.isSkillDisabled === 'function' && contextMod.isSkillDisabled(name)) {
|
|
940
|
-
const label = String(name || '').trim() || 'skill';
|
|
941
|
-
return `Error: skill "${label}" is disabled`;
|
|
942
|
-
}
|
|
943
|
-
const skill = skillContent(name);
|
|
944
|
-
// Return the general tool envelope so the main/Lead session behaves the
|
|
945
|
-
// same as agent-loop sessions: the model-visible tool_result is the short
|
|
946
|
-
// stub (`Loaded skill: <name>`) and the full SKILL.md body is delivered
|
|
947
|
-
// ONCE as a separate injected role:'user' message (newMessages). The
|
|
948
|
-
// envelope passes through internal-tools._normalize untouched (it preserves
|
|
949
|
-
// __toolEnvelope objects), and the agent loop's central normalizeToolEnvelope
|
|
950
|
-
// splits it into stub + injected user body.
|
|
951
|
-
return contextMod.buildSkillToolEnvelope(skill.name, skill.content, skill.dir);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function addProjectSkill(input = {}) {
|
|
955
|
-
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
956
|
-
if (!name) throw new Error('skill name is required');
|
|
957
|
-
const dir = join(currentCwd, '.mixdog', 'skills', name);
|
|
958
|
-
const filePath = join(dir, 'SKILL.md');
|
|
959
|
-
if (existsSync(filePath)) throw new Error(`skill already exists: ${name}`);
|
|
960
|
-
const description = clean(input.description) || 'Project skill.';
|
|
961
|
-
mkdirSync(dir, { recursive: true });
|
|
962
|
-
writeFileSync(filePath, [
|
|
963
|
-
'---',
|
|
964
|
-
`name: ${name}`,
|
|
965
|
-
`description: ${description}`,
|
|
966
|
-
'---',
|
|
967
|
-
'',
|
|
968
|
-
'# Instructions',
|
|
969
|
-
'',
|
|
970
|
-
'Describe when and how to use this skill.',
|
|
971
|
-
'',
|
|
972
|
-
].join('\n'), 'utf8');
|
|
973
|
-
return { name, filePath };
|
|
974
|
-
}
|
|
975
|
-
|
|
976
697
|
const agentToolStartedAt = performance.now();
|
|
977
698
|
const agentTool = createStandaloneAgent({
|
|
978
699
|
cfgMod,
|
|
@@ -1032,6 +753,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1032
753
|
// would re-fork/activate). Idempotent: no-op when already enabled.
|
|
1033
754
|
if (msg?.params?.state === 'acquired' && !remoteEnabled) {
|
|
1034
755
|
remoteEnabled = true;
|
|
756
|
+
if (!remoteSessionId) remoteSessionId = session?.id || null;
|
|
1035
757
|
ensureRemoteTranscriptWriter();
|
|
1036
758
|
// Auto-acquire: the worker restored yesterday's transcript from
|
|
1037
759
|
// persisted status and we just created the CURRENT writer. Push the
|
|
@@ -1076,25 +798,26 @@ export async function createMixdogSessionRuntime({
|
|
|
1076
798
|
// schema-visible tool that policy always rejects is a guaranteed error turn
|
|
1077
799
|
// (user-reported in Solo). Names derive from the live agent tool defs.
|
|
1078
800
|
const agentToolNames = new Set(agentTool.tools.map((tool) => String(tool?.name || '')).filter(Boolean));
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
+
});
|
|
1098
821
|
|
|
1099
822
|
const { contextStatus: computeContextStatus, invalidateContextStatusCache } = createContextStatus({
|
|
1100
823
|
getSession: () => session,
|
|
@@ -1102,34 +825,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1102
825
|
getCurrentCwd: () => currentCwd,
|
|
1103
826
|
getMode: () => mode,
|
|
1104
827
|
});
|
|
1105
|
-
|
|
1106
|
-
function buildPreSessionToolSurface() {
|
|
1107
|
-
const previewTools = typeof mgr.previewSessionTools === 'function'
|
|
1108
|
-
? mgr.previewSessionTools(toolSpecForMode(mode), [])
|
|
1109
|
-
: [];
|
|
1110
|
-
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
1111
|
-
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
1112
|
-
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), modelStandaloneTools(), { provider: route.provider });
|
|
1113
|
-
return surface;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function activeToolSurface() {
|
|
1117
|
-
if (session) return session;
|
|
1118
|
-
preSessionToolSurface ??= buildPreSessionToolSurface();
|
|
1119
|
-
return preSessionToolSurface;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function applyPreSessionToolSelection() {
|
|
1123
|
-
if (!session || !preSessionToolSurface) return;
|
|
1124
|
-
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
1125
|
-
? preSessionToolSurface.deferredSelectedTools
|
|
1126
|
-
: [];
|
|
1127
|
-
const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
|
|
1128
|
-
? preSessionToolSurface.deferredDiscoveredTools
|
|
1129
|
-
: [];
|
|
1130
|
-
const replay = [...new Set([...selected, ...discovered])];
|
|
1131
|
-
if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
|
|
1132
|
-
}
|
|
1133
828
|
internalTools.setInternalToolsProvider({
|
|
1134
829
|
tools: [...standaloneTools, ...searchRuntimeTools.filter((tool) => tool?.public === false)],
|
|
1135
830
|
executor: async (name, args, callerCtx = {}) => {
|
|
@@ -1365,6 +1060,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1365
1060
|
const {
|
|
1366
1061
|
refreshStatuslineUsageSnapshot,
|
|
1367
1062
|
cachedProviderSetup,
|
|
1063
|
+
hasProviderSetupCached,
|
|
1368
1064
|
getUsageDashboard,
|
|
1369
1065
|
} = createProviderUsage({
|
|
1370
1066
|
caches: providerUsageCaches,
|
|
@@ -1711,260 +1407,59 @@ export async function createMixdogSessionRuntime({
|
|
|
1711
1407
|
state: prewarmState,
|
|
1712
1408
|
});
|
|
1713
1409
|
|
|
1714
|
-
//
|
|
1715
|
-
//
|
|
1716
|
-
//
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
_twKey = twKey;
|
|
1733
|
-
} catch (error) {
|
|
1734
|
-
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
1735
|
-
_transcriptWriter = null;
|
|
1736
|
-
_twKey = '';
|
|
1737
|
-
return false;
|
|
1738
|
-
}
|
|
1739
|
-
} else {
|
|
1740
|
-
// Same binding — refresh updatedAt so worker-side discovery keeps
|
|
1741
|
-
// ranking this session as the live parent-chain candidate.
|
|
1742
|
-
try { _transcriptWriter?.writeSessionRecord(); } catch {}
|
|
1743
|
-
}
|
|
1744
|
-
try { _transcriptWriter?.ensureTranscriptFile(); }
|
|
1745
|
-
catch (error) { process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`); }
|
|
1746
|
-
return _transcriptWriter != null;
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
// Push the CURRENT transcript path to the channel worker so outbound
|
|
1750
|
-
// forwarding repoints immediately at the moments the binding can go stale
|
|
1751
|
-
// (auto-acquire, newSession/resume, clear) instead of waiting for the next
|
|
1752
|
-
// inbound parent-chain steal. Best-effort: ensures the writer, then fires
|
|
1753
|
-
// the dedicated idempotent worker op — a missing/not-ready worker or a bind
|
|
1754
|
-
// failure must never throw into the lead paths that call this.
|
|
1755
|
-
function pushTranscriptRebind() {
|
|
1756
|
-
if (!remoteEnabled) return;
|
|
1757
|
-
// Writer not bindable yet (e.g. 'acquired' before the session exists in
|
|
1758
|
-
// lazy mode): defer instead of silently dropping the push. flushPending-
|
|
1759
|
-
// TranscriptRebind() re-fires this exactly once when the writer is ready.
|
|
1760
|
-
if (!ensureRemoteTranscriptWriter()) { _pendingRebind = true; return; }
|
|
1761
|
-
const transcriptPath = _transcriptWriter?.transcriptPath;
|
|
1762
|
-
if (!transcriptPath || !channelsEnabled()) { _pendingRebind = true; return; }
|
|
1763
|
-
_pendingRebind = false;
|
|
1764
|
-
executeTranscriptRebind(transcriptPath, 1);
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// Fire the idempotent worker op with bounded retry. A rejected/throwing
|
|
1768
|
-
// channels.execute retries a few times with short backoff; the final failure
|
|
1769
|
-
// surfaces one stderr line (not only the env-gated bootProfile) so a lost
|
|
1770
|
-
// rebind is diagnosable by default. Best-effort throughout — never throws
|
|
1771
|
-
// into the lead paths that call pushTranscriptRebind().
|
|
1772
|
-
function executeTranscriptRebind(transcriptPath, attempt) {
|
|
1773
|
-
const maxAttempts = 3;
|
|
1774
|
-
const onError = (error) => {
|
|
1775
|
-
const detail = error?.message || String(error);
|
|
1776
|
-
bootProfile('channels:rebind-push-failed', { attempt, error: detail });
|
|
1777
|
-
if (attempt < maxAttempts && remoteEnabled && !closeRequested) {
|
|
1778
|
-
const timer = setTimeout(() => {
|
|
1779
|
-
// Abort the retry chain silently if remote was dropped or the writer
|
|
1780
|
-
// moved on (supersede→re-acquire, newSession/clear): re-firing the
|
|
1781
|
-
// captured path would rebind forwarding back to a stale transcript.
|
|
1782
|
-
if (!remoteEnabled || !channelsEnabled()) return;
|
|
1783
|
-
if (_transcriptWriter?.transcriptPath !== transcriptPath) return;
|
|
1784
|
-
executeTranscriptRebind(transcriptPath, attempt + 1);
|
|
1785
|
-
}, 150 * attempt);
|
|
1786
|
-
timer.unref?.();
|
|
1787
|
-
} else {
|
|
1788
|
-
process.stderr.write(`mixdog: channels: rebind_current_transcript failed after ${attempt} attempt(s): ${detail}\n`);
|
|
1789
|
-
}
|
|
1790
|
-
};
|
|
1791
|
-
try {
|
|
1792
|
-
void channels.execute('rebind_current_transcript', { transcriptPath }).catch(onError);
|
|
1793
|
-
} catch (error) {
|
|
1794
|
-
onError(error);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Re-fire a deferred rebind exactly once, after a session/writer becomes
|
|
1799
|
-
// available (session create or turn start). No-op unless a push was deferred,
|
|
1800
|
-
// so no unconditional rebind fires per turn for already-bound sessions.
|
|
1801
|
-
function flushPendingTranscriptRebind() {
|
|
1802
|
-
if (!_pendingRebind || !remoteEnabled) return;
|
|
1803
|
-
pushTranscriptRebind();
|
|
1804
|
-
}
|
|
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();
|
|
1805
1428
|
|
|
1806
|
-
// Remote (
|
|
1807
|
-
//
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
// Auto-start (config/delayed): do NOT flip this session to remote up
|
|
1822
|
-
// front. Boot the worker to ATTEMPT a claim-if-vacant; remoteEnabled is
|
|
1823
|
-
// set only when the worker reports it actually acquired the seat (the
|
|
1824
|
-
// 'acquired' notification). A live owner already holding the seat makes
|
|
1825
|
-
// the worker back off silently and this session stays non-remote.
|
|
1826
|
-
remoteClaimPending = true;
|
|
1827
|
-
} else {
|
|
1828
|
-
remoteEnabled = true;
|
|
1829
|
-
}
|
|
1830
|
-
// Boot the memory daemon eagerly. The channels worker forwards
|
|
1831
|
-
// transcript ingests/entries to the memory HTTP service, whose port is
|
|
1832
|
-
// published to active-instance.json by getMemoryModule().init(). Without
|
|
1833
|
-
// this, memory only starts on the first turn's getMemoryModule() call —
|
|
1834
|
-
// so early channel traffic finds no memory_port and gets buffered (or,
|
|
1835
|
-
// pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
|
|
1836
|
-
// racing to be live by the time the worker sends its first ingest.
|
|
1837
|
-
//
|
|
1838
|
-
// Not fire-and-forget: init() only resolves the module handle, so a bare
|
|
1839
|
-
// getMemoryModule() could return before /health reports ok — leaving early
|
|
1840
|
-
// ingests to hit a not-yet-listening port. Await the proxy start() and then
|
|
1841
|
-
// poll /health with a bounded retry so the daemon is provably reachable
|
|
1842
|
-
// (or logged failed) rather than assumed-up. Still non-blocking to the
|
|
1843
|
-
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1844
|
-
void (async () => {
|
|
1845
|
-
try {
|
|
1846
|
-
// Yield one event-loop tick before the heavy chain below (module
|
|
1847
|
-
// resolve, daemon fork/health-poll) starts, so Ink's next render
|
|
1848
|
-
// (scheduled via setImmediate/timers) and any queued keypress
|
|
1849
|
-
// events get a turn first instead of being starved by this
|
|
1850
|
-
// detached chain's synchronous setup work.
|
|
1851
|
-
await new Promise((r) => setImmediate(r));
|
|
1852
|
-
const mod = await getMemoryModule();
|
|
1853
|
-
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1854
|
-
const port = started?.port;
|
|
1855
|
-
if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
|
|
1856
|
-
for (let i = 0; i < 30; i++) {
|
|
1857
|
-
try {
|
|
1858
|
-
const ok = await new Promise((res) => {
|
|
1859
|
-
const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
|
|
1860
|
-
let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
|
|
1861
|
-
try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
|
|
1862
|
-
});
|
|
1863
|
-
});
|
|
1864
|
-
req.on('error', () => res(false));
|
|
1865
|
-
req.on('timeout', () => { req.destroy(); res(false); });
|
|
1866
|
-
req.end();
|
|
1867
|
-
});
|
|
1868
|
-
if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
|
|
1869
|
-
} catch {}
|
|
1870
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1871
|
-
}
|
|
1872
|
-
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
1873
|
-
} catch (error) {
|
|
1874
|
-
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
1875
|
-
}
|
|
1876
|
-
})();
|
|
1877
|
-
// Publish this session's record + transcript file BEFORE the worker's
|
|
1878
|
-
// activate-time discovery polls, so output forwarding binds to this
|
|
1879
|
-
// terminal session immediately instead of waiting for the first turn.
|
|
1880
|
-
// No-op when the session has not been created yet (lazy mode); that
|
|
1881
|
-
// case is covered by the turn-start rebind in ask().
|
|
1882
|
-
ensureRemoteTranscriptWriter();
|
|
1883
|
-
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
1884
|
-
bootProfile('channels:start-skipped');
|
|
1885
|
-
return true;
|
|
1886
|
-
}
|
|
1887
|
-
if (closeRequested) return true;
|
|
1888
|
-
if (prewarmTimers.channelStartTimer) {
|
|
1889
|
-
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1890
|
-
prewarmTimers.channelStartTimer = null;
|
|
1891
|
-
}
|
|
1892
|
-
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1893
|
-
void (async () => {
|
|
1894
|
-
// Channels-module toggle gates MESSAGING only: automation (schedules/
|
|
1895
|
-
// webhooks) still boots the worker — its backend runs headless when
|
|
1896
|
-
// messaging is off or unconfigured (see channels/lib/config.mjs).
|
|
1897
|
-
if (!channelsEnabled() && !(await hasActiveAutomation().catch(() => false))) {
|
|
1898
|
-
bootProfile('channels:start-disabled');
|
|
1899
|
-
remoteClaimPending = false;
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
// A backend switch may still be pending or writing asynchronously. Drain
|
|
1903
|
-
// it before the worker reads config; never race it with a sync lock wait.
|
|
1904
|
-
try { await flushBackendSave(); } catch {}
|
|
1905
|
-
// Yield before the createCurrentSession/transcript/fork chain below —
|
|
1906
|
-
// same rationale as the memory-eager-init yield above: this detached
|
|
1907
|
-
// chain runs synchronous config/fs work (createCurrentSession, backend
|
|
1908
|
-
// flush, transcript writer) back-to-back, and without a tick break it
|
|
1909
|
-
// can run ahead of Ink's queued render/input handling.
|
|
1910
|
-
await new Promise((r) => setImmediate(r));
|
|
1911
|
-
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1912
|
-
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1913
|
-
// runs transcript discovery inside its own start(), so publishing the
|
|
1914
|
-
// session record/file first lets that very first discovery pass bind
|
|
1915
|
-
// output forwarding to THIS terminal instead of a persisted/stale
|
|
1916
|
-
// neighbour. Lazy mode means the session may not exist yet at /remote
|
|
1917
|
-
// time; create it here (idempotent — reuses a live session, joins an
|
|
1918
|
-
// in-flight create). On create failure we still claim: that matches
|
|
1919
|
-
// the pre-eager behavior (bind resolves on the first turn's rebind).
|
|
1920
|
-
try { await createCurrentSession('remote-start'); }
|
|
1921
|
-
catch (error) { bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) }); }
|
|
1922
|
-
ensureRemoteTranscriptWriter();
|
|
1923
|
-
// Re-check after the awaits above: stopRemote()/superseded or runtime
|
|
1924
|
-
// close may have landed mid-chain — do not boot/claim for a session
|
|
1925
|
-
// that already turned remote off.
|
|
1926
|
-
if ((!remoteEnabled && !remoteClaimPending) || closeRequested) { remoteClaimPending = false; return; }
|
|
1927
|
-
// Set the fork-inherited intent immediately before the worker fork (the
|
|
1928
|
-
// fork reads process.env synchronously inside invokeChannelStart) and
|
|
1929
|
-
// restore the prior value the instant it resolves, so the pollution
|
|
1930
|
-
// window is just this fork rather than the whole boot chain.
|
|
1931
|
-
const _prevIntent = process.env.MIXDOG_REMOTE_INTENT;
|
|
1932
|
-
try {
|
|
1933
|
-
process.env.MIXDOG_REMOTE_INTENT = intent;
|
|
1934
|
-
await invokeChannelStart();
|
|
1935
|
-
} finally {
|
|
1936
|
-
if (_prevIntent === undefined) delete process.env.MIXDOG_REMOTE_INTENT;
|
|
1937
|
-
else process.env.MIXDOG_REMOTE_INTENT = _prevIntent;
|
|
1938
|
-
}
|
|
1939
|
-
if ((!remoteEnabled && !remoteClaimPending) || closeRequested) { remoteClaimPending = false; return; }
|
|
1940
|
-
// Explicit start: unconditional claim + forwarder rebind (last-wins seat
|
|
1941
|
-
// overwrite + transcript rebind onto this session). AUTO start SKIPS this
|
|
1942
|
-
// — the freshly-forked worker already ran its claim-if-vacant boot claim,
|
|
1943
|
-
// and forcing activate here would steal a live owner that autoStart is
|
|
1944
|
-
// meant to yield to. The worker's acquire notification drives remote ON.
|
|
1945
|
-
if (intent !== 'auto') {
|
|
1946
|
-
await channels.execute('activate_channel_bridge', { active: true });
|
|
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;
|
|
1947
1444
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
return true;
|
|
1967
|
-
}
|
|
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);
|
|
1968
1463
|
|
|
1969
1464
|
function isRemoteEnabled() {
|
|
1970
1465
|
return remoteEnabled;
|
|
@@ -2008,8 +1503,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2008
1503
|
// Channels are opt-in: only boot the worker when this session started in (or
|
|
2009
1504
|
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
2010
1505
|
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
2011
|
-
// `remote.autoStart` in mixdog-config.json makes
|
|
2012
|
-
// at boot
|
|
1506
|
+
// `remote.autoStart` in mixdog-config.json makes terminal sessions attempt a
|
|
1507
|
+
// remote claim at boot. Desktop sessions are explicit-only: opening the app
|
|
1508
|
+
// must never claim or spawn the channel bridge until its Remote button is
|
|
1509
|
+
// pressed, while the shared owner-state watcher still reflects a terminal
|
|
1510
|
+
// that already owns it.
|
|
2013
1511
|
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
2014
1512
|
// (sibling of agent/ui/channels), not inside the agent section that
|
|
2015
1513
|
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
@@ -2022,7 +1520,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2022
1520
|
// known to have won). Track it as a separate deferred auto request; the
|
|
2023
1521
|
// worker's acquire notification flips remoteEnabled if/when it wins the seat.
|
|
2024
1522
|
let remoteAutoStartRequested = false;
|
|
2025
|
-
if (!remoteEnabled) {
|
|
1523
|
+
if (!remoteEnabled && !desktopSession) {
|
|
2026
1524
|
try {
|
|
2027
1525
|
if (config?.remote?.autoStart === true
|
|
2028
1526
|
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
@@ -2039,7 +1537,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2039
1537
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
2040
1538
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
2041
1539
|
// still start immediately (user-initiated, UI already painted).
|
|
2042
|
-
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2043
1540
|
if (remoteEnabled || remoteAutoStartRequested) {
|
|
2044
1541
|
const remoteStartIntent = remoteEnabled ? 'explicit' : 'auto';
|
|
2045
1542
|
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
@@ -2054,9 +1551,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2054
1551
|
prewarmTimers.channelStartTimer.unref?.();
|
|
2055
1552
|
} else {
|
|
2056
1553
|
// Automation decoupling (user decision): enabled schedules/webhooks boot
|
|
2057
|
-
// the worker on their own — no remote flag, no remote.autoStart, no
|
|
2058
|
-
// messaging backend
|
|
2059
|
-
//
|
|
1554
|
+
// the worker on their own — no remote flag, no remote.autoStart, and no
|
|
1555
|
+
// messaging backend. A later explicit/auto Remote claim promotes the same
|
|
1556
|
+
// daemon without restarting schedules or webhooks.
|
|
2060
1557
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
2061
1558
|
prewarmTimers.channelStartTimer = null;
|
|
2062
1559
|
if (closeRequested || remoteEnabled) return;
|
|
@@ -2064,7 +1561,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2064
1561
|
.then((active) => {
|
|
2065
1562
|
if (!active || closeRequested || remoteEnabled) return;
|
|
2066
1563
|
bootProfile('channels:automation-autostart');
|
|
2067
|
-
|
|
1564
|
+
void invokeChannelStart();
|
|
2068
1565
|
})
|
|
2069
1566
|
.catch(() => { /* automation probe is best-effort */ });
|
|
2070
1567
|
}, remoteAutoStartDelayMs);
|
|
@@ -2104,8 +1601,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2104
1601
|
recapEnabledFn,
|
|
2105
1602
|
channelsEnabled,
|
|
2106
1603
|
autoUpdateEnabled,
|
|
2107
|
-
getUpdateCheckState: () =>
|
|
2108
|
-
getUpdateProcessState: () =>
|
|
1604
|
+
getUpdateCheckState: () => selfUpdate.getCheckState(),
|
|
1605
|
+
getUpdateProcessState: () => selfUpdate.getProcessState(),
|
|
2109
1606
|
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
2110
1607
|
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
2111
1608
|
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
@@ -2127,7 +1624,14 @@ export async function createMixdogSessionRuntime({
|
|
|
2127
1624
|
setBackend,
|
|
2128
1625
|
});
|
|
2129
1626
|
|
|
2130
|
-
const channelConfigApi = createChannelConfigApi({
|
|
1627
|
+
const channelConfigApi = createChannelConfigApi({
|
|
1628
|
+
flushBackendSave,
|
|
1629
|
+
channels,
|
|
1630
|
+
reloadChannelsSoon,
|
|
1631
|
+
// Automation saved mid-session boots the worker (claim-if-vacant) even
|
|
1632
|
+
// though the boot-time autostart window has already passed.
|
|
1633
|
+
ensureAutomationRuntime: () => scheduleChannelStart(0),
|
|
1634
|
+
});
|
|
2131
1635
|
const providerAuthApi = createProviderAuthApi({
|
|
2132
1636
|
cfgMod,
|
|
2133
1637
|
getConfig: () => config,
|
|
@@ -2135,6 +1639,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2135
1639
|
displayConfig,
|
|
2136
1640
|
reloadFullConfig,
|
|
2137
1641
|
awaitKeychainPrewarm,
|
|
1642
|
+
isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
|
|
1643
|
+
hasProviderSetupCached,
|
|
2138
1644
|
invalidateProviderCaches,
|
|
2139
1645
|
warmProviderModelCache,
|
|
2140
1646
|
refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
|
|
@@ -2258,6 +1764,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2258
1764
|
displayConfig,
|
|
2259
1765
|
agentRouteFromConfig,
|
|
2260
1766
|
loadAgentDefinition,
|
|
1767
|
+
listCustomAgentIds,
|
|
2261
1768
|
activeWorkflowId,
|
|
2262
1769
|
listWorkflowPacks,
|
|
2263
1770
|
loadWorkflowPack,
|
|
@@ -2286,8 +1793,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2286
1793
|
getCloseRequested: () => closeRequested,
|
|
2287
1794
|
getPendingSessionReset: () => pendingSessionReset,
|
|
2288
1795
|
setPendingSessionReset: (v) => { pendingSessionReset = v; },
|
|
2289
|
-
getTranscriptWriter: () =>
|
|
2290
|
-
getTwKey: () =>
|
|
1796
|
+
getTranscriptWriter: () => remoteTranscript.transcriptWriter,
|
|
1797
|
+
getTwKey: () => remoteTranscript.transcriptKey,
|
|
2291
1798
|
getLastAppendedAssistant: () => _lastAppendedAssistant,
|
|
2292
1799
|
setLastAppendedAssistant: (v) => { _lastAppendedAssistant = v; },
|
|
2293
1800
|
scheduleCodeGraphPrewarm,
|
|
@@ -2404,9 +1911,26 @@ export async function createMixdogSessionRuntime({
|
|
|
2404
1911
|
stopRemote(reason) {
|
|
2405
1912
|
return stopRemote(reason);
|
|
2406
1913
|
},
|
|
1914
|
+
releaseRemote(reason) {
|
|
1915
|
+
return releaseRemote(reason);
|
|
1916
|
+
},
|
|
2407
1917
|
isRemoteEnabled() {
|
|
2408
1918
|
return isRemoteEnabled();
|
|
2409
1919
|
},
|
|
1920
|
+
// Session-scoped remote: the owning session id (null when off/unassigned).
|
|
1921
|
+
getRemoteSessionId() {
|
|
1922
|
+
return remoteSessionId;
|
|
1923
|
+
},
|
|
1924
|
+
// Move the relay seat to the CURRENT session (last-wins within this
|
|
1925
|
+
// engine): rebind the transcript writer + forwarder without restarting
|
|
1926
|
+
// the worker. No-op when remote is off or no session exists.
|
|
1927
|
+
claimRemoteForCurrentSession() {
|
|
1928
|
+
if (!remoteEnabled || !session?.id) return false;
|
|
1929
|
+
remoteSessionId = session.id;
|
|
1930
|
+
ensureRemoteTranscriptWriter();
|
|
1931
|
+
pushTranscriptRebind();
|
|
1932
|
+
return true;
|
|
1933
|
+
},
|
|
2410
1934
|
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
2411
1935
|
// an unsubscribe function. TUI uses this to sync its Remote indicator and
|
|
2412
1936
|
// show a "remote taken over" notice.
|