mixdog 1.0.0 → 1.0.2
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 +2 -2
- 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 +6 -5
- package/src/defaults/skills/setup/references/surfaces.md +9 -6
- 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/context/role-instructions.mjs +0 -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/session/manager/session-prompt-composition.mjs +1 -1
- 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/cwd-plugins/plugin-status.mjs +24 -4
- 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/session-runtime/setup-tool/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
- package/src/tui/app/doctor.mjs +78 -2
- 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/scripts/lib/bench-sandbox.test.mjs +0 -54
- package/scripts/lib/cli-args.test.mjs +0 -64
- package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
- package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
- package/scripts/lib/trace-row.test.mjs +0 -47
- package/scripts/lib/trace-stats.test.mjs +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"!scripts/bench/",
|
|
32
32
|
"!scripts/recall-bench-*.txt",
|
|
33
33
|
"!scripts/*-test.mjs",
|
|
34
|
-
"!scripts
|
|
34
|
+
"!scripts/**/*.test.mjs",
|
|
35
35
|
"!scripts/tool-contracts/",
|
|
36
36
|
"!scripts/provider-toolcall/",
|
|
37
37
|
"!scripts/session-transport/",
|
|
@@ -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
|
|
@@ -64,13 +64,11 @@ Read this when choosing a `setup` mutation.
|
|
|
64
64
|
| Delete an existing schedule/webhook | `delete_automation` |
|
|
65
65
|
| Enable/disable a schedule/webhook | `set_automation_enabled` |
|
|
66
66
|
| Webhook listener enabled state, port, or domain | `set_webhook_config` |
|
|
67
|
-
| Desktop keep-awake, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
67
|
+
| Desktop keep-awake, run in background after window close, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
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
|
-
|
|
|
72
|
-
| Replace unchanged Project/Common Instructions and retain a backup | `set_instructions` |
|
|
73
|
-
| Revoke one linked device; re-pairing restores access | `revoke_linked_device` |
|
|
71
|
+
| Revoke one linked device by its client id from `status connection`; re-pairing restores access | `revoke_linked_device` |
|
|
74
72
|
| Managed local model context allocation; null restores automatic | `set_local_context` |
|
|
75
73
|
|
|
76
74
|
## Routes and workflow
|
|
@@ -118,7 +116,10 @@ values through chat.
|
|
|
118
116
|
## Built-in features
|
|
119
117
|
|
|
120
118
|
Read `status features` and distinguish installation, enabled state, and live
|
|
121
|
-
bridge state.
|
|
119
|
+
bridge state. `status features` covers Git, Memory, Office, Code Tidy, Local
|
|
120
|
+
Provider and Web Search plus Browser/Computer bridge activity; voice status and
|
|
121
|
+
the Browser Use / Computer Use installed and enabled markers are read with
|
|
122
|
+
`status desktop`.
|
|
122
123
|
|
|
123
124
|
- Git, Memory, Office, and Local Provider are install-first capabilities.
|
|
124
125
|
- Git and Office installation run on the Desktop when this conversation is
|
|
@@ -37,9 +37,9 @@ An attached Desktop or TUI may navigate and return `opened:true`.
|
|
|
37
37
|
- Settings → Providers: API-key, OAuth, local providers, and usage sign-in.
|
|
38
38
|
- Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
|
|
39
39
|
- Settings → Connection: web-app pairing and linked devices.
|
|
40
|
-
- Settings → System: update, keep-awake,
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
- Settings → System: update, keep-awake, run in background after the window
|
|
41
|
+
closes, and Doctor.
|
|
42
|
+
- Projects: Project registration, name, Project Memories, and Common Memory.
|
|
43
43
|
- Workflows: workflow packs, Main and Web Search defaults, and agent editing.
|
|
44
44
|
- Extensions → Plugin: Built-in cards and plugins.
|
|
45
45
|
- Extensions → Skill: skills and MCP servers.
|
|
@@ -49,12 +49,15 @@ the user to Desktop.
|
|
|
49
49
|
|
|
50
50
|
## Desktop-backed settings
|
|
51
51
|
|
|
52
|
-
Setup can read and change Desktop appearance, keep-awake,
|
|
53
|
-
Computer observe-only, Browser/Computer/voice installation and
|
|
54
|
-
|
|
52
|
+
Setup can read and change Desktop appearance, keep-awake, run in background,
|
|
53
|
+
usage pin, Computer observe-only, Browser/Computer/voice installation and
|
|
54
|
+
toggles, Projects, and linked-device revocation through the attached
|
|
55
55
|
Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
|
|
56
56
|
to the paired browser. No Desktop claimant means no confirmed change.
|
|
57
57
|
|
|
58
|
+
Computer observe-only has no Desktop settings control; setup is its only
|
|
59
|
+
supported route. Usage pin is the pin toggle on the activity rail usage card.
|
|
60
|
+
|
|
58
61
|
Schedules/webhooks and workflow/agent/skill definitions use their runtime
|
|
59
62
|
actions directly. These do not require clicking their Desktop rails.
|
|
60
63
|
|
|
@@ -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). */
|
|
@@ -281,7 +281,6 @@ export function composeSystemPrompt(opts) {
|
|
|
281
281
|
// pre-tool preamble toward English.
|
|
282
282
|
const environmentParts = [
|
|
283
283
|
opts.sessionStartContext,
|
|
284
|
-
opts.projectInstructionsContext,
|
|
285
284
|
opts.environmentContext,
|
|
286
285
|
opts.languageContext,
|
|
287
286
|
].map(trimmedPromptSlice);
|
|
@@ -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
|
};
|
|
@@ -86,7 +86,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
86
86
|
const languageContext = leadOnly ? _buildLeadLanguageContext() : '';
|
|
87
87
|
const shellEnvironmentContext = buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
|
|
88
88
|
// Persisted env tail: refreshSessionBp3Environment rebuilds the env block
|
|
89
|
-
// as [session,
|
|
89
|
+
// as [session, bp3EnvironmentContext], so the language block must
|
|
90
90
|
// already sit at the end of this string to stay last after a refresh.
|
|
91
91
|
const environmentTailContext = [shellEnvironmentContext, languageContext].filter(Boolean).join('\n\n---\n\n');
|
|
92
92
|
const composed = composeSystemPrompt({
|
|
@@ -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 '';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cwd-plugins/plugin-status.mjs — the registered-plugin status list with its
|
|
2
2
|
// per-root caches (manifest keyed on mtime, MCP discovery and skill-file count
|
|
3
3
|
// on a short TTL).
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { discoverPluginMcp } from '../../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
|
|
5
6
|
import { pluginMetadata } from '../../runtime/shared/plugin-metadata.mjs';
|
|
6
7
|
|
|
@@ -35,11 +36,25 @@ export function createPluginStatus({
|
|
|
35
36
|
}
|
|
36
37
|
return key;
|
|
37
38
|
}
|
|
39
|
+
// A manifest file that exists but is not a JSON object is breakage.
|
|
40
|
+
function manifestInvalid(root) {
|
|
41
|
+
for (const rel of ['.codex-plugin/plugin.json', 'plugin.json']) {
|
|
42
|
+
const file = resolve(root, rel);
|
|
43
|
+
if (!existsSync(file)) continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
38
53
|
function cachedPluginData(root) {
|
|
39
54
|
const key = manifestMtimeKey(root);
|
|
40
55
|
const hit = pluginRootCache.get(root);
|
|
41
56
|
if (hit && hit.key === key) return hit;
|
|
42
|
-
const entry = { key, manifest: pluginManifest(root) };
|
|
57
|
+
const entry = { key, manifest: pluginManifest(root), invalid: manifestInvalid(root) };
|
|
43
58
|
pluginRootCache.set(root, entry);
|
|
44
59
|
return entry;
|
|
45
60
|
}
|
|
@@ -65,8 +80,9 @@ export function createPluginStatus({
|
|
|
65
80
|
|
|
66
81
|
function registeredPlugin(entry, configuredMcp) {
|
|
67
82
|
const root = clean(entry.root);
|
|
68
|
-
if (!root
|
|
69
|
-
|
|
83
|
+
if (!root) return null;
|
|
84
|
+
if (!existsSync(root)) return { missing: true, id: clean(entry.id) || clean(entry.name) || root, name: clean(entry.name) || clean(entry.id) || root.split(/[\\/]/).pop() || root };
|
|
85
|
+
const { manifest, invalid } = cachedPluginData(root);
|
|
70
86
|
const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
|
|
71
87
|
const { mcpScript, mcpInline } = cachedMcpDiscovery(root);
|
|
72
88
|
const plugin = {
|
|
@@ -89,6 +105,7 @@ export function createPluginStatus({
|
|
|
89
105
|
mcpScript,
|
|
90
106
|
mcpInline,
|
|
91
107
|
};
|
|
108
|
+
if (invalid) plugin.invalid = true;
|
|
92
109
|
plugin.mcpServerName = pluginMcpServerName(plugin);
|
|
93
110
|
plugin.mcpEnabled =
|
|
94
111
|
Object.hasOwn(configuredMcp, plugin.mcpServerName) ||
|
|
@@ -101,9 +118,11 @@ export function createPluginStatus({
|
|
|
101
118
|
const dataDir = cfgMod.getPluginData?.();
|
|
102
119
|
const configuredMcp = config?.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
|
|
103
120
|
const plugins = [];
|
|
121
|
+
const missing = [];
|
|
104
122
|
for (const entry of listRegisteredPlugins({ dataDir })) {
|
|
105
123
|
const plugin = registeredPlugin(entry, configuredMcp);
|
|
106
|
-
if (plugin)
|
|
124
|
+
if (plugin?.missing) missing.push({ id: plugin.id, name: plugin.name, enabled: entry.enabled !== false });
|
|
125
|
+
else if (plugin) plugins.push(plugin);
|
|
107
126
|
}
|
|
108
127
|
plugins.sort((a, b) => {
|
|
109
128
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
@@ -113,6 +132,7 @@ export function createPluginStatus({
|
|
|
113
132
|
return {
|
|
114
133
|
count: plugins.length,
|
|
115
134
|
plugins,
|
|
135
|
+
missing,
|
|
116
136
|
roots: {
|
|
117
137
|
registry: admin.registryPath,
|
|
118
138
|
installed: admin.installRoot,
|