mixdog 0.9.69 → 0.9.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +8 -1
- package/src/runtime/media/adapters/codex-image.mjs +109 -0
- package/src/runtime/media/adapters/gemini-image.mjs +68 -0
- package/src/runtime/media/adapters/gemini-video.mjs +119 -0
- package/src/runtime/media/adapters/xai-media.mjs +116 -0
- package/src/runtime/media/auth.mjs +51 -0
- package/src/runtime/media/index.mjs +17 -0
- package/src/runtime/media/jobs.mjs +174 -0
- package/src/runtime/media/lanes.mjs +230 -0
- package/src/runtime/media/store.mjs +164 -0
- package/src/runtime/media/upstream-error.mjs +39 -0
- package/src/session-runtime/channel-config-api.mjs +12 -1
- package/src/session-runtime/media-api.mjs +47 -0
- package/src/session-runtime/model-route-api.mjs +4 -1
- package/src/session-runtime/runtime-core.mjs +17 -1
- package/src/session-runtime/warmup-schedulers.mjs +14 -1
- package/src/session-runtime/workflow-agents-api.mjs +29 -4
- package/src/tui/dist/index.mjs +20 -1
- package/src/tui/engine/session-api-ext.mjs +14 -0
- package/src/tui/engine.mjs +39 -1
|
@@ -96,6 +96,10 @@ export function createModelRouteApi(deps) {
|
|
|
96
96
|
if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
|
|
97
97
|
throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
|
|
98
98
|
}
|
|
99
|
+
// Route-scope isolation: the search route stores its own effort/fast in
|
|
100
|
+
// config.searchRoute. The shared config.modelSettings[provider/model]
|
|
101
|
+
// bucket belongs to the MAIN route alone, so a search-model pick that
|
|
102
|
+
// happens to match Main must not rewrite Main's saved effort/fast.
|
|
99
103
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
100
104
|
const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
|
|
101
105
|
selectedRoute = {
|
|
@@ -103,7 +107,6 @@ export function createModelRouteApi(deps) {
|
|
|
103
107
|
...(effort ? { effort } : {}),
|
|
104
108
|
fast: fastCapable ? selectedRoute.fast === true : false,
|
|
105
109
|
};
|
|
106
|
-
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
|
|
107
110
|
const routeToSave = normalizeSearchRouteConfig(selectedRoute);
|
|
108
111
|
const nextConfig = { ...getConfig() };
|
|
109
112
|
nextConfig.searchRoute = routeToSave;
|
|
@@ -230,6 +230,7 @@ import { createProviderUsage } from './provider-usage.mjs';
|
|
|
230
230
|
import { envFlag } from './env.mjs';
|
|
231
231
|
import { bootProfile, profiledImport } from './boot-profile.mjs';
|
|
232
232
|
import { createChannelConfigApi } from './channel-config-api.mjs';
|
|
233
|
+
import { createMediaApi } from './media-api.mjs';
|
|
233
234
|
import { createProviderAuthApi } from './provider-auth-api.mjs';
|
|
234
235
|
import { createContextStatus } from './context-status.mjs';
|
|
235
236
|
import { createLifecycleApi } from './lifecycle-api.mjs';
|
|
@@ -588,6 +589,9 @@ export async function createMixdogSessionRuntime({
|
|
|
588
589
|
};
|
|
589
590
|
const providerInitPromises = new Map();
|
|
590
591
|
let startupProviderCatalogRefreshStarted = false;
|
|
592
|
+
// True while the boot-time provider-catalog refresh is in flight: warming a
|
|
593
|
+
// model cache it is about to invalidate only burns the load twice.
|
|
594
|
+
let startupProviderCatalogRefreshPending = false;
|
|
591
595
|
let lastProjectMcpKey = null;
|
|
592
596
|
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
593
597
|
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
@@ -1011,20 +1015,28 @@ export async function createMixdogSessionRuntime({
|
|
|
1011
1015
|
}
|
|
1012
1016
|
if (!startupProviderCatalogRefreshStarted && !closeRequested) {
|
|
1013
1017
|
startupProviderCatalogRefreshStarted = true;
|
|
1018
|
+
startupProviderCatalogRefreshPending = true;
|
|
1014
1019
|
try {
|
|
1015
1020
|
void Promise.resolve(reg.refreshProviderCatalogsOnStartup())
|
|
1016
1021
|
.then(() => {
|
|
1017
1022
|
// Fresh catalog rows invalidate model-derived caches, but the
|
|
1018
1023
|
// already initialized provider registry remains valid.
|
|
1019
1024
|
invalidateProviderCaches({ preserveProviderInit: true });
|
|
1020
|
-
|
|
1025
|
+
startupProviderCatalogRefreshPending = false;
|
|
1026
|
+
// Secrets-aware: a no-secrets rewarm bumps the load sequence and
|
|
1027
|
+
// is never adopted, so it discarded the in-flight authoritative
|
|
1028
|
+
// load and left the picker cache empty — every later consumer then
|
|
1029
|
+
// paid a full catalog load (measured ~560ms each).
|
|
1030
|
+
warmProviderModelCache({ loadSecrets: true });
|
|
1021
1031
|
bootProfile('provider-catalogs:refresh-ready');
|
|
1022
1032
|
})
|
|
1023
1033
|
.catch((error) => {
|
|
1034
|
+
startupProviderCatalogRefreshPending = false;
|
|
1024
1035
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1025
1036
|
});
|
|
1026
1037
|
bootProfile('provider-catalogs:refresh-started');
|
|
1027
1038
|
} catch (error) {
|
|
1039
|
+
startupProviderCatalogRefreshPending = false;
|
|
1028
1040
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1029
1041
|
}
|
|
1030
1042
|
}
|
|
@@ -1355,6 +1367,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1355
1367
|
cachedProviderSetup,
|
|
1356
1368
|
warmCatalogsInBackground,
|
|
1357
1369
|
isFirstTurnCompleted: () => firstTurnCompleted,
|
|
1370
|
+
isCatalogRefreshPending: () => startupProviderCatalogRefreshPending,
|
|
1358
1371
|
envFlag,
|
|
1359
1372
|
delays: {
|
|
1360
1373
|
providerWarmupDelayMs,
|
|
@@ -1631,6 +1644,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1631
1644
|
// Automation saved mid-session boots the worker (claim-if-vacant) even
|
|
1632
1645
|
// though the boot-time autostart window has already passed.
|
|
1633
1646
|
ensureAutomationRuntime: () => scheduleChannelStart(0),
|
|
1647
|
+
awaitKeychainPrewarm,
|
|
1634
1648
|
});
|
|
1635
1649
|
const providerAuthApi = createProviderAuthApi({
|
|
1636
1650
|
cfgMod,
|
|
@@ -1648,6 +1662,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1648
1662
|
getUsageDashboard,
|
|
1649
1663
|
collectProviderModels,
|
|
1650
1664
|
});
|
|
1665
|
+
const mediaApi = createMediaApi();
|
|
1651
1666
|
const lifecycleApi = createLifecycleApi({
|
|
1652
1667
|
getSession: () => session,
|
|
1653
1668
|
setSession: (v) => { session = v; },
|
|
@@ -1830,6 +1845,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1830
1845
|
...settingsApi,
|
|
1831
1846
|
...channelConfigApi,
|
|
1832
1847
|
...providerAuthApi,
|
|
1848
|
+
...mediaApi,
|
|
1833
1849
|
// Turn-scoped worktree diff (shadow snapshot): everything changed since
|
|
1834
1850
|
// the current turn's base tree, regardless of which agent/process wrote it.
|
|
1835
1851
|
getTurnReviewDiff: () => getTurnSnapshotReviewDiff(currentCwd, session?.id),
|
|
@@ -26,6 +26,7 @@ export function createWarmupSchedulers({
|
|
|
26
26
|
cachedProviderSetup,
|
|
27
27
|
warmCatalogsInBackground,
|
|
28
28
|
isFirstTurnCompleted,
|
|
29
|
+
isCatalogRefreshPending = () => false,
|
|
29
30
|
envFlag,
|
|
30
31
|
delays,
|
|
31
32
|
flags,
|
|
@@ -104,9 +105,21 @@ export function createWarmupSchedulers({
|
|
|
104
105
|
scheduleProviderModelWarmup(backgroundBusyRetryMs);
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
108
|
+
// The startup catalog refresh invalidates every model-derived cache when
|
|
109
|
+
// it lands, so warming before it finishes throws the whole load away.
|
|
110
|
+
if (isCatalogRefreshPending()) {
|
|
111
|
+
bootProfile('provider-models:warm-deferred', { reason: 'catalog-refresh-pending' });
|
|
112
|
+
scheduleProviderModelWarmup(backgroundBusyRetryMs);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
107
115
|
if (!isFirstTurnCompleted() && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
|
|
108
116
|
bootProfile('provider-models:warm-deferred', { reason: 'first-turn-pending' });
|
|
109
|
-
|
|
117
|
+
// Secrets-aware even before the first turn: the no-secrets variant is
|
|
118
|
+
// never adopted as the picker cache, so every consumer immediately
|
|
119
|
+
// reloaded the whole catalog (measured ~900ms of duplicate provider
|
|
120
|
+
// I/O on a desktop boot). This runs on a background timer and the
|
|
121
|
+
// keychain prewarm is already in flight, so nothing user-facing waits.
|
|
122
|
+
warmProviderModelCache({ loadSecrets: true });
|
|
110
123
|
scheduleProviderModelWarmup(backgroundBusyRetryMs);
|
|
111
124
|
return;
|
|
112
125
|
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { ONBOARDING_VERSION } from './quick-search-models.mjs';
|
|
24
24
|
import { findOutputStyle } from './output-styles.mjs';
|
|
25
25
|
import { ensureProviderEnabled } from './config-helpers.mjs';
|
|
26
|
-
import { fastCapableFor
|
|
26
|
+
import { fastCapableFor } from './model-capabilities.mjs';
|
|
27
27
|
|
|
28
28
|
// Onboarding + agents/workflows/output-style selection surface. Extracted
|
|
29
29
|
// verbatim from the runtime API object; stateless helpers are imported directly
|
|
@@ -31,7 +31,7 @@ import { fastCapableFor, saveModelSettings } from './model-capabilities.mjs';
|
|
|
31
31
|
// session locals plus the closure callbacks.
|
|
32
32
|
export function createWorkflowAgentsApi(deps) {
|
|
33
33
|
const {
|
|
34
|
-
getConfig, getRoute, setRouteState, getSession, setSession,
|
|
34
|
+
getConfig, getRoute, setRouteState, getSession, setSession,
|
|
35
35
|
cfgMod, reg, mgr, STANDALONE_DATA_DIR,
|
|
36
36
|
resolveRoute, lookupModelMeta, adoptConfig, saveConfigAndAdopt, displayConfig, ensureProvidersReady,
|
|
37
37
|
agentRouteFromConfig, loadAgentDefinition, activeWorkflowId, listWorkflowPacks,
|
|
@@ -406,12 +406,37 @@ export function createWorkflowAgentsApi(deps) {
|
|
|
406
406
|
const id = normalizeAgentId(agentId) || normalizeWorkflowId(agentId);
|
|
407
407
|
if (!id) throw new Error(`unknown agent "${agentId}"`);
|
|
408
408
|
if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and has no configurable route`);
|
|
409
|
-
|
|
409
|
+
// Route-scope isolation: an agent route owns its model AND its
|
|
410
|
+
// effort/fast, stored in config.agents[<id>]. The shared
|
|
411
|
+
// config.modelSettings[provider/model] bucket belongs to the MAIN route
|
|
412
|
+
// alone (setRoute/setFast/setEffort) — resolveRoute reads it with
|
|
413
|
+
// priority over the lead preset, so an agent that reused it silently
|
|
414
|
+
// rewrote Main's effort/fast for the next session. Read the agent's own
|
|
415
|
+
// stored route as the fallback instead, and never write the bucket here.
|
|
416
|
+
const requested = { ...(next || {}) };
|
|
417
|
+
const routeDataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
418
|
+
const stored = agentRouteFromConfig(getConfig(), id, routeDataDir) || {};
|
|
419
|
+
// A request with neither provider nor model is the "seed from the current
|
|
420
|
+
// default" path (new custom agent) and inherits Main verbatim.
|
|
421
|
+
const inheritsDefault = !clean(requested.provider) && !clean(requested.model);
|
|
422
|
+
let selectedRoute = resolveRoute(getConfig(), requested);
|
|
423
|
+
if (!inheritsDefault) {
|
|
424
|
+
const sameModel = clean(selectedRoute.provider) === clean(stored.provider)
|
|
425
|
+
&& clean(selectedRoute.model) === clean(stored.model);
|
|
426
|
+
selectedRoute = {
|
|
427
|
+
...selectedRoute,
|
|
428
|
+
effort: requested.effort !== undefined
|
|
429
|
+
? selectedRoute.effort
|
|
430
|
+
: (sameModel ? (stored.effort || null) : null),
|
|
431
|
+
fast: requested.fast !== undefined
|
|
432
|
+
? selectedRoute.fast === true
|
|
433
|
+
: (sameModel && stored.fast === true),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
410
436
|
await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
|
|
411
437
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
412
438
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
413
439
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
414
|
-
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
|
|
415
440
|
|
|
416
441
|
const routeToSave = normalizeWorkflowRoute(selectedRoute);
|
|
417
442
|
if (!routeToSave) throw new Error('agent route requires provider and model');
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -28402,6 +28402,20 @@ function createEngineApiB(bag) {
|
|
|
28402
28402
|
pushNotice("channel saved", "info");
|
|
28403
28403
|
return result;
|
|
28404
28404
|
},
|
|
28405
|
+
// Media studio (image/video generation). Reads stay quiet; only the
|
|
28406
|
+
// generation start posts a notice so the TUI shows background work.
|
|
28407
|
+
listMediaLanes: () => runtime.listMediaLanes?.(),
|
|
28408
|
+
listMediaAssets: (options) => runtime.listMediaAssets?.(options),
|
|
28409
|
+
readMediaAsset: (id) => runtime.readMediaAsset?.(id),
|
|
28410
|
+
getMediaJob: (id) => runtime.getMediaJob?.(id),
|
|
28411
|
+
listMediaJobs: () => runtime.listMediaJobs?.(),
|
|
28412
|
+
// No notice on start: the Studio surfaces progress on its own pending tile,
|
|
28413
|
+
// and a toast for a user-initiated generation is pure noise.
|
|
28414
|
+
startMediaJob: (input) => runtime.startMediaJob(input),
|
|
28415
|
+
cancelMediaJob: (id) => runtime.cancelMediaJob?.(id),
|
|
28416
|
+
deleteMediaAsset: (id) => runtime.deleteMediaAsset?.(id),
|
|
28417
|
+
openMediaAsset: (id) => runtime.openMediaAsset?.(id),
|
|
28418
|
+
openMediaFolder: () => runtime.openMediaFolder?.(),
|
|
28405
28419
|
setWebhookConfig: async (patch) => {
|
|
28406
28420
|
const result = await runtime.setWebhookConfig(patch);
|
|
28407
28421
|
pushNotice("webhook config updated", "info");
|
|
@@ -30532,6 +30546,11 @@ function createEngineItemMutators({
|
|
|
30532
30546
|
|
|
30533
30547
|
// src/tui/engine.mjs
|
|
30534
30548
|
var SESSION_RUNTIME_MODULE = import.meta.url.replace(/\\/g, "/").includes("/tui/dist/") ? "../../mixdog-session-runtime.mjs" : "../mixdog-session-runtime.mjs";
|
|
30549
|
+
var sessionRuntimeModulePromise = null;
|
|
30550
|
+
function importSessionRuntimeModule() {
|
|
30551
|
+
sessionRuntimeModulePromise ??= import(SESSION_RUNTIME_MODULE);
|
|
30552
|
+
return sessionRuntimeModulePromise;
|
|
30553
|
+
}
|
|
30535
30554
|
var TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
30536
30555
|
const value = Number(process.env.MIXDOG_TOOL_APPROVAL_TIMEOUT_MS);
|
|
30537
30556
|
return Number.isFinite(value) && value > 0 ? Math.max(1e3, Math.round(value)) : 12e4;
|
|
@@ -30556,7 +30575,7 @@ async function createEngineSession({
|
|
|
30556
30575
|
process.env.MIXDOG_QUIET_MEMORY_LOG = "1";
|
|
30557
30576
|
process.env.MIXDOG_PATCH_NATIVE_PREWARM ??= "0";
|
|
30558
30577
|
const importStartedAt = performance3.now();
|
|
30559
|
-
const { createMixdogSessionRuntime } = await
|
|
30578
|
+
const { createMixdogSessionRuntime } = await importSessionRuntimeModule();
|
|
30560
30579
|
bootProfile("session-runtime:imported", { ms: (performance3.now() - importStartedAt).toFixed(1) });
|
|
30561
30580
|
const runtime = await createMixdogSessionRuntime({
|
|
30562
30581
|
provider: providerName,
|
|
@@ -673,6 +673,20 @@ export function createEngineApiB(bag) {
|
|
|
673
673
|
pushNotice('channel saved', 'info');
|
|
674
674
|
return result;
|
|
675
675
|
},
|
|
676
|
+
// Media studio (image/video generation). Reads stay quiet; only the
|
|
677
|
+
// generation start posts a notice so the TUI shows background work.
|
|
678
|
+
listMediaLanes: () => runtime.listMediaLanes?.(),
|
|
679
|
+
listMediaAssets: (options) => runtime.listMediaAssets?.(options),
|
|
680
|
+
readMediaAsset: (id) => runtime.readMediaAsset?.(id),
|
|
681
|
+
getMediaJob: (id) => runtime.getMediaJob?.(id),
|
|
682
|
+
listMediaJobs: () => runtime.listMediaJobs?.(),
|
|
683
|
+
// No notice on start: the Studio surfaces progress on its own pending tile,
|
|
684
|
+
// and a toast for a user-initiated generation is pure noise.
|
|
685
|
+
startMediaJob: (input) => runtime.startMediaJob(input),
|
|
686
|
+
cancelMediaJob: (id) => runtime.cancelMediaJob?.(id),
|
|
687
|
+
deleteMediaAsset: (id) => runtime.deleteMediaAsset?.(id),
|
|
688
|
+
openMediaAsset: (id) => runtime.openMediaAsset?.(id),
|
|
689
|
+
openMediaFolder: () => runtime.openMediaFolder?.(),
|
|
676
690
|
setWebhookConfig: async (patch) => {
|
|
677
691
|
const result = await runtime.setWebhookConfig(patch);
|
|
678
692
|
pushNotice('webhook config updated', 'info');
|
package/src/tui/engine.mjs
CHANGED
|
@@ -119,6 +119,44 @@ const SESSION_RUNTIME_MODULE = import.meta.url.replace(/\\/g, '/').includes('/tu
|
|
|
119
119
|
? '../../mixdog-session-runtime.mjs'
|
|
120
120
|
: '../mixdog-session-runtime.mjs';
|
|
121
121
|
|
|
122
|
+
// The runtime graph is imported lazily, but that import (measured ~250ms) used
|
|
123
|
+
// to land inside the FIRST engine creation, which desktop performs while it
|
|
124
|
+
// holds its transition lock — so the user's first navigation paid for it. Hosts
|
|
125
|
+
// can start it during their own idle startup instead; the promise is shared, so
|
|
126
|
+
// the create path either awaits an in-flight preload or does the import itself.
|
|
127
|
+
let sessionRuntimeModulePromise = null;
|
|
128
|
+
function importSessionRuntimeModule() {
|
|
129
|
+
sessionRuntimeModulePromise ??= import(SESSION_RUNTIME_MODULE);
|
|
130
|
+
return sessionRuntimeModulePromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function preloadSessionRuntimeModule() {
|
|
134
|
+
void importSessionRuntimeModule().catch(() => {
|
|
135
|
+
// A real load failure surfaces on the authoritative create path.
|
|
136
|
+
sessionRuntimeModulePromise = null;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Windows keychain reads go through a DPAPI PowerShell host whose cold start
|
|
141
|
+
// dominates a packaged boot (measured ~1.5s). The runtime batches every secret
|
|
142
|
+
// into one call, but it only starts that batch when the runtime is created —
|
|
143
|
+
// late enough that the first context switch waits on it. Hosts can start the
|
|
144
|
+
// same batch while their window is still coming up.
|
|
145
|
+
export function preloadKeychainSecrets() {
|
|
146
|
+
void (async () => {
|
|
147
|
+
try {
|
|
148
|
+
const { createRequire } = await import('node:module');
|
|
149
|
+
const require = createRequire(import.meta.url);
|
|
150
|
+
const keychain = require(import.meta.url.replace(/\\/g, '/').includes('/tui/dist/')
|
|
151
|
+
? '../../lib/keychain-cjs.cjs'
|
|
152
|
+
: '../lib/keychain-cjs.cjs');
|
|
153
|
+
await keychain.prewarmSecrets?.();
|
|
154
|
+
} catch {
|
|
155
|
+
// Prewarm is opportunistic; the runtime still warms on its own path.
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
}
|
|
159
|
+
|
|
122
160
|
const TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
123
161
|
const value = Number(process.env.MIXDOG_TOOL_APPROVAL_TIMEOUT_MS);
|
|
124
162
|
return Number.isFinite(value) && value > 0 ? Math.max(1000, Math.round(value)) : 120_000;
|
|
@@ -161,7 +199,7 @@ export async function createEngineSession({
|
|
|
161
199
|
process.env.MIXDOG_PATCH_NATIVE_PREWARM ??= '0';
|
|
162
200
|
|
|
163
201
|
const importStartedAt = performance.now();
|
|
164
|
-
const { createMixdogSessionRuntime } = await
|
|
202
|
+
const { createMixdogSessionRuntime } = await importSessionRuntimeModule();
|
|
165
203
|
bootProfile('session-runtime:imported', { ms: (performance.now() - importStartedAt).toFixed(1) });
|
|
166
204
|
const runtime = await createMixdogSessionRuntime({
|
|
167
205
|
provider: providerName,
|