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
@@ -10,7 +10,14 @@ import { homedir } from 'node:os';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
- const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
13
+ // Resolved on first use, not at load: the desktop service bundle (CJS) carries
14
+ // this module through the model catalog, and import.meta is empty there, so
15
+ // computing it at load crashed the whole bundle before any caller needed it.
16
+ let defaultRoot = null;
17
+ function packagedRoot() {
18
+ defaultRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..', '..');
19
+ return defaultRoot;
20
+ }
14
21
 
15
22
  // The defaults are joins over values that almost never change; they are hot
16
23
  // (every session path resolves the data dir), so each default join is reused
@@ -32,7 +39,7 @@ export function mixdogHome() {
32
39
  }
33
40
 
34
41
  export function mixdogRoot() {
35
- return process.env.MIXDOG_ROOT || DEFAULT_ROOT;
42
+ return process.env.MIXDOG_ROOT || packagedRoot();
36
43
  }
37
44
 
38
45
  export function resolvePluginData() {
@@ -0,0 +1,25 @@
1
+ // The parent's flags a worker thread may inherit. A worker rejects any flag that
2
+ // governs the process itself (heap sizes, inspector, TLS, test-runner settings)
3
+ // with ERR_WORKER_INVALID_EXEC_ARGV, and Node 24's test runner hands a child its
4
+ // whole option table in execArgv (--use-system-ca, --tls-cipher-list, ...), so a
5
+ // list of known-bad flags kept failing. Only what loads and resolves modules,
6
+ // and what governs warnings, carries over.
7
+ const INHERITED_FLAG =
8
+ /^--(?:import|require|loader|experimental-loader|conditions|enable-source-maps|preserve-symlinks(?:-main)?|no-warnings|disable-warning|trace-warnings|no-deprecation|(?:no-)?experimental-[a-z0-9-]+)(?:=|$)/;
9
+ const SHORT_WITH_VALUE = new Set(['-r', '-C']);
10
+
11
+ export function workerExecArgv(execArgv = process.execArgv) {
12
+ const source = Array.isArray(execArgv) ? execArgv.map(String) : [];
13
+ const kept = [];
14
+ for (let index = 0; index < source.length; index += 1) {
15
+ const argument = source[index];
16
+ const short = SHORT_WITH_VALUE.has(argument);
17
+ if (!short && !INHERITED_FLAG.test(argument)) continue;
18
+ kept.push(argument);
19
+ // A flag written apart from its value (`--import tsx`) keeps the value too.
20
+ const takesValue =
21
+ short || /^--(?:import|require|loader|experimental-loader|conditions|disable-warning)$/.test(argument);
22
+ if (takesValue && index + 1 < source.length) kept.push(source[++index]);
23
+ }
24
+ return kept;
25
+ }
@@ -2,6 +2,16 @@
2
2
  * Response formatter — strips metadata, returns human-readable text.
3
3
  */
4
4
 
5
+ // Whether `text` names exactly this URL: an occurrence that runs on into a
6
+ // longer URL (`…/docs` inside `…/docs/guide`) is a different page.
7
+ function textCitesUrl(text, url) {
8
+ for (let at = text.indexOf(url); at !== -1; at = text.indexOf(url, at + 1)) {
9
+ const rest = text.slice(at + url.length, at + url.length + 2);
10
+ if (!/^(?:[A-Za-z0-9/?#&=%_~-]|\.[A-Za-z0-9])/.test(rest)) return true;
11
+ }
12
+ return false;
13
+ }
14
+
5
15
  function formatWebSearchResults(data) {
6
16
  // data may be the full jsonText payload: { tool, providers, response, cache, ... }
7
17
  const response = data.response || data;
@@ -20,25 +30,40 @@ function formatWebSearchResults(data) {
20
30
  const SNIPPET_CAP = 600;
21
31
  const ANSWER_CAP = 4000;
22
32
  const clip = (text, cap) => (text.length > cap ? `${text.slice(0, cap)}…` : text);
23
- if (answer) blocks.push(clip(answer, ANSWER_CAP));
33
+ const shownAnswer = answer ? clip(answer, ANSWER_CAP) : '';
34
+ if (shownAnswer) blocks.push(shownAnswer);
24
35
  if (!results.length) return blocks.join('\n\n');
25
- blocks.push(
26
- results
27
- .map((r, i) => {
28
- const num = i + 1;
29
- const title = clip(r.title || '(no title)', TITLE_CAP);
30
- const url = r.url || '';
31
- const date = r.publishedDate || '';
32
- const snippet = clip((r.snippet || '').trim(), SNIPPET_CAP);
33
-
34
- const urlPart = [url, date].filter(Boolean).join(' — ');
35
- const lines = [`${num}. ${title}`];
36
- if (urlPart) lines.push(` ${urlPart}`);
37
- if (snippet && snippet !== title && snippet !== url) lines.push(` ${snippet}`);
38
- return lines.join('\n');
39
- })
40
- .join('\n\n')
41
- );
36
+ // A native search route reports the pages a query reached without their
37
+ // titles, and the adapters label each one with that query. A label shared by
38
+ // several URLs names the search, not a page, so such a source prints as its
39
+ // URL alone — and not at all when the answer above already cites that URL.
40
+ // Measured on stored results: every source had this shape and 66% of their
41
+ // URLs were already in the answer.
42
+ const urlsByTitle = new Map();
43
+ for (const r of results) {
44
+ if (!r.title || !r.url) continue;
45
+ if (!urlsByTitle.has(r.title)) urlsByTitle.set(r.title, new Set());
46
+ urlsByTitle.get(r.title).add(r.url);
47
+ }
48
+ const entries = [];
49
+ for (const r of results) {
50
+ const url = r.url || '';
51
+ const date = r.publishedDate || '';
52
+ const snippet = clip((r.snippet || '').trim(), SNIPPET_CAP);
53
+ const urlPart = [url, date].filter(Boolean).join(' — ');
54
+ const queryLabelled =
55
+ !r.title || r.title === url || r.source === 'web_search_call' || urlsByTitle.get(r.title)?.size > 1;
56
+ if (url && !snippet && queryLabelled) {
57
+ if (!textCitesUrl(shownAnswer, url)) entries.push(`${entries.length + 1}. ${urlPart}`);
58
+ continue;
59
+ }
60
+ const title = clip(r.title || '(no title)', TITLE_CAP);
61
+ const lines = [`${entries.length + 1}. ${title}`];
62
+ if (urlPart) lines.push(` ${urlPart}`);
63
+ if (snippet && snippet !== title && snippet !== url) lines.push(` ${snippet}`);
64
+ entries.push(lines.join('\n'));
65
+ }
66
+ if (entries.length) blocks.push(entries.join('\n\n'));
42
67
  return blocks.join('\n\n');
43
68
  }
44
69
 
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Agent delegation for automation sessions (schedules, webhooks). They run
3
+ * headless in the daemon, so no session runtime registers tools for them or
4
+ * receives their agents' completions. The daemon's canonical agent control is
5
+ * exposed under a dedicated internal-tool scope instead, and the completions of
6
+ * the agents a run starts come back here, where the run turns them into the
7
+ * conversation's next turn — what a Lead in a window does when one arrives.
8
+ */
9
+ import { setInternalToolsProvider } from '../runtime/agent/orchestrator/internal-tools.mjs';
10
+ import { AGENT_TOOL } from './services/agent-tool/tool-def.mjs';
11
+
12
+ export const AUTOMATION_TOOL_SCOPE = 'automation';
13
+ // How long a run waits for the next result of an agent it started; past it the
14
+ // run ends with the conversation as it stands.
15
+ const AGENT_RESULT_WAIT_MS = 60 * 60_000;
16
+
17
+ const runs = new Map(); // sessionId → { pending: Set<taskId>, ready: string[], wake }
18
+
19
+ function resultText(result) {
20
+ return typeof result === 'string' ? result : JSON.stringify(result ?? '');
21
+ }
22
+
23
+ /** Registers the agent tool for automation sessions; returns its disposer. */
24
+ export function registerAutomationAgentTool(executeAgentControl) {
25
+ return setInternalToolsProvider({
26
+ scopeId: AUTOMATION_TOOL_SCOPE,
27
+ tools: [AGENT_TOOL],
28
+ async executor(_name, args, callerCtx = {}) {
29
+ const result = await executeAgentControl(args, {
30
+ callerCwd: callerCtx.callerCwd,
31
+ invocationSource: 'model-tool',
32
+ callerSessionId: callerCtx.callerSessionId || null,
33
+ clientHostPid: process.pid,
34
+ signal: callerCtx.signal,
35
+ });
36
+ // A spawn/send acknowledgement ("agent task: <id>", "status: running")
37
+ // is a result still to come for this run.
38
+ const text = resultText(result);
39
+ const taskId = /agent task: ([\w.:-]+)/.exec(text)?.[1];
40
+ const run = runs.get(String(callerCtx.callerSessionId || ''));
41
+ if (run && taskId && /status: running/.test(text)) run.pending.add(taskId);
42
+ return result;
43
+ },
44
+ });
45
+ }
46
+
47
+ /** Hands an agent completion to the automation run that started the task; false when none did. */
48
+ export function deliverAutomationCompletion(sessionId, text, meta = {}) {
49
+ const run = runs.get(String(sessionId || ''));
50
+ const taskId = String(meta?.execution_id || '');
51
+ if (!run?.pending.has(taskId)) return false;
52
+ // The header-only preview precedes the body-carrying completion.
53
+ if (meta?.model_visible === false) return true;
54
+ run.pending.delete(taskId);
55
+ run.ready.push(String(text || ''));
56
+ run.wake?.();
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Runs an automation conversation to its end: the first turn, then one more
62
+ * turn per batch of agent results while agents it started are still out.
63
+ * Resolves with the last turn's result.
64
+ */
65
+ export async function runAutomationTurns(sessionId, firstTurn, nextTurn, { waitMs = AGENT_RESULT_WAIT_MS } = {}) {
66
+ const run = { pending: new Set(), ready: [], wake: null };
67
+ runs.set(sessionId, run);
68
+ try {
69
+ let result = await firstTurn();
70
+ while (run.pending.size > 0 || run.ready.length > 0) {
71
+ if (run.ready.length === 0) {
72
+ const arrived = await new Promise((resolve) => {
73
+ const timer = setTimeout(() => resolve(false), waitMs);
74
+ run.wake = () => {
75
+ clearTimeout(timer);
76
+ resolve(true);
77
+ };
78
+ });
79
+ run.wake = null;
80
+ if (!arrived) break;
81
+ }
82
+ result = await nextTurn(run.ready.splice(0).join('\n\n'));
83
+ }
84
+ return result;
85
+ } finally {
86
+ runs.delete(sessionId);
87
+ }
88
+ }
@@ -10,6 +10,9 @@ import { createWorkflowHelpers } from '../runtime/agent/orchestrator/runtime-cor
10
10
  import { STANDALONE_ROOT, STANDALONE_DATA_DIR } from './runtime-paths.mjs';
11
11
  import { readMarkdownDocument, normalizeAgentPermissionOrNone } from '../runtime/shared/markdown-frontmatter.mjs';
12
12
  import { IMPLICIT_APPROVAL_MODE } from '../runtime/agent/orchestrator/session/approval-mode.mjs';
13
+ import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
14
+ import { configuredOrchestrationMode } from '../runtime/shared/orchestration.mjs';
15
+ import { AUTOMATION_TOOL_SCOPE } from './automation-agents.mjs';
13
16
 
14
17
  let _helpers = null;
15
18
  function helpers() {
@@ -26,21 +29,31 @@ function helpers() {
26
29
 
27
30
  /** Non-interactive createSession opts, plus workflow context when the id resolves. */
28
31
  export function automationWorkflowOpts(workflowId) {
29
- const approvalOpts = { approvalMode: IMPLICIT_APPROVAL_MODE };
32
+ const config = loadConfig({ secrets: false });
33
+ // The user's orchestration setting governs an automation exactly as it does a
34
+ // New task: a workflow id alone never turns delegation on, and the agent
35
+ // tool reaches the run through the automation tool scope.
36
+ const baseOpts = {
37
+ approvalMode: IMPLICIT_APPROVAL_MODE,
38
+ orchestrationMode: configuredOrchestrationMode(config),
39
+ mcpScopeId: AUTOMATION_TOOL_SCOPE,
40
+ };
30
41
  const id = String(workflowId || '').trim();
31
- if (!id) return approvalOpts;
42
+ if (!id) return baseOpts;
32
43
  try {
33
44
  const h = helpers();
34
45
  const pack = h.loadWorkflowPack(undefined, id);
35
- if (!pack) return approvalOpts;
36
- const resolved = h.activeWorkflowContext({ workflow: { active: id } }, undefined);
46
+ if (!pack) return baseOpts;
47
+ // The whole config, so disabled agents and the orchestration mode are the
48
+ // user's, with only the active workflow swapped for the automation's own.
49
+ const resolved = h.activeWorkflowContext({ ...config, workflow: { ...config.workflow, active: id } }, undefined);
37
50
  return {
38
- ...approvalOpts,
51
+ ...baseOpts,
39
52
  workflow: resolved.summary,
40
53
  workflowContext: resolved.context,
41
54
  orchestrationMode: resolved.orchestrationMode,
42
55
  };
43
56
  } catch {
44
- return approvalOpts;
57
+ return baseOpts;
45
58
  }
46
59
  }
@@ -107,7 +107,6 @@ function wireToolPolicyRefresh(boot) {
107
107
  getDataDir: () => dataDirOf(cfgMod),
108
108
  modelStandaloneTools: boot.modelStandaloneTools,
109
109
  featureDisallowedTools: boot.featureDisallowedTools,
110
- memoryToolsEnabled: boot.memoryToolsEnabledFn,
111
110
  loadCoreMemoryContext: boot.loadCoreMemoryContext,
112
111
  activeWorkflowContext: workflowHelpers.activeWorkflowContext,
113
112
  invalidatePreSessionToolSurface: boot.invalidatePreSessionToolSurface,
@@ -274,6 +273,12 @@ function providerAuthApiFor(boot) {
274
273
  getUsageDashboard: boot.getUsageDashboard,
275
274
  consumeCodexRateLimitResetCredit: boot.consumeCodexRateLimitResetCredit,
276
275
  collectProviderModels: boot.collectProviderModels,
276
+ // A deleted custom provider must not stay reachable through the live route
277
+ // or the registry; stored routes naming it are filtered by the picker.
278
+ onCustomProviderRemoved: (id) => {
279
+ reg.disableProvider?.(id);
280
+ if (rt.route?.provider === id) rt.route = { ...rt.route, provider: '', model: '', preset: null };
281
+ },
277
282
  });
278
283
  }
279
284
 
@@ -1,9 +1,14 @@
1
1
  // cwd-plugins/core-memory-context.mjs — the user-curated core-memory block
2
2
  // injected into new sessions, read from the memory runtime's atomic snapshot.
3
- import { featureEnvOverride, memoryToolsEnabled } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
3
+ import { featureEnvOverride } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
4
4
  import { readSessionCoreMemoryPayload } from '../../runtime/memory/lib/core-memory-file.mjs';
5
5
 
6
- export function createCoreMemoryContext({ getCurrentCwd, getConfig, bootProfile, clean, cfgMod, STANDALONE_DATA_DIR }) {
6
+ // Budget for the whole injected block in UTF-8 bytes: bytes track token cost
7
+ // more evenly across scripts than characters do.
8
+ const CORE_MEMORY_MAX_BYTES = 32 * 1024;
9
+ const TRUNCATED_MARK = '\n- ...';
10
+
11
+ export function createCoreMemoryContext({ getCurrentCwd, bootProfile, clean, cfgMod, STANDALONE_DATA_DIR }) {
7
12
  function formatCoreMemoryLines(payload = {}) {
8
13
  const seen = new Set();
9
14
  const lines = [];
@@ -14,21 +19,18 @@ export function createCoreMemoryContext({ getCurrentCwd, getConfig, bootProfile,
14
19
  if (seen.has(key)) continue;
15
20
  seen.add(key);
16
21
  lines.push(`- ${text}`);
17
- if (lines.length >= 40) break;
18
22
  }
19
23
  const out = lines.join('\n');
20
- const maxChars = 6000;
21
- return out.length > maxChars ? `${out.slice(0, maxChars).replace(/\s+\S*$/, '')}\n- ...` : out;
24
+ if (Buffer.byteLength(out, 'utf8') <= CORE_MEMORY_MAX_BYTES) return out;
25
+ const budget = CORE_MEMORY_MAX_BYTES - Buffer.byteLength(TRUNCATED_MARK, 'utf8');
26
+ // A byte cut can split a multi-byte character; drop its replacement chars.
27
+ const head = Buffer.from(out, 'utf8').subarray(0, budget).toString('utf8').replace(/\uFFFD+$/, '');
28
+ return `${head.replace(/\s+\S*$/, '')}${TRUNCATED_MARK}`;
22
29
  }
23
30
 
24
31
  async function loadCoreMemoryContext() {
25
- // User-curated core memory injects into new sessions by default. The
26
- // Memory toggle (settings → General) OFF skips ONLY this accumulated
27
- // core-memory block — the profile injects regardless.
28
- if (!(featureEnvOverride('MIXDOG_FEATURE_MEMORY') ?? memoryToolsEnabled(getConfig(), true))) {
29
- bootProfile('core-memory:disabled');
30
- return '';
31
- }
32
+ // User-curated core memory always injects into new sessions: the Memory
33
+ // install marker and toggle gate only the model-facing memory/recall tools.
32
34
  // Explicit opt-out (MIXDOG_BOOT_CORE_MEMORY=0/false/no/off) skips this
33
35
  // file-backed prompt block. Recall and memory tools remain available.
34
36
  if (featureEnvOverride('MIXDOG_BOOT_CORE_MEMORY') === false) {
@@ -1,41 +1,22 @@
1
- // Browser Use and Computer Use: the environment kill switch, the once-per-
2
- // session first-use approval, then the bridge client.
1
+ // Browser Use and Computer Use: the environment kill switch, then the bridge
2
+ // client. Neither asks the user before a call.
3
3
  import { executeBrowserTool } from '../../runtime/browser-bridge/client.mjs';
4
4
  import { executeComputerTool } from '../../runtime/computer-bridge/client.mjs';
5
- import { createBridgeFirstUseGate } from '../bridge-first-use-gate.mjs';
6
5
  import { featureEnvOverride } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
7
6
  import { createCallerContextResolvers } from './caller-context.mjs';
8
7
 
9
8
  export function createBridgeToolHandlers({ rt }) {
10
- const bridgeFirstUseGate = createBridgeFirstUseGate({ getConfig: () => rt.config });
11
9
  const { sessionIdFor, signalFor } = createCallerContextResolvers(rt);
12
10
 
13
- // Browser Use and Computer Use ask the user once per session before their
14
- // first live call; the answer is the tool result when it is no.
15
- const firstUseDenial = async (name, args, callerCtx, callerCwd) => {
16
- const denial = await bridgeFirstUseGate({
17
- name,
18
- args,
19
- cwd: callerCwd,
20
- sessionId: sessionIdFor(callerCtx),
21
- toolCallId: callerCtx?.toolCallId || null,
22
- toolApprovalHook: callerCtx?.toolApprovalHook,
23
- invocationSource: callerCtx?.invocationSource,
24
- });
25
- return denial ? { content: [{ type: 'text', text: denial }], isError: true } : null;
26
- };
27
-
28
11
  const environmentDisabled = (callerCtx, feature) =>
29
12
  callerCtx?.invocationSource === 'model-tool' && featureEnvOverride(feature) === false;
30
13
 
31
14
  // `browser` and `browser_devtools` are one bridge; the tool name only
32
15
  // scopes which actions the validator admits.
33
- const browser = async (args, { name, callerCtx, callerCwd }) => {
16
+ const browser = async (args, { name, callerCtx }) => {
34
17
  if (environmentDisabled(callerCtx, 'MIXDOG_FEATURE_BROWSER')) {
35
18
  throw new Error('the browser tool is disabled in this environment');
36
19
  }
37
- const denied = await firstUseDenial(name, args, callerCtx, callerCwd);
38
- if (denied) return denied;
39
20
  return await executeBrowserTool(args, {
40
21
  tool: name,
41
22
  sessionId: sessionIdFor(callerCtx),
@@ -44,12 +25,10 @@ export function createBridgeToolHandlers({ rt }) {
44
25
  });
45
26
  };
46
27
 
47
- const computer = async (args, { name, callerCtx, callerCwd }) => {
28
+ const computer = async (args, { callerCtx, callerCwd }) => {
48
29
  if (environmentDisabled(callerCtx, 'MIXDOG_FEATURE_COMPUTER')) {
49
30
  throw new Error('the computer tool is disabled in this environment');
50
31
  }
51
- const denied = await firstUseDenial(name, args, callerCtx, callerCwd);
52
- if (denied) return denied;
53
32
  return await executeComputerTool(args, {
54
33
  sessionId: sessionIdFor(callerCtx),
55
34
  cwd: callerCwd,
@@ -2,6 +2,7 @@
2
2
  // construction. Pure except for the
3
3
  // injected route provider (for sort priority) and webSearchCapableFor predicate.
4
4
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
5
+ import { contextWindowRange } from '../runtime/shared/llm/default-context-window.mjs';
5
6
 
6
7
  function parsedProviderModelVersion(id) {
7
8
  const text = clean(id).toLowerCase();
@@ -106,14 +107,21 @@ export function isSelectableLlmModel(model) {
106
107
  }
107
108
 
108
109
  export function providerModelCacheRow(name, m, webSearchCapableFor) {
110
+ // The pickers read the default window and the slider's range straight off
111
+ // this row, so a default lowered by range has to be stated here.
112
+ const range = contextWindowRange({
113
+ provider: name,
114
+ contextWindow: m.contextWindow,
115
+ maxContextWindow: m.maxContextWindow,
116
+ });
109
117
  return {
110
118
  id: m.id,
111
119
  provider: name,
112
120
  display: m.display || m.name || m.id,
113
121
  created: typeof m.created === 'number' ? m.created : null,
114
122
  releaseDate: m.releaseDate || null,
115
- contextWindow: m.contextWindow,
116
- maxContextWindow: m.maxContextWindow || null,
123
+ contextWindow: range.defaultWindow || m.contextWindow,
124
+ maxContextWindow: range.maxWindow > range.defaultWindow ? range.maxWindow : m.maxContextWindow || null,
117
125
  outputTokens: m.outputTokens || null,
118
126
  family: m.family || null,
119
127
  tier: m.tier || null,
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
14
14
  import { getModelMetadataSync } from '../../runtime/agent/orchestrator/providers/model-catalog.mjs';
15
15
  import { workflowPresetId } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
16
+ import { resolveRouteContextState } from '../route-state.mjs';
16
17
  import { writeStatuslineRoute } from '../statusline-route.mjs';
17
18
  import { sessionHasRouteHistory, shouldRecreateEmptySessionForRouteChange } from '../../runtime/agent/orchestrator/runtime-core/session-route-policy.mjs';
18
19
  import { rebuildDeferredToolSurfaceForProvider } from '../../runtime/agent/orchestrator/runtime-core/tool-catalog.mjs';
@@ -52,7 +53,12 @@ async function resolveSelectedRoute(deps, next) {
52
53
  selectedRoute.modelParameters
53
54
  );
54
55
  return {
55
- selectedRoute: { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false },
56
+ selectedRoute: {
57
+ ...selectedRoute,
58
+ fast: fastCapable ? selectedRoute.fast === true : false,
59
+ // Lets the saved model settings tell a chosen window from the default.
60
+ contextDefaultPercent: resolveRouteContextState(selectedRoute, modelMeta).contextDefaultPercent,
61
+ },
56
62
  fastCapable,
57
63
  modelMeta,
58
64
  };
@@ -8,9 +8,13 @@ import {
8
8
  saveOpenAIUsageSessionKey,
9
9
  saveOpenCodeGoUsageAuth,
10
10
  saveProviderApiKey,
11
+ saveCustomProvider,
12
+ removeCustomProvider,
13
+ testCustomProvider,
14
+ discoverCustomProviderModels,
11
15
  } from '../services/provider-admin.mjs';
12
16
 
13
- export function createLoginApi({ cfgMod, awaitKeychainPrewarm, reloadFullConfig }, refresh) {
17
+ export function createLoginApi({ cfgMod, awaitKeychainPrewarm, reloadFullConfig, onCustomProviderRemoved }, refresh) {
14
18
  async function completeOAuthLogin(login) {
15
19
  return {
16
20
  ...login,
@@ -69,6 +73,27 @@ export function createLoginApi({ cfgMod, awaitKeychainPrewarm, reloadFullConfig
69
73
  refresh.afterUsageAuthChange();
70
74
  return result;
71
75
  },
76
+ async saveCustomProvider(input) {
77
+ await awaitKeychainPrewarm();
78
+ const result = await saveCustomProvider(cfgMod, input);
79
+ refresh.afterCredentialChange();
80
+ return result;
81
+ },
82
+ async removeCustomProvider(providerId) {
83
+ await awaitKeychainPrewarm();
84
+ const result = removeCustomProvider(cfgMod, providerId);
85
+ onCustomProviderRemoved?.(result.provider);
86
+ refresh.afterCredentialChange();
87
+ return result;
88
+ },
89
+ async testCustomProvider(input) {
90
+ await awaitKeychainPrewarm();
91
+ return await testCustomProvider(cfgMod, input);
92
+ },
93
+ async discoverCustomProviderModels(input) {
94
+ await awaitKeychainPrewarm();
95
+ return await discoverCustomProviderModels(cfgMod, input);
96
+ },
72
97
  forgetProviderAuth(providerId, accountId) {
73
98
  const result = forgetProviderAuth(cfgMod, providerId, accountId);
74
99
  refresh.afterCredentialChange();
@@ -46,7 +46,7 @@ export function createQuickProviderRows({
46
46
  const addRoute = (routeLike = {}) => {
47
47
  const provider = clean(routeLike.provider);
48
48
  const model = clean(routeLike.model);
49
- if (!provider || !model) return;
49
+ if (!provider || !model || isDeletedCustom(provider)) return;
50
50
  const meta = metadataFor(provider, model);
51
51
  addModel(provider, {
52
52
  id: model,
@@ -64,8 +64,23 @@ export function createQuickProviderRows({
64
64
  });
65
65
  };
66
66
 
67
- for (const [provider, providerConfig] of Object.entries(pickerConfig.providers || {})) {
67
+ // A custom-* id with no live custom config entry was deleted: stale
68
+ // routes must not bring it back into the picker.
69
+ const customProviders = pickerConfig.providers || {};
70
+ const isDeletedCustom = (provider) =>
71
+ provider.startsWith('custom-') &&
72
+ !(customProviders[provider]?.type === 'custom' && customProviders[provider].enabled !== false);
73
+ for (const [provider, providerConfig] of Object.entries(customProviders)) {
68
74
  if (!providerConfig?.enabled) continue;
75
+ if (providerConfig.type === 'custom') {
76
+ // Configured models are listed at once, before any catalog refresh.
77
+ for (const model of providerConfig.models || []) {
78
+ addModel(provider, {
79
+ ...model,
80
+ outputTokens: model.maxOutputTokens || null,
81
+ });
82
+ }
83
+ }
69
84
  for (const model of providerCachedModelsSync(provider)) addModel(provider, model);
70
85
  }
71
86
  addRoute(route);
@@ -8,7 +8,7 @@ import { createExtensionSync } from './resource-extension-sync.mjs';
8
8
  import { createScopeDecorators } from './resource-scope-decorate.mjs';
9
9
  import { createMcpResourceApi } from './resource-mcp-api.mjs';
10
10
  import { createSkillsResourceApi } from './resource-skills-api.mjs';
11
- import { createPluginsResourceApi } from './resource-plugins-api.mjs';
11
+ import { createPluginsResourceApi, findRegisteredPlugin } from './resource-plugins-api.mjs';
12
12
  import { createRecallResourceApi } from './resource-recall-api.mjs';
13
13
 
14
14
  export function createResourceApi(deps) {
@@ -27,8 +27,14 @@ export function createResourceApi(deps) {
27
27
  if (!EXTENSION_SCOPE_KINDS.includes(scopeKind)) {
28
28
  throw new Error(`extension scope kind must be one of ${EXTENSION_SCOPE_KINDS.join(', ')}`);
29
29
  }
30
- const key = clean(name);
30
+ let key = clean(name);
31
31
  if (!key) throw new Error('extension name is required');
32
+ // Plugin scopes are keyed by registry id; a name must resolve to it.
33
+ if (scopeKind === 'plugins') {
34
+ const plugin = findRegisteredPlugin(pluginsStatus, key);
35
+ if (!plugin) throw new Error(`plugin not registered: ${key}`);
36
+ key = clean(plugin.id || plugin.name);
37
+ }
32
38
  let list = [projects];
33
39
  if (Array.isArray(projects)) list = projects;
34
40
  else if (projects == null) list = [];
@@ -10,6 +10,15 @@ import { pluginMcpServerName, pluginServerMatcher } from '../runtime/agent/orche
10
10
 
11
11
  const pluginKey = (plugin) => clean(plugin.id || plugin.name || plugin);
12
12
 
13
+ /** The registered status row a caller means by `plugin` (row, id, name, or
14
+ * title), or null. Callers such as the setup tool pass only a name, while
15
+ * MCP entry names and scope keys come from the registered record. */
16
+ export function findRegisteredPlugin(pluginsStatus, plugin) {
17
+ const key = pluginKey(plugin);
18
+ if (!key) return null;
19
+ return (pluginsStatus()?.plugins || []).find((row) => [row.id, row.name, row.title].map(clean).includes(key)) || null;
20
+ }
21
+
13
22
  export function createPluginsResourceApi({ deps, sync, decorate }) {
14
23
  const {
15
24
  getConfig,
@@ -52,8 +61,9 @@ export function createPluginsResourceApi({ deps, sync, decorate }) {
52
61
  return { plugin: updated, status: pluginsStatus() };
53
62
  },
54
63
  async setPluginEnabled(plugin = {}, enabled = true) {
64
+ const owner = findRegisteredPlugin(pluginsStatus, plugin) || plugin;
55
65
  const updated = registrySetPluginEnabled(pluginKey(plugin), enabled, { dataDir: pluginData() });
56
- await rewritePluginServers(plugin, (servers, owned) => {
66
+ await rewritePluginServers(owner, (servers, owned) => {
57
67
  const next = {};
58
68
  for (const [name, value] of Object.entries(servers)) {
59
69
  const config = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : value;
@@ -69,8 +79,10 @@ export function createPluginsResourceApi({ deps, sync, decorate }) {
69
79
  return { plugin: updated, status: pluginsStatus() };
70
80
  },
71
81
  async removePlugin(plugin = {}) {
82
+ // Resolve before the registry forgets the plugin and its manifest name.
83
+ const owner = findRegisteredPlugin(pluginsStatus, plugin) || plugin;
72
84
  const removed = registryRemovePlugin(pluginKey(plugin), { dataDir: pluginData() });
73
- await rewritePluginServers(plugin, (servers, owned) => {
85
+ await rewritePluginServers(owner, (servers, owned) => {
74
86
  const next = { ...servers };
75
87
  for (const name of Object.keys(next)) {
76
88
  if (owned(name)) delete next[name];
@@ -5,6 +5,7 @@ import { clean, hasOwn } from '../runtime/agent/orchestrator/runtime-core/sessio
5
5
  import { coerceEffortFor } from '../runtime/agent/orchestrator/runtime-core/effort.mjs';
6
6
  import { fastCapableFor } from '../runtime/agent/orchestrator/runtime-core/model-capabilities.mjs';
7
7
  import { providerCachedModelMetadataSync } from '../runtime/agent/orchestrator/providers/provider-catalog-cache.mjs';
8
+ import { contextWindowRange } from '../runtime/shared/llm/default-context-window.mjs';
8
9
 
9
10
  export function resolveRouteEffortState(targetRoute = {}, modelMeta = null) {
10
11
  const requested = hasOwn(targetRoute, 'effort') ? targetRoute.effort : targetRoute.preset?.effort || null;
@@ -49,8 +50,11 @@ export function resolveRouteContextState(targetRoute = {}, modelMeta = null, win
49
50
  Number(modelMeta?.contextWindow) > 0 || Number(modelMeta?.maxContextWindow) > 0
50
51
  ? modelMeta
51
52
  : windowLookup?.(clean(targetRoute?.provider), clean(targetRoute?.model)) || modelMeta;
52
- const defaultWindow = Math.max(0, Number(windowMeta?.contextWindow) || 0);
53
- const maxWindow = Math.max(defaultWindow, Number(windowMeta?.maxContextWindow) || 0);
53
+ const { defaultWindow, maxWindow } = contextWindowRange({
54
+ provider: clean(targetRoute?.provider),
55
+ contextWindow: windowMeta?.contextWindow,
56
+ maxContextWindow: windowMeta?.maxContextWindow,
57
+ });
54
58
  if (!maxWindow) {
55
59
  return { contextPercent: undefined, contextDefaultPercent: undefined, selectedContextWindow: undefined };
56
60
  }
@@ -11,6 +11,7 @@ import { createSession } from '../runtime/agent/orchestrator/session/manager/ses
11
11
  import { askSession } from '../runtime/agent/orchestrator/session/manager/ask-session.mjs';
12
12
  import { modelRouteFields, parseScheduleModelRef } from '../runtime/shared/schedule-model-ref.mjs';
13
13
  import { automationWorkflowOpts } from './automation-workflow.mjs';
14
+ import { runAutomationTurns } from './automation-agents.mjs';
14
15
  import { automationPromptContent } from '../runtime/shared/automation-attachments.mjs';
15
16
 
16
17
  /** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
@@ -58,6 +59,11 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
58
59
  // title/preview derive from it, so no "[Scheduled task: …]" header noise.
59
60
  // Stored attachments ride along as composer-style content parts.
60
61
  const content = automationPromptContent(prompt, schedule.attachments);
61
- const result = await askSession(session.id, content, null, null, cwd || undefined);
62
+ // Agents the run delegates to report back; each result is the next turn.
63
+ const result = await runAutomationTurns(
64
+ session.id,
65
+ () => askSession(session.id, content, null, null, cwd || undefined),
66
+ (completions) => askSession(session.id, completions, null, null, cwd || undefined)
67
+ );
62
68
  return { sessionId: session.id, result: String(result?.content || '') };
63
69
  }