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
@@ -21,7 +21,7 @@ import { CODEX_OAUTH_ORIGINATOR, codexModelsUrl } from './openai-codex-endpoints
21
21
  import { _normalizeCodexModel, _markLatestCodex } from './openai-codex-model.mjs';
22
22
 
23
23
  const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
24
- const CODEX_MODEL_CACHE_SCHEMA_VERSION = 5;
24
+ const CODEX_MODEL_CACHE_SCHEMA_VERSION = 6;
25
25
  const CATALOG_FETCH_TIMEOUT_MS = 10_000;
26
26
 
27
27
  // In-memory mirror of the on-disk catalog, same pattern as anthropic-oauth.
@@ -59,6 +59,12 @@ export function codexCatalogHas(id) {
59
59
  return _mirror.some((m) => m.id === id);
60
60
  }
61
61
 
62
+ /** The catalog's own answer, or null when it has none for this model. */
63
+ export function codexModelSupportsEffortUpdates(id) {
64
+ const supported = findCachedCodexModel(id)?.supportsReasoningEffortUpdates;
65
+ return typeof supported === 'boolean' ? supported : null;
66
+ }
67
+
62
68
  export function codexModelSupportsServiceTier(id, serviceTier) {
63
69
  return modelSupportsServiceTier(findCachedCodexModel(id), serviceTier);
64
70
  }
@@ -0,0 +1,24 @@
1
+ // Keep saved custom endpoints and OpenAI-compatible clients under the same
2
+ // transport policy: credentials may travel over HTTP only on loopback.
3
+ export function assertSafeBaseURL(rawURL, providerName) {
4
+ let parsed;
5
+ try {
6
+ parsed = new URL(String(rawURL));
7
+ } catch {
8
+ throw new Error(`[provider:${providerName}] invalid baseURL`);
9
+ }
10
+ const scheme = parsed.protocol.toLowerCase();
11
+ if (scheme !== 'https:' && scheme !== 'http:') {
12
+ throw new Error(`[provider:${providerName}] baseURL scheme not allowed: ${parsed.protocol} (only http/https)`);
13
+ }
14
+ if (scheme === 'http:') {
15
+ const host = parsed.hostname.toLowerCase();
16
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
17
+ if (!isLocal) {
18
+ throw new Error(
19
+ `[provider:${providerName}] baseURL must use https for non-localhost host (got ${parsed.protocol}//${parsed.hostname})`
20
+ );
21
+ }
22
+ }
23
+ return rawURL;
24
+ }
@@ -150,7 +150,16 @@ const PROVIDER_MODULES = {
150
150
  'opencode-go': ['./opencode-go.mjs', 'OpenCodeGoProvider'],
151
151
  };
152
152
 
153
- async function loadProviderCtor(name, signal = null) {
153
+ // User-defined providers (cfg.type === 'custom') are keyed by a generated
154
+ // `custom-<uuid>` id and never collide with the built-in names.
155
+ function isCustomProviderConfig(name, cfg) {
156
+ return cfg?.type === 'custom' && String(name).startsWith('custom-');
157
+ }
158
+
159
+ async function loadProviderCtor(name, signal = null, cfg = null) {
160
+ if (isCustomProviderConfig(name, cfg)) {
161
+ return loadProviderExport('custom-provider', './custom-provider.mjs', 'createCustomProvider', signal);
162
+ }
154
163
  if (Object.hasOwn(PROVIDER_MODULES, name)) {
155
164
  const [spec, exportName] = PROVIDER_MODULES[name];
156
165
  return loadProviderExport(name, spec, exportName, signal);
@@ -161,7 +170,8 @@ async function loadProviderCtor(name, signal = null) {
161
170
  throw new Error(`unknown enabled provider: ${name}`);
162
171
  }
163
172
 
164
- function instantiateProvider(name, Ctor, cfg) {
173
+ async function instantiateProvider(name, Ctor, cfg) {
174
+ if (isCustomProviderConfig(name, cfg)) return wrapProviderAdmission(await Ctor(name, cfg), name);
165
175
  if (Object.hasOwn(OPENAI_COMPAT_PRESETS, name) && name !== 'opencode-go') {
166
176
  return wrapProviderAdmission(new Ctor(name, cfg), name);
167
177
  }
@@ -239,9 +249,9 @@ async function _initProvidersUnsynchronized(config, signal = null) {
239
249
  return { name, inst: providers.get(name), sig };
240
250
  }
241
251
  try {
242
- const Ctor = await loadProviderCtor(name, signal);
252
+ const Ctor = await loadProviderCtor(name, signal, cfg);
243
253
  throwIfAborted(signal);
244
- const inst = instantiateProvider(name, Ctor, cfg);
254
+ const inst = await instantiateProvider(name, Ctor, cfg);
245
255
  return { name, inst, sig };
246
256
  } catch (err) {
247
257
  const msg = err instanceof Error ? err.message : String(err);
@@ -469,6 +469,22 @@ const CONTEXT_OVERFLOW_PATTERNS = [
469
469
  /context[_ ]length[_ ]exceeded/i,
470
470
  /prompt is too long/i,
471
471
  /reduce the length of (?:the )?(?:messages|input|prompt)/i,
472
+ // Wordings other backends use for the same refusal, each tied to a context
473
+ // or input-length noun so a tokens-per-minute rate limit never matches.
474
+ /prompt too long/i, // z.ai, Ollama
475
+ /prompt exceeds max length/i, // z.ai CN endpoint
476
+ /input is too long for requested model/i, // Amazon Bedrock
477
+ /input token count.*exceeds the maximum/i, // Google
478
+ /maximum prompt length is \d+/i, // xAI
479
+ /exceeds (?:the )?maximum allowed input length/i, // OpenRouter relays
480
+ /is longer than the model'?s context length/i, // Together AI
481
+ /exceeds the available context size/i, // llama.cpp server
482
+ /greater than the context length/i, // LM Studio
483
+ /context window exceeds limit/i, // MiniMax
484
+ /exceeded model token limit/i, // Kimi
485
+ /too large for model with \d+ maximum context length/i, // Mistral
486
+ /range of input length should be/i, // DashScope / Qwen
487
+ /model_context_window_exceeded/i, // z.ai finish reason surfaced as text
472
488
  ];
473
489
 
474
490
  /**
@@ -148,26 +148,6 @@ export function localGitToolsActive(configLike, { gitAvailable } = {}) {
148
148
  );
149
149
  }
150
150
 
151
- /** Capabilities that ask the user once per session before their first live
152
- * call: the desktop and the browser are the user's, and one approval at the
153
- * moment of first use is how the user learns the model reached for them. */
154
- export const BRIDGE_FIRST_USE_IDS = Object.freeze(['browser', 'computer']);
155
-
156
- /** On unless the profile turns it off for that capability;
157
- * MIXDOG_BRIDGE_FIRST_USE_APPROVAL overrides per process (headless, bench). */
158
- export function builtinFirstUseApproval(configLike, id) {
159
- return (
160
- featureEnvOverride('MIXDOG_BRIDGE_FIRST_USE_APPROVAL') ?? configLike?.builtins?.[id]?.firstUseApproval !== false
161
- );
162
- }
163
-
164
- export function setBuiltinFirstUseApprovalInConfig(configLike, id, enabled) {
165
- const next = { ...(configLike || {}) };
166
- next.builtins = { ...(next.builtins || {}) };
167
- next.builtins[id] = { ...(next.builtins[id] || {}), firstUseApproval: enabled !== false };
168
- return next;
169
- }
170
-
171
151
  export function setBuiltinInstalledInConfig(configLike, id, installed = true) {
172
152
  const next = { ...(configLike || {}) };
173
153
  next.builtins = { ...(next.builtins || {}) };
@@ -14,7 +14,7 @@ export function makeResolveDefaultProvider(isKnownProvider) {
14
14
  return function resolveDefaultProvider(config) {
15
15
  const selectedMain = findPreset(config, config?.default);
16
16
  const configured = clean(selectedMain?.provider);
17
- if (configured && isKnownProvider(configured)) return configured;
17
+ if (configured && isKnownProvider(configured, config)) return configured;
18
18
  return DEFAULT_PROVIDER;
19
19
  };
20
20
  }
@@ -124,7 +124,8 @@ export function isLikelyRawModelId(value) {
124
124
  const model = clean(value);
125
125
  if (!model || model.length > 160) return false;
126
126
  if (/\s/.test(model)) return false;
127
- return /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(model);
127
+ // OpenRouter's rolling model aliases start with a literal "~".
128
+ return /^~?[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(model);
128
129
  }
129
130
 
130
131
  export function validateRequestedModelSelector(config, requested = {}) {
@@ -5,6 +5,7 @@
5
5
  import { clean, hasOwn } from './session-text.mjs';
6
6
  import { modelSupportsServiceTier } from '../providers/model-service-tiers.mjs';
7
7
  import { openAiDirectSupportsFast } from '../providers/openai-direct-request.mjs';
8
+ import { supportsAnthropicFastMode } from '../../../shared/llm/anthropic-betas.mjs';
8
9
 
9
10
  const FAST_CAPABLE_PROVIDERS = new Set([
10
11
  'anthropic',
@@ -52,16 +53,11 @@ function geminiModelSupportsHostedWebSearch(model) {
52
53
  function anthropicModelSupportsHostedWebSearch(model) {
53
54
  const id = clean(model?.id || model).toLowerCase();
54
55
  if (!id) return false;
55
- const match = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[-.](\d+))?/);
56
+ const match = id.match(/^claude-(opus|sonnet|haiku|fable|mythos)-(\d+)(?:[-.](\d+))?/);
56
57
  if (!match) return false;
57
58
  return (Number(match[2]) || 0) >= 4;
58
59
  }
59
60
 
60
- function anthropicModelMetaSupportsFast(model) {
61
- const id = clean(model?.id || model).toLowerCase();
62
- return /^claude-(opus|sonnet)/.test(id);
63
- }
64
-
65
61
  export function fastCapableFor(provider, model, effort = null, modelParameters = {}) {
66
62
  const p = clean(provider);
67
63
  if (!FAST_CAPABLE_PROVIDERS.has(p)) return false;
@@ -82,7 +78,7 @@ export function fastCapableFor(provider, model, effort = null, modelParameters =
82
78
  }
83
79
  if (p === 'openai') return openAiDirectSupportsFast(model);
84
80
  if (p === 'openai-oauth') return modelSupportsServiceTier(model, 'priority');
85
- if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelMetaSupportsFast(model);
81
+ if (p === 'anthropic' || p === 'anthropic-oauth') return supportsAnthropicFastMode(clean(model?.id || model));
86
82
  return false;
87
83
  }
88
84
 
@@ -127,9 +123,15 @@ export function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfi
127
123
  } else {
128
124
  delete nextSetting.modelParameters;
129
125
  }
130
- const contextPercent = Number(route.contextPercent);
131
- if (Number.isFinite(contextPercent) && contextPercent >= 10 && contextPercent <= 100) {
132
- nextSetting.contextPercent = Math.round(contextPercent / 10) * 10;
126
+ // Only a window the user moved off the model's default is a setting. The
127
+ // pickers send the default stop with every selection; storing it froze each
128
+ // model at whatever its default was that day, so a later default never
129
+ // reached anyone who had already selected the model.
130
+ const requestedPercent = Number(route.contextPercent);
131
+ const contextPercent = Math.round(requestedPercent / 10) * 10;
132
+ const isDefault = contextPercent === Number(route.contextDefaultPercent);
133
+ if (requestedPercent >= 10 && requestedPercent <= 100 && !isDefault) {
134
+ nextSetting.contextPercent = contextPercent;
133
135
  } else {
134
136
  delete nextSetting.contextPercent;
135
137
  }
@@ -119,7 +119,9 @@ function summarySendOpts(opts) {
119
119
  effort: 'low',
120
120
  effortConfiguration: undefined,
121
121
  effortConfigurationEnabled: false,
122
- fast: opts.fast ?? opts.sendOpts?.fast ?? true,
122
+ // Fast mode is opt-in (it needs separate account credits); the summary
123
+ // follows the route/session choice and never enables it on its own.
124
+ fast: opts.fast ?? opts.sendOpts?.fast ?? false,
123
125
  maxOutputTokens: opts.maxOutputTokens || SUMMARY_OUTPUT_TOKENS,
124
126
  providerState: undefined,
125
127
  onToolCall: undefined,
@@ -54,7 +54,7 @@ export async function resolveCompactionRoute({
54
54
  const contextWindow =
55
55
  sameModel && positiveInt(sessionRef?.contextWindow)
56
56
  ? positiveInt(sessionRef.contextWindow)
57
- : resolveSessionContextMeta(selectedProvider, selectedModel).contextWindow;
57
+ : resolveSessionContextMeta(selectedProvider, selectedModel, {}, { wholeWindow: true }).contextWindow;
58
58
  return {
59
59
  provider: selectedProvider,
60
60
  providerName,
@@ -2,6 +2,7 @@
2
2
  // resolution.
3
3
 
4
4
  import { getModelMetadataSync } from '../../providers/model-catalog.mjs';
5
+ import { contextWindowRange } from '../../../../shared/llm/default-context-window.mjs';
5
6
  import { positiveInt } from '../../../../shared/numbers.mjs';
6
7
 
7
8
  // Family-pattern fallback used only when the provider and external catalogs
@@ -96,11 +97,12 @@ function providerRawContextWindow(info, catalogInfo) {
96
97
  if (catalogWindow && fromCache !== catalogWindow) return catalogWindow;
97
98
  return fromCache || null;
98
99
  }
99
- export function resolveSessionContextMeta(provider, model, seed = {}) {
100
+ // `wholeWindow` asks for everything the model can take in (the summary route
101
+ // budgets its own input that way) instead of the window a session starts with.
102
+ export function resolveSessionContextMeta(provider, model, seed = {}, { wholeWindow = false } = {}) {
100
103
  const info = typeof provider?.getCachedModelInfo === 'function' ? provider.getCachedModelInfo(model) : null;
101
104
  const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
102
- const requestedContextWindow =
103
- positiveInt(seed.selectedContextWindow) ||
105
+ const servedContextWindow =
104
106
  providerRawContextWindow(info, catalogInfo) ||
105
107
  positiveInt(catalogInfo?.contextWindow) ||
106
108
  positiveInt(catalogInfo?.maxContextWindow) ||
@@ -110,6 +112,16 @@ export function resolveSessionContextMeta(provider, model, seed = {}) {
110
112
  positiveInt(seed.raw_context_window) ||
111
113
  positiveInt(seed.contextWindow) ||
112
114
  guessContextWindow(model, providerNameOf(provider));
115
+ // A session that records no selection starts from the model's default
116
+ // window, the one the picker shows. A recorded percentage without its window
117
+ // (a session older than selectedContextWindow, a route saved before the
118
+ // catalog warmed) is still the user's choice and keeps the served window.
119
+ const recordsSelection = wholeWindow || boundedPercent(seed.contextPercent) !== null;
120
+ const requestedContextWindow =
121
+ positiveInt(seed.selectedContextWindow) ||
122
+ (recordsSelection
123
+ ? servedContextWindow
124
+ : contextWindowRange({ provider: providerNameOf(provider), contextWindow: servedContextWindow }).defaultWindow);
113
125
  // A managed runtime's allocated capacity also bounds restored selections
114
126
  // and catalog metadata; raising a slider cannot allocate server memory.
115
127
  const runtimeContextWindow = positiveInt(info?.runtimeContextWindow);
@@ -13,7 +13,12 @@ import { buildProviderCacheOpts, cacheCapabilityForProvider } from '../../agent-
13
13
  import { normalizeAutoClearConfig, resolveAutoClearIdleMs } from '../../runtime-core/config-helpers.mjs';
14
14
  import { _buildBaseRules } from './rules-cache.mjs';
15
15
  import { composeSessionSystem, seedSessionMessages } from './session-prompt-composition.mjs';
16
- import { _prepareResumeTools, delegationDisabled, resolveSessionToolSurface } from './session-tool-surface.mjs';
16
+ import {
17
+ _prepareResumeTools,
18
+ delegationDisabled,
19
+ honoredSchemaAllowlist,
20
+ resolveSessionToolSurface,
21
+ } from './session-tool-surface.mjs';
17
22
  import { _forgetPreparedResume, _preparedResumeMatches, _readPreparedResume } from './prepared-resume-cache.mjs';
18
23
  import {
19
24
  filterModelEditToolNames,
@@ -175,6 +180,7 @@ export function createSession(opts) {
175
180
  logSessionSurface(surface);
176
181
  const contextMeta = resolveSessionContextMeta(provider, modelName, {
177
182
  selectedContextWindow: opts.selectedContextWindow,
183
+ contextPercent: route.contextPercent,
178
184
  });
179
185
  const session = buildSessionRecord({
180
186
  opts,
@@ -215,7 +221,10 @@ export function _refreshSessionRuleVariantsForModel(session, previousModel, prev
215
221
  ...(getHiddenAgent(session?.agent || null) ? ['Skill'] : []),
216
222
  ...(delegationDisabled(session, isAgentOwner(session)) ? ['agent'] : []),
217
223
  ];
218
- const allowTools = isAgentOwner(session) ? null : session?.schemaAllowedTools;
224
+ const allowTools = honoredSchemaAllowlist(session?.schemaAllowedTools, {
225
+ ownerIsAgent: isAgentOwner(session),
226
+ agent: session?.agent || null,
227
+ });
219
228
  const previousRules = _buildBaseRules({
220
229
  omitTools: [...deny, unusedModelEditToolName(previousModel)],
221
230
  allowTools,
@@ -63,7 +63,11 @@ function buildShellEnvironmentContext(opts, ownerIsAgent, toolsForRouting) {
63
63
  // four system blocks; environmentTailContext is the persisted env tail.
64
64
  export function composeSessionSystem(opts, { profile, providerName, modelName, surface }) {
65
65
  const { ownerIsAgent, resolvedAgent, skills } = surface;
66
- const skipAgentRules = opts.skipAgentRules === true;
66
+ // A tool-free hidden role (a one-shot classifier or title writer) carries
67
+ // only its own role rules: the shared tool policy and the worker conduct
68
+ // rules describe work it is offered no tool for.
69
+ const toolFreeRole = ownerIsAgent && surface.schemaAllowedTools?.length === 0;
70
+ const skipAgentRules = opts.skipAgentRules === true || toolFreeRole;
67
71
  const injectedRules = skipAgentRules
68
72
  ? ''
69
73
  : _buildBaseRules({
@@ -29,6 +29,19 @@ function resolveToolSpec(toolPreset, profile, ownerIsAgent) {
29
29
  return Array.isArray(profile?.tools) ? profile.tools : toolPreset;
30
30
  }
31
31
 
32
+ // The schema allowlist a session honours. Agent-owned sessions share one
33
+ // schema, so an allowlist handed down by a caller never narrows them — except
34
+ // the profile the session's own hidden role declares (agents.json
35
+ // toolSchemaProfile). Without that exception a tool-free maintenance role was
36
+ // sent the whole Agent schema on every one-shot call while the dispatch gate
37
+ // refused each of those tools (usage ledger 2026-09-30..10-05: 4104 cycle1
38
+ // calls at ~8.5K prompt tokens, about two thirds of it the 12-tool schema).
39
+ export function honoredSchemaAllowlist(schemaAllowedTools, { ownerIsAgent = false, agent = null } = {}) {
40
+ if (!Array.isArray(schemaAllowedTools)) return null;
41
+ if (ownerIsAgent && !getHiddenAgent(agent)) return null;
42
+ return schemaAllowedTools;
43
+ }
44
+
32
45
  // Every tool omitted by a caller schema allowlist is omitted from BP1 too.
33
46
  // The model never receives guidance for a tool it cannot call, and a new
34
47
  // process-wide built-in cannot leak into a narrow profile's prompt.
@@ -48,13 +61,17 @@ export function resolveSessionToolSurface(opts, { profile, toolPreset, modelName
48
61
  const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
49
62
  const hiddenAgent = getHiddenAgent(resolvedAgent);
50
63
  const isRetrievalAgent = hiddenAgent?.kind === 'retrieval';
51
- // Lead and Agent share the same cwd-scoped Skill inventory.
52
- const skills = opts.skipSkills ? [] : collectPromptSkillsCached(opts.cwd);
53
- // BP1 shared tool policy ships to EVERY role (Lead, workers, retrieval,
54
- // maintenance): its anti-spiral clauses (one anchor is enough, never
64
+ const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
65
+ const schemaAllowedTools = honoredSchemaAllowlist(opts.schemaAllowedTools, { ownerIsAgent, agent: resolvedAgent });
66
+ // Lead and Agent share the same cwd-scoped Skill inventory. A session that
67
+ // is offered no tool cannot load a skill, so it is not sent the manifest.
68
+ const skills = opts.skipSkills || schemaAllowedTools?.length === 0 ? [] : collectPromptSkillsCached(opts.cwd);
69
+ // BP1 shared tool policy ships to every role that is offered tools (Lead,
70
+ // workers, retrieval): its anti-spiral clauses (one anchor is enough, never
55
71
  // repeat equivalent patterns/scopes, plausible hit → stop) are exactly
56
72
  // what narrow retrieval roles need. Role docs
57
- // override role-inapplicable entries.
73
+ // override role-inapplicable entries. A tool-free role is sent none of it
74
+ // (session-prompt-composition.mjs).
58
75
  const sessionDeny = [
59
76
  ...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []),
60
77
  ...(delegationDisabled(opts, ownerIsAgent) ? ['agent'] : []),
@@ -70,8 +87,6 @@ export function resolveSessionToolSurface(opts, { profile, toolPreset, modelName
70
87
  cwd: opts.cwd || null,
71
88
  });
72
89
 
73
- const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
74
- const schemaAllowedTools = ownerIsAgent || !hasCallerAllow ? null : opts.schemaAllowedTools;
75
90
  const tools = finalizeSessionToolList(toolsForRouting, {
76
91
  schemaAllowedTools,
77
92
  disallowedTools: sessionDeny,
@@ -121,8 +136,7 @@ export function _prepareResumeTools(session, preset) {
121
136
  toolSpec,
122
137
  ownerIsAgent,
123
138
  tools: finalizeSessionToolList(toolsForRouting, {
124
- schemaAllowedTools:
125
- !ownerIsAgent && Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
139
+ schemaAllowedTools: honoredSchemaAllowlist(session.schemaAllowedTools, { ownerIsAgent, agent: session.agent }),
126
140
  disallowedTools: [
127
141
  ...(Array.isArray(session.disallowedTools) ? session.disallowedTools : []),
128
142
  ...(delegationDisabled(session, ownerIsAgent) ? ['agent'] : []),
@@ -1,8 +1,12 @@
1
1
  // Commands start in the foreground. Only work still running after the
2
- // 10 s coordination budget is promoted to a tracked background task.
2
+ // 15 s coordination budget is promoted to a tracked background task.
3
3
  // Short commands therefore complete in the original tool turn, while longer
4
4
  // work returns partial output plus task_id and finishes by notification.
5
- export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 10_000;
5
+ // 15 s because a command promoted moments before it ends costs a whole
6
+ // follow-up request on `task wait`. Session logs (2026-09-30..10-05), the 260
7
+ // promoted commands with a recorded duration: 108 had ended within 10 s, 144
8
+ // within 15 s and only 152 within 20 s.
9
+ export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 15_000;
6
10
 
7
11
  // JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
8
12
  // 2^31-1 wraps to a tiny/negative value and fires immediately. Clamp the
@@ -13,12 +17,17 @@ const TIMER_MAX_MS = 2_147_483_647;
13
17
 
14
18
  // Main-agent blocking budget. A timeout is the command's total deadline, not
15
19
  // permission to hold the conversation open for that whole duration: after
16
- // 10 s a still-running command becomes a tracked background task and
20
+ // 15 s a still-running command becomes a tracked background task and
17
21
  // completion is pushed to the owner. MIXDOG_SHELL_AUTO_BACKGROUND_MS
18
22
  // overrides; an explicit 0 disables. Gated on backgroundOnTimeout so
19
23
  // disabled background tasks remain foreground.
20
- // A per-call wait_ms replaces that default for the call, capped by the
21
- // foreground maximum.
24
+ // A per-call wait_ms extends that default for the call, capped by the
25
+ // foreground maximum; it never shortens it. A shorter window promotes a
26
+ // command that would have returned its result in the starting call and spends
27
+ // a whole follow-up request on `task wait`. Session logs (2026-09-30..10-05,
28
+ // 3062 shell calls): 329 passed a wait_ms under the default (53% of one model
29
+ // family's calls), 256 of those were promoted, and 108 of the 192 with a
30
+ // recorded duration had finished inside the default window (10 s then).
22
31
  function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroundMs) {
23
32
  const raw = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
24
33
  const parsed = Number(raw);
@@ -26,7 +35,7 @@ function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroun
26
35
  raw != null && String(raw).trim() !== '' && Number.isFinite(parsed) && parsed >= 0
27
36
  ? Math.floor(parsed)
28
37
  : DEFAULT_SHELL_AUTO_BACKGROUND_MS;
29
- if (waitMs > 0) defaultMs = Math.min(waitMs, maxForegroundMs);
38
+ if (waitMs > 0) defaultMs = Math.max(defaultMs, Math.min(waitMs, maxForegroundMs));
30
39
  if (!backgroundOnTimeout || defaultMs <= 0) return 0;
31
40
  return timeout > 0 ? Math.min(defaultMs, timeout) : defaultMs;
32
41
  }
@@ -146,7 +146,7 @@ export const BUILTIN_TOOLS = [
146
146
  openWorldHint: true,
147
147
  compressible: true,
148
148
  },
149
- description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 10s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id.'}`,
149
+ description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 15s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id.'}`,
150
150
  inputSchema: {
151
151
  type: 'object',
152
152
  properties: {
@@ -155,14 +155,14 @@ export const BUILTIN_TOOLS = [
155
155
  type: 'integer',
156
156
  minimum: 0,
157
157
  description:
158
- 'Hard kill deadline in ms (omit or 0 = none), separate from the 10s foreground window; set one for throwaway probes so a slow check fails fast.',
158
+ 'Hard kill deadline in ms (omit or 0 = none), separate from the 15s foreground window; set one for throwaway probes so a slow check fails fast.',
159
159
  },
160
160
  wait_ms: {
161
161
  type: 'integer',
162
- minimum: 1000,
162
+ minimum: 15000,
163
163
  maximum: 120000,
164
164
  description:
165
- 'Foreground wait in ms before unfinished work continues as a background task (default 10000); raise it for a command that should return its result in this call.',
165
+ 'Foreground wait in ms before unfinished work continues as a background task (default 15000); raise it for a command that should return its result in this call.',
166
166
  },
167
167
  },
168
168
  required: ['command'],
@@ -439,6 +439,8 @@ class NativePatchServer {
439
439
 
440
440
  #contractPromise = null;
441
441
 
442
+ #contractPending = false;
443
+
442
444
  constructor(binPath) {
443
445
  this.binPath = binPath;
444
446
  // windowsHide: mixdog-patch.exe is a console binary; without this each spawn
@@ -484,6 +486,13 @@ class NativePatchServer {
484
486
  this.#child.stdin.on('error', (err) => {
485
487
  this.failTransport(err);
486
488
  });
489
+ // A spawn that fails outright (an artifact without the execute bit, or one
490
+ // removed between the gate's read and the spawn) reports only through the
491
+ // child's own 'error' event; unheard, it crashed the host instead of
492
+ // failing the handshake closed.
493
+ this.#child.on('error', (err) => {
494
+ this.failTransport(err);
495
+ });
487
496
  this.#child.on('exit', (code, signal) => {
488
497
  this.exited = true;
489
498
  const err = new Error(`native patch server exited code=${code} signal=${signal} stderr=${this.stderr}`);
@@ -537,15 +546,23 @@ class NativePatchServer {
537
546
  // the caller may respawn and retry without any risk of double-applying.
538
547
  // Contract handshake on THIS session, memoized per instance: N concurrent
539
548
  // callers await one exchange, and a respawned session proves itself again.
549
+ /** True while the handshake runs: between its exchanges no waiter is queued, yet the handles must stay referenced. */
550
+ get contractPending() {
551
+ return this.#contractPending;
552
+ }
553
+
540
554
  contract() {
541
555
  if (!this.#contractPromise) {
556
+ this.#contractPending = true;
542
557
  this.#contractPromise = verifyContractOverSession(this.#handshakeView()).then(
543
558
  (ok) => {
559
+ this.#contractPending = false;
544
560
  this.#contractVerified = ok === true;
545
561
  if (!ok) this.markContractFailed();
546
562
  return ok;
547
563
  },
548
564
  () => {
565
+ this.#contractPending = false;
549
566
  this.#contractVerified = false;
550
567
  this.markContractFailed();
551
568
  return false;
@@ -877,7 +894,11 @@ export function scheduleNativePatchPrewarm() {
877
894
  }
878
895
  }
879
896
  await getNativePatchServer().ping();
880
- if (!nativePatchPersistent() && (_nativePatchServer?.waiters?.length || 0) === 0) {
897
+ if (
898
+ !nativePatchPersistent() &&
899
+ (_nativePatchServer?.waiters?.length || 0) === 0 &&
900
+ !_nativePatchServer?.contractPending
901
+ ) {
881
902
  _nativePatchServer?.unref();
882
903
  }
883
904
  if (nativePatchTraceEnabled()) {
@@ -98,6 +98,35 @@ async function probeAudioDurationSec(filePath) {
98
98
  }
99
99
  }
100
100
 
101
+ // Leading silence so a clipped onset still decodes as the first word. The
102
+ // bundled ffmpeg carries only aformat/anull/aresample
103
+ // (scripts/build-ffmpeg-runtime.sh), so the pad is spliced into the converted
104
+ // wav here: zero samples in front of its PCM data.
105
+ const ONSET_SILENCE_SECONDS = 0.3;
106
+
107
+ export function prependWavSilence(wavPath, seconds = ONSET_SILENCE_SECONDS) {
108
+ const wav = fs.readFileSync(wavPath);
109
+ let sampleRate = 0;
110
+ let blockAlign = 0;
111
+ for (let offset = 12; offset + 8 <= wav.length; ) {
112
+ const id = wav.toString('ascii', offset, offset + 4);
113
+ const size = wav.readUInt32LE(offset + 4);
114
+ if (id === 'fmt ') {
115
+ sampleRate = wav.readUInt32LE(offset + 12);
116
+ blockAlign = wav.readUInt16LE(offset + 20);
117
+ } else if (id === 'data') {
118
+ const silence = Buffer.alloc(Math.round(sampleRate * seconds) * blockAlign);
119
+ const out = Buffer.concat([wav.subarray(0, offset + 8), silence, wav.subarray(offset + 8)]);
120
+ out.writeUInt32LE(size + silence.length, offset + 4);
121
+ out.writeUInt32LE(out.length - 8, 4);
122
+ fs.writeFileSync(wavPath, out);
123
+ return;
124
+ }
125
+ offset += 8 + size + (size % 2);
126
+ }
127
+ throw new Error(`no PCM data chunk in ${wavPath}`);
128
+ }
129
+
101
130
  // One whisper-ready wav per attachment: a cached conversion is reused while its
102
131
  // file still exists, and parallel callers for the same key share a single
103
132
  // ffmpeg spawn instead of racing two conversions onto the same output path.
@@ -126,9 +155,6 @@ function createVoiceWavCache() {
126
155
  const _ffmpegPromise = runCmd(ffmpegPath, [
127
156
  '-i',
128
157
  audioPath,
129
- // Leading silence so a clipped onset still decodes as the first word.
130
- '-af',
131
- 'adelay=300:all=1',
132
158
  '-ar',
133
159
  String(sampleRate),
134
160
  '-ac',
@@ -137,7 +163,7 @@ function createVoiceWavCache() {
137
163
  String(threadCount),
138
164
  '-y',
139
165
  wavPath,
140
- ]);
166
+ ]).then(() => prependWavSilence(wavPath));
141
167
  ffmpegInflight.set(_ffmpegKey, _ffmpegPromise);
142
168
  try {
143
169
  await _ffmpegPromise;
@@ -118,11 +118,11 @@ const captureProperties = {
118
118
  include_ocr: {
119
119
  type: 'boolean',
120
120
  description:
121
- 'State/SOM automatically use offline Windows OCR when semantic targets are absent; true always runs OCR even when semantic targets exist. OCR shares max_elements. Zoom recognizes the crop itself, which is how enlarged text becomes readable.',
121
+ 'State/SOM automatically use offline OS OCR when semantic targets are absent; true always runs OCR even when semantic targets exist. OCR shares max_elements. Zoom recognizes the crop itself, which is how enlarged text becomes readable.',
122
122
  },
123
123
  ocr_language: {
124
124
  ...ocrLanguage,
125
- description: 'Installed Windows OCR language tag, e.g. ko or en-US.',
125
+ description: 'Installed OS OCR language tag, e.g. ko or en-US.',
126
126
  },
127
127
  // Image encoding (JPEG quality, downscale width) and the OCR word cap are
128
128
  // host defaults: the element budget already bounds OCR, and a frame the
@@ -213,7 +213,7 @@ export const COMPUTER_INPUT_SCHEMA = {
213
213
  ...windowTarget,
214
214
  ocr_language: {
215
215
  ...ocrLanguage,
216
- description: 'Optional Windows OCR language tag to verify, e.g. ko or en-US.',
216
+ description: 'Optional OS OCR language tag to verify, e.g. ko or en-US.',
217
217
  },
218
218
  }),
219
219
  false
@@ -31,6 +31,10 @@ action('ocr_image', nativeRead);
31
31
  action('ocr_status', nativeRead);
32
32
  action('release_session', { retainNativeRefs: true });
33
33
  action('release_cursor_theme', { retainNativeRefs: true });
34
+ // Handing held input, focus and pointer back changes no target's content, so a
35
+ // turn's end keeps the refs of its last observation for a follow-up turn.
36
+ action('release_held_input', { retainNativeRefs: true });
37
+ action('restore_input_state', { retainNativeRefs: true });
34
38
  action('list_apps', { ...hostRead, ...replay, policy: 'list' });
35
39
  action('list_installed_apps', { ...nativeRead, ...replay, policy: 'list' });
36
40
  action('list_history', { ...hostRead, ...replay, policy: 'list' });
@@ -0,0 +1,22 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ // Creates a temp directory and points the env var `name` at it until `cleanup()`.
6
+ export async function makeTempEnvDir(name, prefix) {
7
+ const directory = await mkdtemp(join(tmpdir(), prefix));
8
+ const previous = process.env[name];
9
+ process.env[name] = directory;
10
+ return {
11
+ directory,
12
+ async cleanup() {
13
+ if (previous === undefined) delete process.env[name];
14
+ else process.env[name] = previous;
15
+ await rm(directory, { recursive: true, force: true });
16
+ },
17
+ };
18
+ }
19
+
20
+ export function writeBridgeDiscovery(directory, fields) {
21
+ return writeFile(join(directory, 'computer-bridge.json'), JSON.stringify({ version: 1, ...fields }));
22
+ }