mixdog 1.0.8 → 1.0.10

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.
Files changed (124) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  13. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  17. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  19. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  20. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  21. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  22. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +163 -0
  23. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  24. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
  27. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  28. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +3 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1 -28
  33. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  35. package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
  36. package/src/runtime/agent/orchestrator/providers/registry.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  38. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  39. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
  40. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  41. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  42. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  44. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  45. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  46. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  48. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  49. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +22 -1
  50. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  51. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  52. package/src/runtime/computer-bridge/actions.mjs +4 -0
  53. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  54. package/src/runtime/computer-bridge/client.mjs +10 -183
  55. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  56. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  57. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  58. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  59. package/src/runtime/local-provider/catalog.mjs +4 -7
  60. package/src/runtime/local-provider/data/manifest.json +14 -1
  61. package/src/runtime/local-provider/hardware.mjs +36 -5
  62. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  63. package/src/runtime/local-provider/server.mjs +1 -1
  64. package/src/runtime/memory/index.mjs +2 -4
  65. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  66. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  67. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  68. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  69. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  70. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  71. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  72. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  73. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  74. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  75. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  76. package/src/runtime/memory/lib/memory.mjs +79 -32
  77. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  78. package/src/runtime/office/com/com-adapter.mjs +3 -0
  79. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  80. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  81. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  82. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  83. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  84. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  85. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  86. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  87. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  88. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  89. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  90. package/src/runtime/shared/model-alias.mjs +5 -0
  91. package/src/runtime/shared/plugin-paths.mjs +9 -2
  92. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  93. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  94. package/src/session-runtime/automation-agents.mjs +88 -0
  95. package/src/session-runtime/automation-workflow.mjs +19 -6
  96. package/src/session-runtime/boot/apis.mjs +6 -1
  97. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  98. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  99. package/src/session-runtime/model-recency.mjs +10 -2
  100. package/src/session-runtime/model-route/set-route.mjs +7 -1
  101. package/src/session-runtime/provider-auth/login.mjs +26 -1
  102. package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
  103. package/src/session-runtime/resource-api.mjs +8 -2
  104. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  105. package/src/session-runtime/route-state.mjs +6 -2
  106. package/src/session-runtime/schedule-session-run.mjs +7 -1
  107. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  108. package/src/session-runtime/services/provider-admin.mjs +201 -3
  109. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  110. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  111. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  112. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  113. package/src/session-runtime/webhook-session-run.mjs +10 -11
  114. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  115. package/src/standalone/daemon-agent-control.mjs +8 -1
  116. package/src/standalone/daemon.mjs +4 -0
  117. package/src/standalone/session-protocol.mjs +4 -0
  118. package/src/tui/app/model-options.mjs +10 -1
  119. package/src/tui/dist/index.mjs +12 -3
  120. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  121. package/src/tui/session/live-share.mjs +16 -1
  122. package/src/tui/session/notification-plan.mjs +4 -0
  123. package/src/tui/session/session-api/integrations.mjs +12 -0
  124. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
@@ -17,13 +17,8 @@ export function abnormalEmptyFinishError(result, agent) {
17
17
  if (!reason) return null;
18
18
  const iterations = result?.iterations ?? 0;
19
19
  const toolCallsTotal = result?.toolCallsTotal ?? 0;
20
- const maxLoopIterations = result?.maxLoopIterations ?? 0;
21
20
  const stopReason = result?.stopReason ?? result?.stop_reason ?? null;
22
21
  switch (reason) {
23
- case 'iteration_cap':
24
- // Preserve failure reporting for stored results from older runtimes.
25
- // Current execution no longer applies a loop iteration ceiling.
26
- return `agent '${agent}' hit the loop iteration ceiling (${maxLoopIterations} iterations, ${toolCallsTotal} tool calls) without producing a final answer`;
27
22
  case 'truncated':
28
23
  return `agent '${agent}' response was truncated (stopReason=${stopReason}) before a final answer (${iterations} iterations, ${toolCallsTotal} tool calls)`;
29
24
  case 'empty':
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import {
2
3
  AGENT_PROVIDER_ENV,
3
4
  SECRET_ACCOUNTS,
@@ -156,9 +157,14 @@ function availableOAuthProvider(id) {
156
157
  return oauth && isOAuthProviderAvailable(id) ? oauth : null;
157
158
  }
158
159
 
159
- export function isKnownProvider(provider) {
160
+ export function isKnownProvider(provider, config = {}) {
160
161
  const id = String(provider || '').trim();
161
- return id !== '' && (API_PROVIDER_IDS.has(id) || BUILTIN_PROVIDER_IDS.has(id) || !!availableOAuthProvider(id));
162
+ return id !== '' && (
163
+ API_PROVIDER_IDS.has(id) ||
164
+ BUILTIN_PROVIDER_IDS.has(id) ||
165
+ !!availableOAuthProvider(id) ||
166
+ (isCustomProviderId(id) && isCustomProviderConfig(config.providers?.[id]) && config.providers[id].enabled !== false)
167
+ );
162
168
  }
163
169
 
164
170
  function updateConfigProvider(cfgMod, providerId, patch) {
@@ -275,11 +281,57 @@ function oauthProviderSetup(p, providers, checkSecrets) {
275
281
  };
276
282
  }
277
283
 
284
+ const CUSTOM_ID_RE = /^custom-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
285
+ const CUSTOM_TEST_TIMEOUT_MS = 20_000;
286
+ const CUSTOM_DISCOVERY_TIMEOUT_MS = 15_000;
287
+
288
+ export function isCustomProviderConfig(entry) {
289
+ return Boolean(entry) && typeof entry === 'object' && entry.type === 'custom';
290
+ }
291
+
292
+ export function isCustomProviderId(id) {
293
+ return CUSTOM_ID_RE.test(String(id || ''));
294
+ }
295
+
296
+ const loadCustomProviderModule = () => import('../../runtime/agent/orchestrator/providers/custom-provider.mjs');
297
+
298
+ // Secret-free public view of a custom provider. Never carries an API key.
299
+ function customProviderRow(id, entry, checkSecrets = true) {
300
+ const configuredEnabled = entry.enabled !== false;
301
+ const stored = checkSecrets ? hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(id)) : false;
302
+ return {
303
+ id,
304
+ name: entry.name,
305
+ protocol: entry.protocol,
306
+ baseURL: entry.baseURL,
307
+ models: Array.isArray(entry.models) ? entry.models.map((m) => ({ ...m })) : [],
308
+ custom: true,
309
+ group: 'api',
310
+ type: 'api-key',
311
+ enabled: configuredEnabled,
312
+ authenticated: checkSecrets ? stored : configuredEnabled,
313
+ stored,
314
+ env: false,
315
+ envName: null,
316
+ status: stored ? 'Set' : configuredEnabled ? 'No Key' : 'Off',
317
+ detail: stored ? 'stored in keychain' : 'custom endpoint',
318
+ };
319
+ }
320
+
321
+ function customProviderRows(providers, checkSecrets) {
322
+ return Object.entries(providers)
323
+ .filter(([id, entry]) => isCustomProviderId(id) && isCustomProviderConfig(entry))
324
+ .map(([id, entry]) => customProviderRow(id, entry, checkSecrets));
325
+ }
326
+
278
327
  export async function providerSetup(config = {}, options = {}) {
279
328
  const providers = config.providers || {};
280
329
  const checkSecrets = options?.checkSecrets !== false;
281
330
  return {
282
- api: API_PROVIDERS.map((p) => apiProviderSetup(p, providers, checkSecrets)),
331
+ api: [
332
+ ...API_PROVIDERS.map((p) => apiProviderSetup(p, providers, checkSecrets)),
333
+ ...customProviderRows(providers, checkSecrets),
334
+ ],
283
335
  oauth: availableOAuthProviders().map((p) => oauthProviderSetup(p, providers, checkSecrets)),
284
336
  local: [builtInLocalProviderSetup(config, options)],
285
337
  };
@@ -322,6 +374,16 @@ function oauthProviderStatusRow(p, config) {
322
374
  export function providerStatus(config = {}) {
323
375
  return [
324
376
  ...API_PROVIDERS.map((p) => apiProviderStatusRow(p, config)),
377
+ ...customProviderRows(config.providers || {}, true).map((row) => ({
378
+ id: row.id,
379
+ type: 'api-key',
380
+ enabled: row.enabled,
381
+ authenticated: row.authenticated,
382
+ stored: row.stored,
383
+ env: false,
384
+ envName: null,
385
+ label: row.name,
386
+ })),
325
387
  ...availableOAuthProviders().map((p) => oauthProviderStatusRow(p, config)),
326
388
  ];
327
389
  }
@@ -525,6 +587,142 @@ export function saveProviderApiKey(cfgMod, provider, secret) {
525
587
  return { provider: id, type: 'api-key', authenticated: true };
526
588
  }
527
589
 
590
+ function sanitizeCustomError(error, secrets = []) {
591
+ let text = String(error?.message || error || 'request failed');
592
+ for (const secret of secrets) if (secret) text = text.split(secret).join('[redacted]');
593
+ text = text
594
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [redacted]')
595
+ .replace(/\b(?:sk|key|tok)[-_][A-Za-z0-9_-]{8,}/gi, '[redacted]')
596
+ .replace(/(api[-_]?key|authorization|token)(["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, '$1$2[redacted]')
597
+ .replace(/[\r\n]+/g, ' ')
598
+ .trim();
599
+ if (text.length > 300) text = `${text.slice(0, 300)}…`;
600
+ return new Error(text || 'request failed');
601
+ }
602
+
603
+ async function withTimeout(run, ms, label) {
604
+ const controller = new AbortController();
605
+ let timer;
606
+ const timeout = new Promise((_, reject) => {
607
+ timer = setTimeout(() => {
608
+ controller.abort();
609
+ reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s`));
610
+ }, ms);
611
+ });
612
+ try {
613
+ return await Promise.race([run(controller.signal), timeout]);
614
+ } finally {
615
+ clearTimeout(timer);
616
+ }
617
+ }
618
+
619
+ function existingCustomEntry(cfgMod, id) {
620
+ const entry = cfgMod.loadConfig().providers?.[id];
621
+ if (!isCustomProviderId(id) || !isCustomProviderConfig(entry)) throw new Error(`unknown custom provider "${id}"`);
622
+ return entry;
623
+ }
624
+
625
+ function inputObject(input) {
626
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('custom provider is invalid');
627
+ return input;
628
+ }
629
+
630
+ // Validated, secret-free config for `input`; the key (typed, else stored for an
631
+ // existing provider) is resolved separately and never enters the config.
632
+ async function resolveCustomInput(cfgMod, rawInput, options) {
633
+ const input = inputObject(rawInput);
634
+ const id = input.id === undefined || input.id === null || input.id === '' ? null : String(input.id);
635
+ if (id) existingCustomEntry(cfgMod, id);
636
+ const { normalizeCustomProviderConfig } = await loadCustomProviderModule();
637
+ const config = normalizeCustomProviderConfig(input, options);
638
+ const typed = typeof input.apiKey === 'string' ? input.apiKey.trim() : '';
639
+ const apiKey = typed || (id ? getAgentApiKey(id) || '' : '');
640
+ return { id, config, typed, apiKey };
641
+ }
642
+
643
+ export async function saveCustomProvider(cfgMod, rawInput) {
644
+ const { id: existingId, config, typed, apiKey } = await resolveCustomInput(cfgMod, rawInput, { requireModels: false });
645
+ if (!apiKey) throw new Error('API key is required');
646
+ const id = existingId || `custom-${randomUUID()}`;
647
+ const entry = {
648
+ type: 'custom',
649
+ name: config.name,
650
+ protocol: config.protocol,
651
+ baseURL: config.baseURL,
652
+ models: config.models,
653
+ enabled: true,
654
+ };
655
+ const account = SECRET_ACCOUNTS.agentApiKey(id);
656
+ const previousKey = typed && existingId ? getAgentApiKey(id) : null;
657
+ if (typed) saveSecret(account, typed);
658
+ try {
659
+ const current = cfgMod.loadConfig();
660
+ cfgMod.saveConfig({ ...current, providers: { ...(current.providers || {}), [id]: entry } }, { baseConfig: current });
661
+ } catch (error) {
662
+ if (typed) {
663
+ if (previousKey) saveSecret(account, previousKey);
664
+ else deleteSecret(account);
665
+ }
666
+ throw error;
667
+ }
668
+ return customProviderRow(id, entry);
669
+ }
670
+
671
+ export function removeCustomProvider(cfgMod, rawId) {
672
+ const id = String(rawId || '').trim();
673
+ existingCustomEntry(cfgMod, id);
674
+ const current = cfgMod.loadConfig();
675
+ const { [id]: _removed, ...providers } = current.providers || {};
676
+ cfgMod.saveConfig({ ...current, providers }, { baseConfig: current });
677
+ deleteSecret(SECRET_ACCOUNTS.agentApiKey(id));
678
+ return { provider: id, type: 'custom', removed: true };
679
+ }
680
+
681
+ export async function testCustomProvider(cfgMod, rawInput) {
682
+ const { id, config, apiKey } = await resolveCustomInput(cfgMod, rawInput, { requireModels: false });
683
+ try {
684
+ const models = config.models.length ? config.models : (await discoverCustomProviderModels(cfgMod, rawInput)).models;
685
+ const model = models[0]?.id;
686
+ if (!model) throw new Error('No models were found for this provider');
687
+ const { createCustomProvider } = await loadCustomProviderModule();
688
+ const provider = await createCustomProvider(id || 'custom-test', { ...config, apiKey });
689
+ await withTimeout(
690
+ (signal) => provider.send([{ role: 'user', content: 'Reply with OK.' }], model, [], { signal, maxTokens: 16 }),
691
+ CUSTOM_TEST_TIMEOUT_MS,
692
+ 'connection test'
693
+ );
694
+ return { ok: true };
695
+ } catch (error) {
696
+ throw sanitizeCustomError(error, [apiKey]);
697
+ }
698
+ }
699
+
700
+ export async function discoverCustomProviderModels(cfgMod, rawInput) {
701
+ const { id, config, apiKey } = await resolveCustomInput(cfgMod, rawInput, { requireModels: false });
702
+ try {
703
+ const { createCustomProvider } = await loadCustomProviderModule();
704
+ const provider = await createCustomProvider(id || 'custom-discovery', { ...config, models: [], apiKey });
705
+ const listed = await withTimeout(() => provider.listModels(), CUSTOM_DISCOVERY_TIMEOUT_MS, 'model discovery');
706
+ const seen = new Set();
707
+ const models = [];
708
+ for (const item of Array.isArray(listed) ? listed : []) {
709
+ const modelId = String(item?.id || '').trim();
710
+ if (!modelId || seen.has(modelId)) continue;
711
+ seen.add(modelId);
712
+ const model = { id: modelId };
713
+ const name = String(item.name || item.display || '').trim();
714
+ if (name && name !== modelId) model.name = name;
715
+ if (Number(item.contextWindow) > 0) model.contextWindow = Math.floor(Number(item.contextWindow));
716
+ const out = Number(item.maxOutputTokens ?? item.outputTokens);
717
+ if (out > 0) model.maxOutputTokens = Math.floor(out);
718
+ models.push(model);
719
+ }
720
+ return { models };
721
+ } catch (error) {
722
+ throw sanitizeCustomError(error, [apiKey]);
723
+ }
724
+ }
725
+
528
726
  export function saveOpenAIUsageSessionKey(cfgMod, secret) {
529
727
  const value = String(secret || '').trim();
530
728
  if (!value) throw new Error('OpenAI usage session key is required for credit lookup');
@@ -1,12 +1,9 @@
1
1
  // Built-in tool modules: the enable/install toggles (web search, memory, git,
2
2
  // office, tidy, local provider), the recap switch and the Code Tidy card.
3
3
  import {
4
- BRIDGE_FIRST_USE_IDS,
5
4
  INSTALLABLE_BUILTIN_IDS,
6
5
  builtinFeatureActive,
7
- builtinFirstUseApproval,
8
6
  builtinInstalled,
9
- setBuiltinFirstUseApprovalInConfig,
10
7
  setBuiltinInstalledInConfig,
11
8
  } from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
12
9
  import { LOCAL_PROVIDER_ID } from '../runtime/local-provider/managed-runtime.mjs';
@@ -115,19 +112,6 @@ export function createBuiltinToolSettings(
115
112
  await refreshEmptySessionToolPolicy?.();
116
113
  return this.getToolModuleSettings();
117
114
  },
118
- /** Once-per-session approval before the first Browser Use or Computer Use
119
- * call. The setting is read at each call, so it applies at once. */
120
- async setBridgeFirstUseApproval(name, enabled) {
121
- if (!BRIDGE_FIRST_USE_IDS.includes(name)) {
122
- throw new TypeError('First-use approval applies to browser or computer.');
123
- }
124
- saveConfigAndAdopt(setBuiltinFirstUseApprovalInConfig({ ...getConfig() }, name, enabled !== false));
125
- return {
126
- name,
127
- firstUseApproval: builtinFirstUseApproval(getConfig(), name),
128
- appliesTo: 'the next first use in any session, including this one',
129
- };
130
- },
131
115
  async setBuiltinToolEnabled(name, enabled) {
132
116
  if (name !== 'git' && name !== 'office' && name !== 'tidy' && name !== 'localProvider') {
133
117
  throw new TypeError('Built-in tool must be git, office, tidy, or localProvider.');
@@ -3,7 +3,7 @@
3
3
  * reconnects, and the empty-session tool-policy refresh all apply exactly as
4
4
  * they do for a UI click. The facade is read lazily because runtime-core
5
5
  * registers the tool executor before it finishes assembling the API object. */
6
- import { builtinFeatureActive, builtinFirstUseApproval } from '../../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
6
+ import { builtinFeatureActive } from '../../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
7
7
  import { ORCHESTRATION_MODES } from '../../runtime/shared/orchestration.mjs';
8
8
  import { SETUP_DESKTOP_DOMAINS, SETUP_HANDOFFS } from './settings-contract.mjs';
9
9
  import { executeExtendedSetupAction, publicAutomation, validateMcpInput } from './extended-actions.mjs';
@@ -253,14 +253,8 @@ const SETUP_STATUS_READERS = {
253
253
  const config = getConfig();
254
254
  return {
255
255
  ...featureFlags(rt),
256
- browser: {
257
- active: builtinFeatureActive(config, 'browser'),
258
- firstUseApproval: builtinFirstUseApproval(config, 'browser'),
259
- },
260
- computer: {
261
- active: builtinFeatureActive(config, 'computer'),
262
- firstUseApproval: builtinFirstUseApproval(config, 'computer'),
263
- },
256
+ browser: { active: builtinFeatureActive(config, 'browser') },
257
+ computer: { active: builtinFeatureActive(config, 'computer') },
264
258
  };
265
259
  },
266
260
  shell: (rt) => rt.getSystemShell?.() || {},
@@ -398,10 +392,6 @@ const SETUP_ACTION_HANDLERS = {
398
392
  if (DESKTOP_HOSTED_FEATURES.includes(name)) return requestDesktop(args);
399
393
  return rt.setBuiltinToolEnabled(name, requireBoolean(args.enabled));
400
394
  },
401
- set_first_use_approval: (rt, args) => {
402
- const name = requireEnum(args.name, ['browser', 'computer'], 'name');
403
- return rt.setBridgeFirstUseApproval(name, requireBoolean(args.enabled));
404
- },
405
395
  install_builtin: async (rt, args, { requestDesktop }) => {
406
396
  const name = requireEnum(args.name, SETUP_INSTALLABLE_FEATURES, 'name');
407
397
  if (DESKTOP_HOSTED_FEATURES.includes(name)) return requestDesktop(args);
@@ -596,6 +586,10 @@ export function createSetupToolExecutor({ getApi, getConfig, notifySessionUi, ge
596
586
  const result = await execute(args, options);
597
587
  if (READ_ONLY_SETUP_ACTIONS.has(args.action)) return JSON.stringify(result ?? {}, null, 2);
598
588
  await flushSettings?.();
589
+ // Attached surfaces re-read what changed instead of waiting for their
590
+ // next open or cache expiry.
591
+ const sessionId = clean(getSessionId?.());
592
+ if (sessionId) notifySessionUi?.(sessionId, 'Settings changed', { kind: 'setup-changed', action: args.action });
599
593
  return setupMutationReceipt(args.action, result);
600
594
  },
601
595
  };
@@ -78,7 +78,6 @@ export const SETUP_ACTION_FIELDS = Object.freeze({
78
78
  set_recap_enabled: 'enabled',
79
79
  set_web_search_enabled: 'enabled',
80
80
  set_builtin_enabled: 'name enabled',
81
- set_first_use_approval: 'name enabled',
82
81
  install_builtin: 'name',
83
82
  install_local_model: 'modelId',
84
83
  start_local_installation: 'phase modelId?',
@@ -209,11 +208,11 @@ export const SETUP_TOOL_DEFS = Object.freeze([
209
208
  enabled: {
210
209
  type: 'boolean',
211
210
  description:
212
- 'Boolean setters (set_*_enabled, set_first_use_approval, set_compaction, set_auto_update, set_mcp_enabled, set_plugin_enabled).',
211
+ 'Boolean setters (set_*_enabled, set_compaction, set_auto_update, set_mcp_enabled, set_plugin_enabled).',
213
212
  },
214
213
  name: {
215
214
  type: 'string',
216
- description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; revoke_linked_device: client id from status connection, not its display name; set_developer_option: option id from status developer; else MCP/plugin/provider id.`,
215
+ description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. revoke_linked_device: client id from status connection, not its display name; set_developer_option: option id from status developer; read_definition/delete_definition: the definition id; else MCP/plugin/provider id.`,
217
216
  },
218
217
  accountId: {
219
218
  type: 'string',
@@ -71,7 +71,6 @@ export function createToolPolicyRefresh({
71
71
  getDataDir,
72
72
  modelStandaloneTools,
73
73
  featureDisallowedTools,
74
- memoryToolsEnabled,
75
74
  loadCoreMemoryContext,
76
75
  activeWorkflowContext,
77
76
  invalidatePreSessionToolSurface,
@@ -94,7 +93,6 @@ export function createToolPolicyRefresh({
94
93
  }
95
94
 
96
95
  async function leadCoreMemoryContext() {
97
- if (!memoryToolsEnabled()) return '';
98
96
  try {
99
97
  return await loadCoreMemoryContext();
100
98
  } catch {
@@ -10,12 +10,16 @@ import { createSession } from '../runtime/agent/orchestrator/session/manager/ses
10
10
  import { askSession } from '../runtime/agent/orchestrator/session/manager/ask-session.mjs';
11
11
  import { modelRouteFields, parseScheduleModelRef } from '../runtime/shared/schedule-model-ref.mjs';
12
12
  import { automationWorkflowOpts } from './automation-workflow.mjs';
13
+ import { runAutomationTurns } from './automation-agents.mjs';
13
14
  import { automationPromptContent } from '../runtime/shared/automation-attachments.mjs';
14
15
 
15
16
  // Webhook payloads are authenticated transport data, not trusted user intent.
16
17
  // Keep their sessions on the deterministic read-only bundle even when a
17
- // preset or workflow would otherwise resolve to full tools.
18
- export const WEBHOOK_SESSION_TOOLS = Object.freeze(['tools:readonly']);
18
+ // preset or workflow would otherwise resolve to full tools. `tools:mcp` adds
19
+ // only the automation scope's internal tools — the agent tool, offered when the
20
+ // user's orchestration mode allows delegation (user decision: automations may
21
+ // delegate); the scope connects no MCP servers.
22
+ export const WEBHOOK_SESSION_TOOLS = Object.freeze(['tools:readonly', 'tools:mcp']);
19
23
 
20
24
  /** Endpoint model ref wins; the maintenance.webhook route is the fallback. */
21
25
  function webhookRoute(modelRef) {
@@ -66,14 +70,9 @@ export async function runWebhookSession({
66
70
  // askOpts.signal is the parent-abort link: it is cascaded onto the turn
67
71
  // controller, so aborting it stops the in-flight provider/tool work instead
68
72
  // of leaving a timed-out delivery's session running with live side effects.
69
- const result = await askSession(
70
- session.id,
71
- content,
72
- null,
73
- null,
74
- projectCwd || undefined,
75
- undefined,
76
- signal ? { signal } : {}
77
- );
73
+ const ask = (turnContent) =>
74
+ askSession(session.id, turnContent, null, null, projectCwd || undefined, undefined, signal ? { signal } : {});
75
+ // Agents the run delegates to report back; each result is the next turn.
76
+ const result = await runAutomationTurns(session.id, () => ask(content), ask);
78
77
  return { sessionId: session.id, result: String(result?.content || '') };
79
78
  }
@@ -73,6 +73,11 @@ export function createAgentRouteApi(deps) {
73
73
  const keepOff = requested.disabled === true;
74
74
  if (keepOff && !(clean(requested.provider) && clean(requested.model))) {
75
75
  saveDisabled(id, true);
76
+ // An explicitly empty provider still asks to drop the override, as it
77
+ // does for an agent that stays on: off and inheriting Main.
78
+ if (hasOwn(requested, 'provider') && !clean(requested.provider)) {
79
+ return { ...clearOverride(id), id, disabled: true };
80
+ }
76
81
  return { ...stored, id, disabled: true };
77
82
  }
78
83
  if (requested.disabled === false && Object.keys(requested).length === 1) {
@@ -8,6 +8,8 @@
8
8
  // this control exists — the host takes `execute` as a constructor option), and
9
9
  // `cwd`. Output: { canonicalAgentTool, execute }.
10
10
 
11
+ import { deliverAutomationCompletion } from '../session-runtime/automation-agents.mjs';
12
+
11
13
  export function createCanonicalAgentControl({ getSessionService, getSessionRuntimeHost, cwd }) {
12
14
  let canonicalAgentToolPromise = null;
13
15
 
@@ -32,7 +34,12 @@ export function createCanonicalAgentControl({ getSessionService, getSessionRunti
32
34
  isKeychainPrewarmReady: () => true,
33
35
  sessionSurface: sessionService.agentSurface,
34
36
  notifySessionCompletion(ownerSessionId, text, meta = {}) {
35
- return getSessionRuntimeHost()?.notifySessionCompletion?.(ownerSessionId, text, meta) === true;
37
+ // A schedule/webhook run owns no session runtime; its own
38
+ // agent-result waiter takes the completions it is owed.
39
+ return (
40
+ getSessionRuntimeHost()?.notifySessionCompletion?.(ownerSessionId, text, meta) === true ||
41
+ deliverAutomationCompletion(ownerSessionId, text, meta)
42
+ );
36
43
  },
37
44
  });
38
45
  })
@@ -71,6 +71,7 @@ import { createLagProfiler } from './daemon-lag-profiler.mjs';
71
71
  import { createChannelsRuntimeLoader } from './daemon-channels-loader.mjs';
72
72
  import { createDesktopRuntime } from './daemon-desktop-runtime.mjs';
73
73
  import { createCanonicalAgentControl } from './daemon-agent-control.mjs';
74
+ import { registerAutomationAgentTool } from '../session-runtime/automation-agents.mjs';
74
75
  import {
75
76
  compareRuntimeVersions,
76
77
  SESSION_CAPABILITY_FINGERPRINT,
@@ -666,6 +667,9 @@ async function main() {
666
667
  getSessionRuntimeHost: () => sessionRuntimeHost,
667
668
  cwd: CWD,
668
669
  });
670
+ // Schedules and webhooks run headless here; their sessions reach the same
671
+ // canonical agent control through the automation tool scope.
672
+ registerAutomationAgentTool(agentControl.execute);
669
673
  const requestedRuntimeMode = String(process.env.MIXDOG_SESSION_RUNTIME_MODE || '')
670
674
  .trim()
671
675
  .toLowerCase();
@@ -93,6 +93,10 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
93
93
  'deleteMediaAsset',
94
94
  'deleteMediaAssets',
95
95
  'deleteSchedule',
96
+ 'saveCustomProvider',
97
+ 'removeCustomProvider',
98
+ 'testCustomProvider',
99
+ 'discoverCustomProviderModels',
96
100
  'deleteSession',
97
101
  'deleteWebhook',
98
102
  'deleteWorkflow',
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { theme } from '../theme.mjs';
9
9
  import { displayModelName } from '../../runtime/shared/model-display.mjs';
10
+ import { isRollingModelAlias } from '../../runtime/shared/model-alias.mjs';
10
11
 
11
12
  export const parsedModelVersion = (id) => {
12
13
  const text = String(id || '').toLowerCase();
@@ -147,7 +148,15 @@ export const normalizeModelOptions = (models) => {
147
148
  const providerModels = [];
148
149
  for (const [family, group] of families.entries()) {
149
150
  const limit = modelFamilyLimit(provider, family);
150
- providerModels.push(...group.slice().sort(compareModelRecency).slice(0, limit));
151
+ // Version limits must not hide rolling routes, even when several
152
+ // vendors' "~..." IDs share the fallback family.
153
+ let versions = 0;
154
+ providerModels.push(
155
+ ...group
156
+ .slice()
157
+ .sort(compareModelRecency)
158
+ .filter((model) => isRollingModelAlias(model.id) || versions++ < limit)
159
+ );
151
160
  }
152
161
  normalized.push(...providerModels.sort(compareModelRecency));
153
162
  }
@@ -44194,7 +44194,6 @@ import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:pa
44194
44194
  import { homedir } from "node:os";
44195
44195
  import { dirname, join } from "node:path";
44196
44196
  import { fileURLToPath } from "node:url";
44197
- var DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
44198
44197
  var homeBase = null;
44199
44198
  var homePath = null;
44200
44199
  var dataBase = null;
@@ -47687,7 +47686,9 @@ var LITELLM_FLAG_FIELDS = [
47687
47686
  "supports_web_search",
47688
47687
  "supports_websearch",
47689
47688
  "supports_prompt_caching",
47690
- "supports_reasoning"
47689
+ "supports_reasoning",
47690
+ // Reader: supportsAnthropicFastMode (anthropic-betas.mjs).
47691
+ "supports_fast_mode"
47691
47692
  ];
47692
47693
  var LITELLM_STRING_FIELDS = ["mode", "litellm_provider", "reasoning_content_field"];
47693
47694
  function projectLitellmRow(row) {
@@ -55202,6 +55203,11 @@ function createMaintenancePickers({
55202
55203
  };
55203
55204
  }
55204
55205
 
55206
+ // src/runtime/shared/model-alias.mjs
55207
+ function isRollingModelAlias(id) {
55208
+ return /-latest$/i.test(String(id || "").trim());
55209
+ }
55210
+
55205
55211
  // src/tui/app/model-options.mjs
55206
55212
  var parsedModelVersion = (id) => {
55207
55213
  const text = String(id || "").toLowerCase();
@@ -55321,7 +55327,10 @@ var normalizeModelOptions = (models) => {
55321
55327
  const providerModels = [];
55322
55328
  for (const [family, group] of families.entries()) {
55323
55329
  const limit = modelFamilyLimit(provider, family);
55324
- providerModels.push(...group.slice().sort(compareModelRecency).slice(0, limit));
55330
+ let versions = 0;
55331
+ providerModels.push(
55332
+ ...group.slice().sort(compareModelRecency).filter((model) => isRollingModelAlias(model.id) || versions++ < limit)
55333
+ );
55325
55334
  }
55326
55335
  normalized.push(...providerModels.sort(compareModelRecency));
55327
55336
  }
@@ -15,8 +15,15 @@ export function createNotificationRouter({
15
15
  modelNotification,
16
16
  }) {
17
17
  let uiOpenSeq = 0;
18
+ let setupChangeSeq = 0;
18
19
  const uiActions = {
19
20
  'setup-ui': (delivery) => set({ setupUiRequest: { id: delivery.id, at: Date.now() } }),
21
+ 'setup-changed': (delivery) => {
22
+ // Monotonic like ui-open: consecutive changes by the same action must
23
+ // each refresh the attached surfaces.
24
+ setupChangeSeq += 1;
25
+ set({ setupChanged: { action: delivery.setupAction, seq: setupChangeSeq, at: Date.now() } });
26
+ },
20
27
  'ui-open': (delivery) => {
21
28
  // Monotonic seq: the same command requested twice must fire twice, and
22
29
  // the consuming App effect keys on the object identity + seq.
@@ -13,9 +13,17 @@
13
13
  * pipe is down. Ownership/promotion semantics are unchanged — the pipe is a
14
14
  * transport, never a second writer.
15
15
  */
16
+ import { createHash } from 'node:crypto';
17
+ import { join } from 'node:path';
16
18
  import { createOwnerLeg } from './live-share/owner.mjs';
17
19
  import { createViewerLeg } from './live-share/viewer.mjs';
18
20
 
21
+ // A unix socket path is a fixed sun_path buffer: 104 bytes on macOS and the
22
+ // BSDs, 108 on Linux, NUL included. A desktop session id alone is 64 hex, so
23
+ // the path beside its session file runs past the macOS limit and listen()
24
+ // failed there for every desktop session.
25
+ const UNIX_SOCKET_PATH_MAX_BYTES = 103;
26
+
19
27
  // Session entry is latency-sensitive: the owner pipe can be a few event-loop
20
28
  // turns behind the viewer resume. Retry locally instead of waiting for the
21
29
  // coarse 3s session runtime safety tick.
@@ -23,7 +31,14 @@ const LIVE_CONNECT_RETRY_MIN_MS = 10;
23
31
  const LIVE_CONNECT_RETRY_MAX_MS = 160;
24
32
 
25
33
  export function liveSharePipePath(sessionId, sessionFilePath) {
26
- return process.platform === 'win32' ? `\\\\.\\pipe\\mixdog-live-${sessionId}` : `${sessionFilePath}.live.sock`;
34
+ if (process.platform === 'win32') return `\\\\.\\pipe\\mixdog-live-${sessionId}`;
35
+ const beside = `${sessionFilePath}.live.sock`;
36
+ if (Buffer.byteLength(beside) <= UNIX_SOCKET_PATH_MAX_BYTES) return beside;
37
+ // Owner and viewer derive the same short name from the same session file.
38
+ // /tmp, not os.tmpdir(): TMPDIR is itself long on macOS and may differ
39
+ // between the desktop daemon and a terminal, which would split the pair.
40
+ const digest = createHash('sha256').update(beside).digest('hex').slice(0, 24);
41
+ return join('/tmp', `mixdog-live-${process.getuid()}-${digest}.sock`);
27
42
  }
28
43
 
29
44
  export function createLiveShare({
@@ -89,6 +89,10 @@ export function resolveTuiRuntimeNotificationDelivery(event, text) {
89
89
  if (meta.kind === 'setup-ui') {
90
90
  return meta.id ? { action: 'setup-ui', id: String(meta.id) } : { action: 'ignore' };
91
91
  }
92
+ if (meta.kind === 'setup-changed') {
93
+ const setupAction = String(meta.action || '').trim();
94
+ return setupAction ? { action: 'setup-changed', setupAction } : { action: 'ignore' };
95
+ }
92
96
  if (meta.kind === 'ui-open') {
93
97
  const command = String(meta.command || '')
94
98
  .trim()
@@ -159,6 +159,18 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
159
159
  pushNotice(`provider api key saved: ${result.provider}`, 'info');
160
160
  return true;
161
161
  },
162
+ saveCustomProvider: announced(
163
+ (input) => runtime.saveCustomProvider(input),
164
+ (result) => `custom provider saved: ${result.id}`
165
+ ),
166
+ removeCustomProvider: async (id) => {
167
+ const result = await runtime.removeCustomProvider(id);
168
+ refreshRouteStats();
169
+ pushNotice(`custom provider removed: ${result.provider}`, 'info');
170
+ return result;
171
+ },
172
+ testCustomProvider: (input) => runtime.testCustomProvider(input),
173
+ discoverCustomProviderModels: (input) => runtime.discoverCustomProviderModels(input),
162
174
  saveOpenCodeGoUsageAuth: (opts) => {
163
175
  runtime.saveOpenCodeGoUsageAuth(opts);
164
176
  pushNotice('OpenCode Go usage auth saved', 'info');