mixdog 1.0.0 → 1.0.1

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/defaults/skills/computer-use/SKILL.md +13 -12
  3. package/src/defaults/skills/setup/SKILL.md +1 -4
  4. package/src/defaults/skills/setup/references/actions.md +0 -2
  5. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  6. package/src/rules/shared/70-delivery.md +3 -0
  7. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +5 -0
  8. package/src/runtime/agent/orchestrator/config.mjs +8 -1
  9. package/src/runtime/agent/orchestrator/providers/registry-errors.mjs +9 -0
  10. package/src/runtime/agent/orchestrator/providers/registry.mjs +3 -2
  11. package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +13 -5
  12. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +3 -2
  13. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +27 -4
  16. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +3 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  18. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +13 -5
  19. package/src/runtime/computer-bridge/error-recovery.mjs +1 -1
  20. package/src/runtime/shared/config-patch.mjs +2 -0
  21. package/src/runtime/shared/user-data-guard.mjs +1 -1
  22. package/src/session-runtime/config-lifecycle.mjs +11 -1
  23. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +1 -1
  24. package/src/session-runtime/native-web-search/tool-shape.mjs +16 -4
  25. package/src/session-runtime/provider-models/catalog-load.mjs +18 -3
  26. package/src/session-runtime/setup-tool/executor.mjs +0 -1
  27. package/src/session-runtime/setup-tool/settings-contract.mjs +2 -13
  28. package/src/tui/dist/index.mjs +1 -1
  29. package/src/workflows/default/WORKFLOW.md +0 -2
  30. package/src/workflows/headless/WORKFLOW.md +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -115,18 +115,19 @@ GNOME with the Window Calls extension); prefer accessibility refs there.
115
115
  of the shared desktop. If that surface is unavailable, use semantic refs or
116
116
  report `pixel_unavailable`; do not substitute a screen grab.
117
117
  - If the user intervenes, preserve their cursor and focus. Worker termination
118
- and input cleanup must finish before resuming; a failed cleanup is not cleared
119
- by a resume request. Ask the user to press `Ctrl+Alt+Esc`, or the overlay's
120
- Stop control, for verified cleanup recovery.
121
- If target-local release remains unconfirmed, the user must inspect/recover
122
- that window; restarting the host alone does not prove release. Obtain a new
123
- observation after an interruption.
124
- Use `wait_for_user` to keep the task waiting without sending input. The host
125
- may resume ordinary physical-input interruptions after its configured quiet
126
- interval; renewed input resets that interval. Explicit pauses/stops and
127
- uncertain observation/cleanup never auto-resume. The overlay's only control
128
- is Stop, which cancels the task. Never operate it or the recovery controls,
129
- or change the idle policy, on the user's behalf.
118
+ and input cleanup must finish before resuming. The user's Resume on the
119
+ overlay continues a pause; an ordinary physical-input pause may also resume
120
+ after the host's configured quiet interval (renewed input resets it), while
121
+ explicit pauses, stops and uncertain cleanup never auto-resume. Stop (or
122
+ `Ctrl+Alt+Esc`) cancels the task, which then reports the stop as a failure.
123
+ A cleanup that could not be confirmed is retried before the next input
124
+ command; if it still fails, that command returns the reason — relay it to
125
+ the user. If target-local release remains unconfirmed, the user must
126
+ inspect/recover that window; restarting the host alone does not prove
127
+ release. Obtain a new observation after an interruption.
128
+ Use `wait_for_user` to keep the task waiting without sending input. Never
129
+ operate Resume, Stop or the recovery controls, or change the idle policy, on
130
+ the user's behalf.
130
131
  While paused, only `list`, `diagnose` and `wait_for_user` are available.
131
132
  Chat text alone does not clear the host. After `resumed`, capture fresh state;
132
133
  after `timeout` or `cancelled`, no input is authorized. Never replay the
@@ -25,7 +25,7 @@ supported `setup` action, and read its result as the change receipt.
25
25
  arguments and the transcript. Open the Providers surface and let the user
26
26
  enter them.
27
27
  - A mutation follows the active workflow's approval rules. Status, catalog,
28
- definition, MCP-config and Instructions reads do not change persisted settings.
28
+ definition and MCP-config reads do not change persisted settings.
29
29
  - Most changes affect new sessions. Report `appliedToCurrentSession` and
30
30
  `appliesTo` exactly when the result supplies them.
31
31
  - This skill does not own source edits, builds, installation deployment,
@@ -78,9 +78,6 @@ that a call returned without transport error.
78
78
  - Read definitions before editing them; use skill-creator when authoring a
79
79
  skill. Setup persists the supported definition fields. Workflow and agent
80
80
  ids are stable identities, distinct from their display names.
81
- - Read Instructions before replacing them and pass the returned text as
82
- `expectedContent`. `projectPath:null` means Common Instructions; otherwise
83
- use an exact registered Project root. Keep the returned backup.
84
81
  - MCP servers and plugins are machine-global integrations whose visibility may
85
82
  be scoped to selected Projects.
86
83
  - User skills are machine-global. Enabled plugin skills and built-in skills are
@@ -68,8 +68,6 @@ Read this when choosing a `setup` mutation.
68
68
  | Desktop theme, display language, side panels, or zoom | `set_appearance` |
69
69
  | Register a Project and optionally set its display alias | `save_project` |
70
70
  | Unregister a Project without deleting its files | `remove_project` |
71
- | Read Project/Common Instructions before editing | `get_instructions` |
72
- | Replace unchanged Project/Common Instructions and retain a backup | `set_instructions` |
73
71
  | Revoke one linked device; re-pairing restores access | `revoke_linked_device` |
74
72
  | Managed local model context allocation; null restores automatic | `set_local_context` |
75
73
 
@@ -38,8 +38,7 @@ An attached Desktop or TUI may navigate and return `opened:true`.
38
38
  - Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
39
39
  - Settings → Connection: web-app pairing and linked devices.
40
40
  - Settings → System: update, keep-awake, and Doctor.
41
- - Projects: Project registration, name, Instructions, Common Instructions, and
42
- Core Memories.
41
+ - Projects: Project registration, name, Project Memories, and Common Memory.
43
42
  - Workflows: workflow packs, Main and Web Search defaults, and agent editing.
44
43
  - Extensions → Plugin: Built-in cards and plugins.
45
44
  - Extensions → Skill: skills and MCP servers.
@@ -51,7 +50,7 @@ the user to Desktop.
51
50
 
52
51
  Setup can read and change Desktop appearance, keep-awake, usage pin,
53
52
  Computer observe-only, Browser/Computer/voice installation and toggles,
54
- Projects, Instructions, and linked-device revocation through the attached
53
+ Projects, and linked-device revocation through the attached
55
54
  Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
56
55
  to the paired browser. No Desktop claimant means no confirmed change.
57
56
 
@@ -7,6 +7,9 @@
7
7
  absolute path, optionally `:line[:column]` (`deliverables/report.docx`,
8
8
  `src/app.ts:42`); never a bare name, a path relative to another folder, or a
9
9
  `file://` URI.
10
+ - Deliver requested visual artifacts (SVG, diagrams, HTML pages) as saved files
11
+ and give their path; never paste their source as the answer unless the user
12
+ asks for the code.
10
13
 
11
14
  <!-- tools: git -->
12
15
  - A commit request includes selecting and staging its changes; a stage-only
@@ -410,6 +410,11 @@ function classifyToolFailure(resultText, toolName) {
410
410
  if (/\[signal:\s*[^\]]+/i.test(leading)) return 'process/signal';
411
411
  return 'command-exit';
412
412
  }
413
+ // edit quotes the file around a miss (the nearest line and a current-file
414
+ // excerpt). That quoted text is file content, so a word in it such as
415
+ // "unreadable", "timeout" or "permission" must not pick the category.
416
+ if (toolName === 'edit' && /^edit failed \(old_string (?:not found|found \d+ times)\b/i.test(leading))
417
+ return 'runtime/failure';
413
418
  if (/compacted-history placeholder/.test(text)) return 'expected-preflight';
414
419
  if (/\[tool-input-validation\]/.test(text)) return 'schema/args';
415
420
  // These are emitted validation/cancellation contracts, not keywords found
@@ -100,7 +100,9 @@ export function buildDefaultConfig(options = {}) {
100
100
  providers['mixdog-local'] = { enabled: false };
101
101
  return {
102
102
  providers,
103
- disabledAgents: [...DEFAULT_DISABLED_AGENT_IDS],
103
+ // The Maintainer also starts off: background upkeep spends model calls, so
104
+ // it runs only once the user turns it on and picks its model.
105
+ disabledAgents: [...DEFAULT_DISABLED_AGENT_IDS, 'maintainer'],
104
106
  workflow: { active: 'default' },
105
107
  orchestrationMode: 'none',
106
108
  };
@@ -272,6 +274,11 @@ export function loadConfig(options = {}) {
272
274
  recap: { enabled: true },
273
275
  developer: {},
274
276
  modules: {},
277
+ // No stored section at all is a brand-new profile: its built-ins start
278
+ // uninstalled. The defaults above carry presets/recap/modules, which
279
+ // would otherwise read as a pre-section profile and be grandfathered.
280
+ // A stored section that failed to parse proves nothing and keeps that path.
281
+ ...(hasKeys(sectionRaw) ? {} : { builtins: {} }),
275
282
  };
276
283
  }
277
284
  /** In-lock patch of `skills.disabled` only (avoids whole-config lost-update). */
@@ -0,0 +1,9 @@
1
+ // Error code for a provider init that has no enabled provider to register.
2
+ // The registry refuses such an init to protect its live instances; readers
3
+ // that legitimately run before any provider exists (the model catalog on a
4
+ // fresh install) treat it as "nothing to load" instead of a failure.
5
+ export const NO_ENABLED_PROVIDERS = 'PROVIDERS_NONE_ENABLED';
6
+
7
+ export function noEnabledProvidersError(message) {
8
+ return Object.assign(new Error(message), { code: NO_ENABLED_PROVIDERS });
9
+ }
@@ -4,6 +4,7 @@ import { refreshCatalog as refreshMetadataCatalog } from './model-catalog.mjs';
4
4
  import { wrapProviderAdmission } from './admission-scheduler.mjs';
5
5
  import { createAccountPoolProvider } from './account-pool.mjs';
6
6
  import { isInclusiveProvider } from '../../../shared/llm/cost.mjs';
7
+ import { noEnabledProvidersError } from './registry-errors.mjs';
7
8
  // OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
8
9
  // OPENAI_COMPAT_PRESETS. No parallel list maintained here.
9
10
  const providers = new Map();
@@ -223,7 +224,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
223
224
  // here preserves whatever was already registered.
224
225
  const entries = Object.entries(config || {});
225
226
  if (entries.length === 0) {
226
- throw new Error('[provider] initProviders called with empty config — refusing to clear registry');
227
+ throw noEnabledProvidersError('[provider] initProviders called with empty config — refusing to clear registry');
227
228
  }
228
229
  const next = new Map();
229
230
  const nextSignatures = new Map();
@@ -258,7 +259,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
258
259
  // Second guard: every entry was disabled. Same reasoning — keep the
259
260
  // existing registry rather than going dark.
260
261
  if (next.size === 0) {
261
- throw new Error('[provider] all providers disabled in config — refusing to clear registry');
262
+ throw noEnabledProvidersError('[provider] all providers disabled in config — refusing to clear registry');
262
263
  }
263
264
  // OAuth preservation guard. anthropic-oauth / openai-oauth are NOT stored
264
265
  // in mixdog-config.json — buildDefaultConfig injects them at runtime by
@@ -6,11 +6,13 @@ import { clean } from '../../../shared/clean.mjs';
6
6
  // dropped on normalize rather than rejected.
7
7
  const TASK_ENTRY_FIELDS = ['id', 'text', 'status', 'kind'];
8
8
 
9
- // Dropped rows remain in the record but are no longer requested work.
9
+ // Dropped rows remain in the record but are no longer requested work. Every
10
+ // row, completed and dropped included, still fills one of the capped slots.
10
11
  export function goalTaskProgress(tasks) {
11
12
  return {
12
13
  tasksCompleted: tasks.filter((task) => task?.status === 'completed').length,
13
14
  tasksTotal: tasks.filter((task) => task?.status !== 'dropped').length,
15
+ taskSlotsUsed: tasks.length,
14
16
  };
15
17
  }
16
18
 
@@ -89,12 +91,18 @@ function taskInputRetains(entry, task) {
89
91
  }
90
92
 
91
93
  export function patchGoalTasks(previous, { updates, tasks } = {}) {
92
- const patches = updates ?? [];
93
- const additions = tasks ?? [];
94
- if (!Array.isArray(patches) || !Array.isArray(additions)) throw new Error('goal updates and tasks must be arrays');
94
+ const requested = updates ?? [];
95
+ const entries = tasks ?? [];
96
+ if (!Array.isArray(requested) || !Array.isArray(entries)) throw new Error('goal updates and tasks must be arrays');
97
+ const byId = new Map(previous.map((task) => [task.id, { ...task }]));
98
+ // A new-task entry naming an id this Goal already has can only mean that
99
+ // task, so it is applied as the task's update. An id the Goal does not have
100
+ // is still refused below.
101
+ const addressesExisting = (entry) => byId.has(clean(entry?.id));
102
+ const patches = [...requested, ...entries.filter(addressesExisting)];
103
+ const additions = entries.filter((entry) => !addressesExisting(entry));
95
104
  if (!patches.length && !additions.length) throw new Error('goal update_tasks requires updates or new tasks');
96
105
  if (patches.length > MAX_GOAL_TASKS) throw new Error(`goal updates support at most ${MAX_GOAL_TASKS} entries`);
97
- const byId = new Map(previous.map((task) => [task.id, { ...task }]));
98
106
  const seen = new Set();
99
107
  for (const patch of patches) {
100
108
  const id = clean(patch?.id);
@@ -3,6 +3,7 @@
3
3
  // No filesystem, no runtime coupling — values in, text out.
4
4
 
5
5
  import { goalTaskProgress } from './goal-tasks.mjs';
6
+ import { MAX_GOAL_TASKS } from './goal-tool-defs.mjs';
6
7
 
7
8
  function escapeGoalPromptText(value) {
8
9
  return String(value ?? '')
@@ -214,7 +215,7 @@ export function goalStateReminder(goal, { reason = '' } = {}) {
214
215
  }
215
216
  if (reason === 'deadline-reached' && goal.status === 'active') return '';
216
217
  const tasks = Array.isArray(goal.tasks) ? goal.tasks : [];
217
- const { tasksCompleted, tasksTotal } = goalTaskProgress(tasks);
218
+ const { tasksCompleted, tasksTotal, taskSlotsUsed } = goalTaskProgress(tasks);
218
219
  // Event-specific steering: what the model could not have learned from its own
219
220
  // tool results. Standing rules stay in the cached tool description.
220
221
  let lead = 'Current durable Goal snapshot:';
@@ -233,7 +234,7 @@ export function goalStateReminder(goal, { reason = '' } = {}) {
233
234
  ...(goal.status === 'blocked' ? [BLOCKED_LINE] : []),
234
235
  '',
235
236
  `Objective: ${escapeGoalPromptText(goal.objective)}`,
236
- `Status: ${escapeGoalPromptText(goal.status)} · tasks ${tasksCompleted}/${tasksTotal}`,
237
+ `Status: ${escapeGoalPromptText(goal.status)} · tasks ${tasksCompleted}/${tasksTotal} · task slots used ${taskSlotsUsed}/${MAX_GOAL_TASKS}`,
237
238
  ...(goal.blocker ? [`Waiting or stop reason: ${escapeGoalPromptText(goal.blocker)}`] : []),
238
239
  ...(goal.revision ? [`Revision: ${goal.revision}`] : []),
239
240
  ...goalTimeLines(goal),
@@ -16,7 +16,7 @@ const taskFields = {
16
16
  type: 'string',
17
17
  minLength: 1,
18
18
  maxLength: MAX_GOAL_TASK_TEXT_LENGTH,
19
- description: 'Required work or verification outcome.',
19
+ description: 'Brief work/check; results go in reply.',
20
20
  },
21
21
  status: {
22
22
  type: 'string',
@@ -22,6 +22,7 @@ function toolReply(ctx, id, goal, { full = false, previousIds = null } = {}) {
22
22
  status: goal.status,
23
23
  tasksCompleted: goal.tasksCompleted,
24
24
  tasksTotal: goal.tasksTotal,
25
+ taskSlotsUsed: goal.taskSlotsUsed,
25
26
  tasksUpdatedAt: goal.tasksUpdatedAt,
26
27
  timeUsedMs: goal.timeUsedMs,
27
28
  };
@@ -174,6 +174,23 @@ function hasMultipleAbsoluteWindowsPaths(value) {
174
174
  return Array.isArray(matches) && matches.length > 1;
175
175
  }
176
176
 
177
+ // A string that only lists absolute Windows paths ("C:\a C:\b", "C:\a,C:\b",
178
+ // "[C:/a, C:/b]", '["C:/a", "D:/b"]') splits exactly: every piece starts at a
179
+ // drive root, and no drive root can occur inside a Windows path, so spaces
180
+ // within a path stay put. Any other text around the paths returns null.
181
+ function splitListedAbsoluteWindowsPaths(value) {
182
+ const body = value
183
+ .trim()
184
+ .replace(/^\[([\s\S]*)\]$/, '$1')
185
+ .replace(/^[\s,;]+|[\s,;]+$/g, '');
186
+ const pieces = body
187
+ .split(/[\s,;]+(?=["']?[A-Za-z]:[\\/])/)
188
+ .map((piece) => piece.replace(/^(["'])(.*)\1$/, '$2'));
189
+ return pieces.every((piece) => /^[A-Za-z]:[\\/]/.test(piece) && !/[A-Za-z]:[\\/]/.test(piece.slice(3)))
190
+ ? pieces
191
+ : null;
192
+ }
193
+
177
194
  function isFiniteInt(v) {
178
195
  return typeof v === 'number' && Number.isFinite(v) && Math.floor(v) === v;
179
196
  }
@@ -418,8 +435,17 @@ function guardGrep(a) {
418
435
  return `Error: grep arg "${k}" must be string (got ${describeType(a[k])})`;
419
436
  }
420
437
  }
421
- // Independent explicit scopes share one request; no inferred path splitting.
438
+ // Independent explicit scopes share one request. A string that only lists
439
+ // absolute paths is those scopes; any other string holding several drive
440
+ // roots is refused rather than guessed at.
422
441
  for (const k of ['path', 'root']) {
442
+ if (hasOwn(a, k) && hasMultipleAbsoluteWindowsPaths(a[k])) {
443
+ const listed = splitListedAbsoluteWindowsPaths(a[k]);
444
+ if (!listed) {
445
+ return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
446
+ }
447
+ a[k] = listed;
448
+ }
423
449
  if (hasOwn(a, k) && Array.isArray(a[k])) {
424
450
  if (a[k].length === 0 || a[k].length > PUBLIC_PATH_BATCH_LIMIT || !a[k].every(isNonEmptyString)) {
425
451
  return `Error: grep arg "${k}" must contain 1-${PUBLIC_PATH_BATCH_LIMIT} non-empty path strings`;
@@ -429,9 +455,6 @@ function guardGrep(a) {
429
455
  if (hasOwn(a, k) && !isString(a[k])) {
430
456
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
431
457
  }
432
- if (hasOwn(a, k) && hasMultipleAbsoluteWindowsPaths(a[k])) {
433
- return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
434
- }
435
458
  }
436
459
  for (const k of ['head_limit', 'offset']) {
437
460
  const err = checkIntInRange(a, k, 0, MAX_INT);
@@ -33,7 +33,9 @@ export function sliceReadBodyByLines(body, origOffset, origLimit, readOffsetBase
33
33
  if (ln < minSeen) minSeen = ln;
34
34
  if (ln > maxSeen) maxSeen = ln;
35
35
  if (ln >= firstLine && ln <= lastLine) kept.push(line);
36
- } else if (kept.length === 0 && footerIdx === -1) {
36
+ } else if (kept.length === 0 && footerIdx === -1 && !Number.isFinite(maxSeen)) {
37
+ // Only a preamble before the first numbered row; a blank line or a
38
+ // truncation marker after the rows is not this slot's content.
37
39
  kept.push(line);
38
40
  }
39
41
  }
@@ -42,7 +42,8 @@ async function estimateTaskBytes(entry, workDir) {
42
42
  if (!st?.isFile()) return Infinity;
43
43
  const lines = Math.max(1, st.size / AVG_LINE_BYTES);
44
44
  let need = st.size + st.size / MIN_LINE_BYTES * LINE_PREFIX_BYTES;
45
- const limit = Number(entry?.limit);
45
+ // A coalesced entry reads its whole union window, not only its own slot.
46
+ const limit = Number(readEntryCoalescedDiskWindow(entry)?.limit ?? entry?.limit);
46
47
  if (Number.isFinite(limit) && limit > 0) need *= Math.min(1, limit / lines);
47
48
  return Math.ceil(need) + READ_RENDER_SLACK_BYTES;
48
49
  }
@@ -100,9 +100,12 @@ function _throwIfAborted(signal) {
100
100
  if (signal?.aborted) throw new Error('aborted');
101
101
  }
102
102
 
103
- function codeGraphBuildInvalidatedError() {
103
+ // `graph` is the build that finished anyway: the tree as of its own manifest,
104
+ // never cached as current. The last retry answers from it instead of failing.
105
+ function codeGraphBuildInvalidatedError(graph = null) {
104
106
  const error = new Error('code-graph build invalidated during prewarm');
105
107
  error.code = 'ERR_CODE_GRAPH_BUILD_INVALIDATED';
108
+ if (graph) error.graph = graph;
106
109
  return error;
107
110
  }
108
111
 
@@ -115,7 +118,12 @@ export async function _retryCodeGraphBuildAfterInvalidation(run, { signal = null
115
118
  try {
116
119
  return await run(attempt);
117
120
  } catch (error) {
118
- if (signal?.aborted || !isCodeGraphBuildInvalidatedError(error) || attempt === 1) {
121
+ if (signal?.aborted || !isCodeGraphBuildInvalidatedError(error)) throw error;
122
+ // Files kept changing through the retry too (another session editing
123
+ // the same tree). The finished graph is a consistent snapshot a few
124
+ // seconds old; answer this query from it rather than fail it.
125
+ if (attempt === 1) {
126
+ if (error.graph) return error.graph;
119
127
  throw error;
120
128
  }
121
129
  }
@@ -159,7 +167,7 @@ async function _validateDiskCodeGraphHit({
159
167
  if (diskEntry.signature !== signature) return { graph: null, manifest, signature };
160
168
  const graph = deserializeGraph(graphCwd, diskEntry);
161
169
  if (!graph) return { graph: null, manifest, signature };
162
- if (getGeneration(graphCwd) !== genAtStart) return { invalidated: true };
170
+ if (getGeneration(graphCwd) !== genAtStart) return { invalidated: true, graph };
163
171
  setMemoryCache(graphCwd, { ts: now, signature, graph });
164
172
  return { graph, manifest, signature };
165
173
  }
@@ -195,7 +203,7 @@ export async function _runDiskCodeGraphFastPath({
195
203
  _throwIfAborted(signal);
196
204
  const hit = await validateDiskHit({ graphCwd, diskEntry, genAtStart, signal });
197
205
  _throwIfAborted(signal);
198
- if (hit?.invalidated) throw codeGraphBuildInvalidatedError();
206
+ if (hit?.invalidated) throw codeGraphBuildInvalidatedError(hit.graph);
199
207
  if (hit?.graph) return hit.graph;
200
208
  _throwIfAborted(signal);
201
209
  const workerPromise = spawnWorker(release, hit?.manifest || null, hit?.signature || null);
@@ -463,7 +471,7 @@ export function _spawnCodeGraphWorker(
463
471
  // manifest lock.
464
472
  setDiskCache(graphCwd, msg.graph, { persist: false });
465
473
  }
466
- settle(genStillCurrent ? msg.graph : codeGraphBuildInvalidatedError());
474
+ settle(genStillCurrent ? msg.graph : codeGraphBuildInvalidatedError(msg.graph));
467
475
  } else {
468
476
  settle(_codeGraphWorkerFailure(msg));
469
477
  }
@@ -79,7 +79,7 @@ function recoveryForCode(code, args) {
79
79
  code,
80
80
  next: 'wait_for_user',
81
81
  guidance:
82
- 'Computer Use yielded to the user. Call wait_for_user for bounded waiting. Ordinary physical input may resume after the host-configured quiet interval (default 5 seconds); explicit stops and uncertain cleanup/observation require the user. Timeout does not authorize input. After resumed, capture fresh state; never replay interrupted input. Manual Resume is also available on the overlay.',
82
+ "Computer Use yielded to the user. Call wait_for_user for bounded waiting. The user's Resume on the overlay continues the pause, and ordinary physical input may also resume after the host-configured quiet interval (default 5 seconds); explicit stops and uncertain cleanup/observation require the user. Stop cancels the task. Timeout does not authorize input. After resumed, capture fresh state; never replay interrupted input.",
83
83
  };
84
84
  }
85
85
  if (DIAGNOSE_CODES.has(code)) {
@@ -25,6 +25,8 @@ function applyField(current, path, change) {
25
25
  const next = isPlainObject(current) ? { ...current } : {};
26
26
  const [key, ...rest] = path;
27
27
  if (change.remove && rest.length && !Object.hasOwn(next, key)) return next;
28
+ // Seed-only field: written where it is missing, never over a stored value.
29
+ if (change.ifAbsent && !rest.length && Object.hasOwn(next, key)) return next;
28
30
  if (!rest.length && change.remove) {
29
31
  delete next[key];
30
32
  } else {
@@ -28,7 +28,7 @@ function getBackupRoot() {
28
28
  return process.env.MIXDOG_USER_DATA_BACKUP_ROOT || join(mixdogConfigBaseDir(), 'backups', 'user-data');
29
29
  }
30
30
 
31
- const USER_DATA_FILES = ['mixdog-config.json', 'instructions.md', 'user-workflow.md'];
31
+ const USER_DATA_FILES = ['mixdog-config.json'];
32
32
 
33
33
  const USER_DATA_DIRS = ['schedules', 'webhooks', 'workflows'];
34
34
 
@@ -102,10 +102,20 @@ export function createConfigLifecycle({
102
102
  const adopted = adoptConfig(nextConfig, { hasSecrets });
103
103
  const changes = cfgMod.createConfigPatch(configSaveBaseline, adopted);
104
104
  configSaveBaseline = structuredClone(adopted);
105
- if (changes.length) writers.queueConfigChanges(changes);
105
+ if (changes.length) writers.queueConfigChanges([...changes, ...seedChanges(adopted, changes)]);
106
106
  return adopted;
107
107
  }
108
108
 
109
+ // A fresh profile's install markers (an empty `builtins`) and off-by-default
110
+ // agents equal the defaults, so no diff ever carries them — yet on disk
111
+ // their absence reads as a grandfathered profile with every agent on. The
112
+ // first write seeds them where the stored section lacks them.
113
+ function seedChanges(config, changes) {
114
+ return ['builtins', 'disabledAgents']
115
+ .filter((key) => config[key] !== undefined && !changes.some((change) => change.path[0] === key))
116
+ .map((key) => ({ path: [key], value: structuredClone(config[key]), ifAbsent: true }));
117
+ }
118
+
109
119
  function scheduleSkillsSave(names) {
110
120
  // This field belongs to its dedicated writer, not a later unrelated save.
111
121
  configSaveBaseline.skills = structuredClone(getConfig().skills);
@@ -24,7 +24,7 @@ export function createCoreMemoryContext({ getCurrentCwd, getConfig, bootProfile,
24
24
  async function loadCoreMemoryContext() {
25
25
  // User-curated core memory injects into new sessions by default. The
26
26
  // Memory toggle (settings → General) OFF skips ONLY this accumulated
27
- // core-memory block — profile and Project Instructions inject regardless.
27
+ // core-memory block — the profile injects regardless.
28
28
  if (!(featureEnvOverride('MIXDOG_FEATURE_MEMORY') ?? memoryToolsEnabled(getConfig(), true))) {
29
29
  bootProfile('core-memory:disabled');
30
30
  return '';
@@ -17,6 +17,18 @@ export function normalizeWebSearchAllowedDomain(site) {
17
17
  }
18
18
  }
19
19
 
20
+ // A site filter may name several domains the way a search box takes them
21
+ // ("a.com OR b.com", "a.com, b.com", "site:a.com site:b.com"); each becomes its
22
+ // own allowed domain instead of one invalid combined entry.
23
+ export function normalizeWebSearchAllowedDomains(site) {
24
+ const domains = clean(site)
25
+ .split(/[\s,|]+/)
26
+ .filter((part) => part && part.toUpperCase() !== 'OR')
27
+ .map((part) => normalizeWebSearchAllowedDomain(part.replace(/^site:/i, '')))
28
+ .filter(Boolean);
29
+ return [...new Set(domains)];
30
+ }
31
+
20
32
  export function nativeWebSearchUserLocation(locale) {
21
33
  if (!locale || typeof locale !== 'object' || Array.isArray(locale)) return null;
22
34
  const location = { type: 'approximate' };
@@ -28,7 +40,7 @@ export function nativeWebSearchUserLocation(locale) {
28
40
  }
29
41
 
30
42
  export function nativeWebSearchTool(args = {}, toolType = 'web_search', providerName = '') {
31
- const domain = normalizeWebSearchAllowedDomain(args.site);
43
+ const domains = normalizeWebSearchAllowedDomains(args.site);
32
44
  const type = clean(toolType) || 'web_search';
33
45
  const location = nativeWebSearchUserLocation(args.locale);
34
46
  if (providerName === 'gemini') {
@@ -40,19 +52,19 @@ export function nativeWebSearchTool(args = {}, toolType = 'web_search', provider
40
52
  name: 'web_search',
41
53
  max_uses: Math.max(1, Math.min(10, Number(args.maxResults) || 5)),
42
54
  };
43
- if (domain) tool.allowed_domains = [domain];
55
+ if (domains.length) tool.allowed_domains = domains;
44
56
  if (location) tool.user_location = location;
45
57
  return tool;
46
58
  }
47
59
  if (providerName === 'grok-oauth' || providerName === 'xai') {
48
60
  const tool = { type };
49
- if (domain) tool.filters = { allowed_domains: [domain] };
61
+ if (domains.length) tool.filters = { allowed_domains: domains };
50
62
  return tool;
51
63
  }
52
64
  const tool = { type };
53
65
  if (type === 'web_search') {
54
66
  tool.search_context_size = clean(args.contextSize) || 'low';
55
- if (domain) tool.filters = { allowed_domains: [domain] };
67
+ if (domains.length) tool.filters = { allowed_domains: domains };
56
68
  if (location) tool.user_location = location;
57
69
  }
58
70
  return tool;
@@ -4,6 +4,7 @@
4
4
  // catalog flattened into deduplicated rows with per-provider profiling.
5
5
  import { isSelectableLlmModel } from '../model-recency.mjs';
6
6
  import { sharedProviderCatalog } from '../provider-catalog-cache.mjs';
7
+ import { NO_ENABLED_PROVIDERS } from '../../runtime/agent/orchestrator/providers/registry-errors.mjs';
7
8
 
8
9
  export function createCatalogLoader({
9
10
  caches,
@@ -36,7 +37,9 @@ export function createCatalogLoader({
36
37
  * for. ensureProvidersReady starts the daemon-wide force refresh without
37
38
  * blocking boot; a foreground full picker load must join that refresh,
38
39
  * otherwise it can snapshot yesterday's provider cache moments before the
39
- * refresh invalidates it, leaving the UI on the stale first-open rows. */
40
+ * refresh invalidates it, leaving the UI on the stale first-open rows.
41
+ * Resolves false when no provider is enabled: a fresh install has nothing
42
+ * to load, and the registry refuses an all-disabled init by design. */
40
43
  async function prepare({ forceRefresh, loadSecrets }) {
41
44
  if (loadSecrets) {
42
45
  const secretsStartedAt = performance.now();
@@ -45,7 +48,12 @@ export function createCatalogLoader({
45
48
  profile('secrets-ready', { ms: (performance.now() - secretsStartedAt).toFixed(1) });
46
49
  }
47
50
  const providersStartedAt = performance.now();
48
- await ensureProvidersReady(config().providers || {});
51
+ try {
52
+ await ensureProvidersReady(config().providers || {});
53
+ } catch (error) {
54
+ if (error?.code === NO_ENABLED_PROVIDERS) return false;
55
+ throw error;
56
+ }
49
57
  profile('providers-ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
50
58
  const refreshStartedAt = performance.now();
51
59
  if (!forceRefresh && typeof reg().refreshProviderCatalogsOnStartup === 'function') {
@@ -55,6 +63,7 @@ export function createCatalogLoader({
55
63
  await reg().refreshCatalogs({ force: true });
56
64
  }
57
65
  profile('catalog-refresh-ready', { ms: (performance.now() - refreshStartedAt).toFixed(1) });
66
+ return true;
58
67
  }
59
68
 
60
69
  /** `request` ({ seq }) identifies the caller's load; `request.complete`
@@ -62,7 +71,13 @@ export function createCatalogLoader({
62
71
  return async function loadProviderModelsFresh({ forceRefresh = false, loadSecrets = true, request = null } = {}) {
63
72
  const startedAt = performance.now();
64
73
  profile('load:start', { forceRefresh, loadSecrets });
65
- await prepare({ forceRefresh, loadSecrets });
74
+ // No enabled provider is an empty catalog, not a failure. The request
75
+ // stays incomplete so this answer is never cached: connecting a provider
76
+ // is picked up by the next read.
77
+ if (!(await prepare({ forceRefresh, loadSecrets }))) {
78
+ profile('load:done', { ms: (performance.now() - startedAt).toFixed(1), providers: 0, rows: 0 });
79
+ return [];
80
+ }
66
81
  const ownsLoad = request !== null && request.seq === caches.providerModelsLoadSeq;
67
82
  const revision = meta.syncCatalogRevision();
68
83
  // Preparation may legitimately refresh the catalog. Carry that revision
@@ -464,7 +464,6 @@ const READ_ONLY_SETUP_ACTIONS = new Set([
464
464
  'list_models',
465
465
  'get_mcp_server',
466
466
  'read_definition',
467
- 'get_instructions',
468
467
  'search_local_models',
469
468
  'inspect_hf_model',
470
469
  'local_model_details',
@@ -30,8 +30,6 @@ export const SETUP_EXTENDED_ACTION_FIELDS = Object.freeze({
30
30
  set_appearance: 'appearance',
31
31
  save_project: 'project',
32
32
  remove_project: 'projectPath',
33
- get_instructions: 'projectPath',
34
- set_instructions: 'projectPath content expectedContent',
35
33
  revoke_linked_device: 'name',
36
34
  });
37
35
 
@@ -50,8 +48,6 @@ export const SETUP_DESKTOP_ACTIONS = Object.freeze([
50
48
  'set_appearance',
51
49
  'save_project',
52
50
  'remove_project',
53
- 'get_instructions',
54
- 'set_instructions',
55
51
  'revoke_linked_device',
56
52
  ]);
57
53
  export const SETUP_DESKTOP_DOMAINS = Object.freeze(['desktop', 'appearance', 'projects', 'connection']);
@@ -166,6 +162,7 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
166
162
  }),
167
163
  desktop: object({
168
164
  keepAwake: boolean,
165
+ runInBackground: boolean,
169
166
  usagePinned: boolean,
170
167
  computerObserveOnly: boolean,
171
168
  }),
@@ -179,15 +176,7 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
179
176
  zoom: { type: 'number', minimum: 0.5, maximum: 2 },
180
177
  }),
181
178
  project: object({ path: text, alias: text }, ['path']),
182
- projectPath: {
183
- type: ['string', 'null'],
184
- description: 'Exact registered Project root; null means Common Instructions (instruction actions only).',
185
- },
186
- content: { ...text, description: 'Complete new Instructions content. Preserve unrelated instructions.' },
187
- expectedContent: {
188
- ...text,
189
- description: 'Unchanged content returned by get_instructions; prevents overwriting a concurrent edit.',
190
- },
179
+ projectPath: { ...text, description: 'Exact registered Project root.' },
191
180
  });
192
181
 
193
182
  export const SETUP_HANDOFFS = Object.freeze({
@@ -57382,7 +57382,7 @@ function mixdogConfigBaseDir() {
57382
57382
  function getBackupRoot() {
57383
57383
  return process.env.MIXDOG_USER_DATA_BACKUP_ROOT || join10(mixdogConfigBaseDir(), "backups", "user-data");
57384
57384
  }
57385
- var USER_DATA_FILES = ["mixdog-config.json", "instructions.md", "user-workflow.md"];
57385
+ var USER_DATA_FILES = ["mixdog-config.json"];
57386
57386
  var USER_DATA_DIRS = ["schedules", "webhooks", "workflows"];
57387
57387
  function stamp() {
57388
57388
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -4,8 +4,6 @@ name: Default
4
4
  description: "Handle tasks as requested."
5
5
  ---
6
6
 
7
- # Default
8
-
9
7
  Consult the user and build the work plan together. Explicit read-only requests
10
8
  proceed immediately. When file edits, state mutation, or delegation are
11
9
  required, investigate and plan only until the user approves the latest plan.
@@ -5,7 +5,5 @@ description: "No user mid-run; requests are approved. Complete, then report."
5
5
  hidden: true
6
6
  ---
7
7
 
8
- # Headless
9
-
10
8
  The user does not intervene during the run. Treat the request as approved,
11
9
  carry it out to the end, and report the result.