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.
- package/package.json +1 -1
- package/src/defaults/skills/computer-use/SKILL.md +13 -12
- package/src/defaults/skills/setup/SKILL.md +1 -4
- package/src/defaults/skills/setup/references/actions.md +0 -2
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +5 -0
- package/src/runtime/agent/orchestrator/config.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/registry-errors.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +13 -5
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +27 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +13 -5
- package/src/runtime/computer-bridge/error-recovery.mjs +1 -1
- package/src/runtime/shared/config-patch.mjs +2 -0
- package/src/runtime/shared/user-data-guard.mjs +1 -1
- package/src/session-runtime/config-lifecycle.mjs +11 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +1 -1
- package/src/session-runtime/native-web-search/tool-shape.mjs +16 -4
- package/src/session-runtime/provider-models/catalog-load.mjs +18 -3
- package/src/session-runtime/setup-tool/executor.mjs +0 -1
- package/src/session-runtime/setup-tool/settings-contract.mjs +2 -13
- package/src/tui/dist/index.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +0 -2
- package/src/workflows/headless/WORKFLOW.md +0 -2
package/package.json
CHANGED
|
@@ -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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
or change the idle policy, on
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
93
|
-
const
|
|
94
|
-
if (!Array.isArray(
|
|
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),
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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'
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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({
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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"
|
|
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.
|