mixdog 0.9.23 → 0.9.24
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/scripts/boot-smoke.mjs +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/channel-daemon-smoke.mjs +327 -9
- package/scripts/channel-daemon-stub.mjs +12 -1
- package/scripts/debounced-skills-async-save-test.mjs +57 -0
- package/scripts/explore-bench-tmp.mjs +17 -0
- package/scripts/find-fuzzy-hidden-test.mjs +145 -0
- package/scripts/mcp-grace-deferred-test.mjs +149 -0
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/tool-smoke.mjs +38 -30
- package/src/rules/agent/30-explorer.md +6 -0
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/shared/01-tool.md +11 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
- package/src/runtime/agent/orchestrator/config.mjs +33 -7
- package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +39 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +145 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +43 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +70 -21
- package/src/runtime/channels/lib/crash-log.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -180
- package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -13
- package/src/runtime/channels/lib/runtime-paths.mjs +6 -6
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -17
- package/src/runtime/channels/lib/tool-format.mjs +7 -2
- package/src/runtime/channels/lib/worker-main.mjs +9 -28
- package/src/runtime/memory/lib/query-handlers.mjs +4 -1
- package/src/runtime/memory/lib/recall-format.mjs +7 -3
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +130 -2
- package/src/runtime/shared/background-tasks.mjs +1 -1
- package/src/runtime/shared/config.mjs +53 -1
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +19 -0
- package/src/runtime/shared/update-checker.mjs +3 -0
- package/src/runtime/shared/user-data-guard.mjs +66 -0
- package/src/session-runtime/config-lifecycle.mjs +175 -15
- package/src/session-runtime/mcp-glue.mjs +30 -0
- package/src/session-runtime/runtime-core.mjs +91 -7
- package/src/session-runtime/session-turn-api.mjs +42 -16
- package/src/session-runtime/tool-catalog.mjs +44 -0
- package/src/standalone/channel-admin.mjs +32 -3
- package/src/standalone/channel-daemon-client.mjs +3 -1
- package/src/standalone/channel-daemon-transport.mjs +202 -8
- package/src/standalone/channel-daemon.mjs +54 -17
- package/src/standalone/channel-worker.mjs +18 -7
- package/src/standalone/explore-tool.mjs +87 -15
- package/src/tui/App.jsx +2 -2
- package/src/tui/components/StatusLine.jsx +3 -3
- package/src/tui/components/ToolExecution.jsx +14 -2
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/dist/index.mjs +209 -44
- package/src/tui/engine/agent-job-feed.mjs +5 -0
- package/src/tui/engine/notification-plan.mjs +5 -0
- package/src/tui/engine/session-api.mjs +6 -1
- package/src/tui/engine/tool-card-results.mjs +14 -5
- package/src/tui/engine/turn.mjs +9 -2
- package/src/tui/engine.mjs +31 -12
- package/src/ui/statusline-agents.mjs +36 -0
- package/src/ui/statusline.mjs +15 -5
- package/src/runtime/channels/lib/seat-lock.mjs +0 -196
|
@@ -17,8 +17,7 @@ import {
|
|
|
17
17
|
resolveV4AEntryPath,
|
|
18
18
|
parsedEntryResolvedPath,
|
|
19
19
|
isResolvedPathOutsideBase,
|
|
20
|
-
|
|
21
|
-
mergeDuplicateParsedModifyEntries,
|
|
20
|
+
splitParsedModifyWaves,
|
|
22
21
|
renderParsedUnifiedPatch,
|
|
23
22
|
rewriteHeaderPaths,
|
|
24
23
|
preValidateNativeBatch,
|
|
@@ -190,17 +189,17 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
190
189
|
if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
|
|
191
190
|
return 'Error: patch contained no file sections';
|
|
192
191
|
}
|
|
192
|
+
// Split duplicate modify-target blocks into sequential waves: occurrence i
|
|
193
|
+
// of a path lands in wave i so each duplicate applies against the prior
|
|
194
|
+
// wave's on-disk result. Non-duplicate patches yield exactly one wave, so
|
|
195
|
+
// single-target behavior is unchanged.
|
|
196
|
+
let parsedWaves = v4aRenameOnly ? [] : [parsed];
|
|
193
197
|
if (!v4aRenameOnly) {
|
|
194
198
|
try {
|
|
195
|
-
|
|
199
|
+
parsedWaves = splitParsedModifyWaves(parsed, basePath);
|
|
196
200
|
} catch (err) {
|
|
197
201
|
return `Error: ${err?.message || String(err)}`;
|
|
198
202
|
}
|
|
199
|
-
const merged = mergeDuplicateParsedModifyEntries(parsed, basePath);
|
|
200
|
-
if (merged.changed) {
|
|
201
|
-
parsed = merged.parsed;
|
|
202
|
-
normalizedPatchStr = renderParsedUnifiedPatch(parsed);
|
|
203
|
-
}
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
if (!v4aRenameOnly) {
|
|
@@ -210,18 +209,22 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
210
209
|
return `Error: ${err?.message || String(err)}`;
|
|
211
210
|
}
|
|
212
211
|
}
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
// Pre-validate each wave independently: a wave only ever holds unique
|
|
213
|
+
// targets, so the native batch's per-file semantics stay intact.
|
|
214
|
+
const waveDispatch = [];
|
|
215
215
|
if (!v4aRenameOnly) {
|
|
216
216
|
try {
|
|
217
|
-
(
|
|
217
|
+
for (const wparsed of parsedWaves) {
|
|
218
|
+
const { entries, headerRewrites } = await preValidateNativeBatch(wparsed, basePath);
|
|
219
|
+
waveDispatch.push({ parsed: wparsed, entries, headerRewrites });
|
|
220
|
+
}
|
|
218
221
|
} catch (err) {
|
|
219
222
|
return `Error: ${err?.message || String(err)}`;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
const _lockPaths = [
|
|
224
|
-
...entries.map((entry) => entry.fullPath),
|
|
227
|
+
...new Set(waveDispatch.flatMap((wd) => wd.entries.map((entry) => entry.fullPath))),
|
|
225
228
|
...(v4aRenamePlan?.renameSections || []).flatMap((section) => {
|
|
226
229
|
const src = resolveV4AEntryPath(basePath, section.path);
|
|
227
230
|
const dest = resolveV4AEntryPath(basePath, section.movePath);
|
|
@@ -240,56 +243,96 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
240
243
|
if (lines.length === 0) return 'Error: patch contained no applicable file sections';
|
|
241
244
|
return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { backend: 'v4a-rename' });
|
|
242
245
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
nativePatchStr,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
246
|
+
// Apply one wave (a set of unique targets) via the existing native(+js)
|
|
247
|
+
// split. Returns { backend, text } on success or { backend, error } so the
|
|
248
|
+
// caller can decide whether earlier waves already committed to disk.
|
|
249
|
+
const applyWave = async ({ parsed: wparsed, entries: wentries, headerRewrites: whr }) => {
|
|
250
|
+
const insideEntries = wentries.filter((entry) => !isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
251
|
+
const outsideEntries = wentries.filter((entry) => isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
252
|
+
const parsedInside = (wparsed || []).filter(
|
|
253
|
+
(entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
|
|
254
|
+
);
|
|
255
|
+
const backend = outsideEntries.length > 0
|
|
256
|
+
? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
|
|
257
|
+
: 'native-patch';
|
|
258
|
+
const resultParts = [];
|
|
259
|
+
if (insideEntries.length > 0) {
|
|
260
|
+
const nativePatchStr = rewriteHeaderPaths(renderParsedUnifiedPatch(parsedInside), whr);
|
|
261
|
+
const nativeResult = await dispatchNativePatch({
|
|
262
|
+
entries: insideEntries,
|
|
263
|
+
basePath,
|
|
264
|
+
nativePatchStr,
|
|
265
|
+
fuzz,
|
|
266
|
+
rejectPartial,
|
|
267
|
+
dryRun,
|
|
268
|
+
readStateScope,
|
|
269
|
+
signal: abortSignal,
|
|
270
|
+
parsed: parsedInside,
|
|
271
|
+
});
|
|
272
|
+
if (isPatchErrorText(nativeResult)) return { backend, error: nativeResult };
|
|
273
|
+
resultParts.push(nativeResult);
|
|
267
274
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
if (outsideEntries.length > 0) {
|
|
276
|
+
// Out-of-base targets are applied via the JS dispatcher (no base-path
|
|
277
|
+
// confinement); write permission is enforced at the hook layer.
|
|
278
|
+
const jsResult = await dispatchJsPatchEntries({
|
|
279
|
+
rows: outsideEntries,
|
|
280
|
+
parsed: wparsed,
|
|
281
|
+
basePath,
|
|
282
|
+
dryRun,
|
|
283
|
+
fuzzy,
|
|
284
|
+
readStateScope,
|
|
285
|
+
});
|
|
286
|
+
if (isPatchErrorText(jsResult)) return { backend, error: jsResult };
|
|
287
|
+
resultParts.push(jsResult);
|
|
288
|
+
}
|
|
289
|
+
return { backend, text: resultParts.join('\n') };
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Duplicate-target blocks were split into contiguous sequential groups
|
|
293
|
+
// (listed order preserved); apply them in order, each against the prior
|
|
294
|
+
// group's on-disk result.
|
|
295
|
+
const waveTexts = [];
|
|
296
|
+
let backend = 'native-patch';
|
|
297
|
+
// dry_run never writes, so a later group would validate against unchanged
|
|
298
|
+
// disk and false-fail on any block that depends on an earlier edit. Only
|
|
299
|
+
// the first group is validated under dry_run; the rest are reported as
|
|
300
|
+
// unsimulated below (no false failures).
|
|
301
|
+
const groupCount = (dryRun && waveDispatch.length > 1) ? 1 : waveDispatch.length;
|
|
302
|
+
for (let w = 0; w < groupCount; w++) {
|
|
303
|
+
const res = await applyWave(waveDispatch[w]);
|
|
304
|
+
backend = res.backend;
|
|
305
|
+
if (res.error) {
|
|
306
|
+
if (w === 0) return wrapPatchMutationOutput(res.error, mutationPlan, { backend });
|
|
307
|
+
// A later group failed. rejectPartial makes each group all-or-nothing,
|
|
308
|
+
// so every block in groups 1..w is fully committed to disk and left in
|
|
309
|
+
// place. List them all so the caller knows the true on-disk state.
|
|
310
|
+
const failMsg = res.error.replace(/^Error:\s*/, '');
|
|
311
|
+
const note = [
|
|
312
|
+
`Error: apply_patch: a block failed in sequential group ${w + 1}/${waveDispatch.length}; every edit listed below was already applied to disk (writes committed) and left in place:`,
|
|
313
|
+
waveTexts.join('\n'),
|
|
314
|
+
'--- failing block ---',
|
|
315
|
+
failMsg,
|
|
316
|
+
].join('\n');
|
|
317
|
+
return wrapPatchMutationOutput(note, mutationPlan, { backend });
|
|
283
318
|
}
|
|
284
|
-
|
|
319
|
+
waveTexts.push(res.text);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let combined = waveTexts.join('\n');
|
|
323
|
+
if (dryRun && waveDispatch.length > 1) {
|
|
324
|
+
const skipped = [...new Set(
|
|
325
|
+
waveDispatch.slice(1).flatMap((wd) => wd.entries.map((e) => e.displayPath)),
|
|
326
|
+
)];
|
|
327
|
+
combined += `\n(dry_run: only the first sequential group was validated against disk; blocks depending on earlier edits were not simulated: ${skipped.join(', ')})`;
|
|
285
328
|
}
|
|
286
|
-
let combined = resultParts.join('\n');
|
|
287
329
|
const renameLines = formatV4ARenameSuccessLines(v4aRenameResults);
|
|
288
330
|
if (renameLines.length > 0 && !isPatchErrorText(combined)) {
|
|
289
331
|
combined = `${renameLines.join('\n')}\n${combined}`;
|
|
290
332
|
}
|
|
291
333
|
if (!isPatchErrorText(combined) && options?.toolCallId) {
|
|
292
|
-
|
|
334
|
+
const allRewrites = waveDispatch.flatMap((wd) => wd.headerRewrites);
|
|
335
|
+
registerApplyPatchUiDiff(options.toolCallId, rewriteHeaderPaths(normalizedPatchStr, allRewrites));
|
|
293
336
|
}
|
|
294
337
|
if (!isPatchErrorText(combined) && rejectedV4AHunks.length > 0) {
|
|
295
338
|
const tail = [
|
|
@@ -88,42 +88,60 @@ function parsedEntryTargetKey(entry, basePath) {
|
|
|
88
88
|
return process.platform === 'win32' ? fullPath.toLowerCase() : fullPath;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
// Group parsed entries into sequential application "waves". When a file is
|
|
92
|
+
// listed as a modify target N times, occurrence i is placed in wave i, so
|
|
93
|
+
// each duplicate block applies against the on-disk result of the previous
|
|
94
|
+
// one — the native engine re-reads the file per apply() call, giving true
|
|
95
|
+
// sequential semantics ("block 2 against block 1's output"). Unique and
|
|
96
|
+
// non-modify entries always land in wave 0.
|
|
97
|
+
//
|
|
98
|
+
// Genuinely unsupported same-path combos (a create/delete mixed with any
|
|
99
|
+
// other block for that path) cannot be sequenced and throw with guidance to
|
|
100
|
+
// merge hunks into one block or send separate apply_patch calls.
|
|
101
|
+
export function splitParsedModifyWaves(parsed, basePath) {
|
|
102
|
+
const entries = Array.isArray(parsed) ? parsed : [];
|
|
103
|
+
const kindsByPath = new Map();
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const kind = classifyEntry(entry);
|
|
106
|
+
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
107
|
+
if (!headerName || DEV_NULL.test(headerName)) continue;
|
|
108
|
+
const full = resolveEntryPath(basePath, headerName);
|
|
109
|
+
const key = process.platform === 'win32' ? full.toLowerCase() : full;
|
|
110
|
+
const rec = kindsByPath.get(key) || { kinds: [], headerName };
|
|
111
|
+
rec.kinds.push(kind);
|
|
112
|
+
kindsByPath.set(key, rec);
|
|
113
|
+
}
|
|
114
|
+
for (const { kinds, headerName } of kindsByPath.values()) {
|
|
115
|
+
if (kinds.length > 1 && kinds.some((k) => k !== 'modify')) {
|
|
116
|
+
const display = normalizeOutputPath(stripDiffPrefix(headerName));
|
|
117
|
+
throw new Error(
|
|
118
|
+
`apply_patch: unsupported duplicate target ${display} — a create/delete block cannot `
|
|
119
|
+
+ 'be combined with other blocks for the same path. Merge the hunks into one block or '
|
|
120
|
+
+ 'send separate apply_patch calls.',
|
|
121
|
+
);
|
|
106
122
|
}
|
|
107
|
-
existing.hunks.push(...(entry.hunks || []));
|
|
108
|
-
changed = true;
|
|
109
123
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
// Split into contiguous sequential groups that PRESERVE the patch's block
|
|
125
|
+
// listing order: accumulate blocks until a target path would repeat within
|
|
126
|
+
// the current group, then start a new group beginning with that block.
|
|
127
|
+
// Every group holds unique targets (the native batch stays valid) and
|
|
128
|
+
// groups apply in listed order, so the effective apply order — and thus the
|
|
129
|
+
// set of committed blocks at any failure point — equals the on-wire order.
|
|
130
|
+
const groups = [];
|
|
131
|
+
let current = [];
|
|
132
|
+
let currentKeys = new Set();
|
|
133
|
+
for (const entry of entries) {
|
|
118
134
|
const key = parsedEntryTargetKey(entry, basePath);
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
throw new Error(`apply_patch: duplicate target ${display} — patch lists the same path twice.`);
|
|
135
|
+
if (key && currentKeys.has(key)) {
|
|
136
|
+
groups.push(current);
|
|
137
|
+
current = [];
|
|
138
|
+
currentKeys = new Set();
|
|
124
139
|
}
|
|
125
|
-
|
|
140
|
+
current.push(entry);
|
|
141
|
+
if (key) currentKeys.add(key);
|
|
126
142
|
}
|
|
143
|
+
if (current.length > 0) groups.push(current);
|
|
144
|
+
return groups;
|
|
127
145
|
}
|
|
128
146
|
|
|
129
147
|
function headerRelFromBase(basePath, absNorm) {
|
|
@@ -526,10 +526,19 @@ function readRawBufForV4AConversion(fullPath) {
|
|
|
526
526
|
return buf;
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
// win32 filesystems are case-insensitive, so `Foo` and `foo` are the same
|
|
530
|
+
// file: the V4A source-line cache MUST key on this normalized form at every
|
|
531
|
+
// get/set, otherwise a mixed-case duplicate section refreshed under one
|
|
532
|
+
// casing is missed under another and converts against stale/original lines.
|
|
533
|
+
export function v4aLinesCacheKey(fullPath) {
|
|
534
|
+
return process.platform === 'win32' ? String(fullPath).toLowerCase() : String(fullPath);
|
|
535
|
+
}
|
|
536
|
+
|
|
529
537
|
function v4aConversionSourceLines(fullPath, linesCache) {
|
|
530
|
-
|
|
538
|
+
const cacheKey = v4aLinesCacheKey(fullPath);
|
|
539
|
+
if (linesCache.has(cacheKey)) return linesCache.get(cacheKey);
|
|
531
540
|
const lines = splitTextLinesForPatch(readRawBufForV4AConversion(fullPath).toString('utf-8'));
|
|
532
|
-
linesCache.set(
|
|
541
|
+
linesCache.set(cacheKey, lines);
|
|
533
542
|
return lines;
|
|
534
543
|
}
|
|
535
544
|
|
|
@@ -552,6 +561,20 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
552
561
|
const fuzzy = options.fuzzy !== false;
|
|
553
562
|
const out = [];
|
|
554
563
|
const v4aLinesCache = new Map();
|
|
564
|
+
// Paths that appear as update targets more than once: their duplicate
|
|
565
|
+
// sections must be converted against the PRIOR section's result so the
|
|
566
|
+
// emitted unified hunks line up for sequential (wave) application. We
|
|
567
|
+
// refresh v4aLinesCache after each such section below.
|
|
568
|
+
const dupUpdatePaths = new Set();
|
|
569
|
+
{
|
|
570
|
+
const seenUpd = new Set();
|
|
571
|
+
for (const s of sections || []) {
|
|
572
|
+
if (!s || s.kind === 'add' || s.kind === 'delete' || typeof s.path !== 'string' || !s.path) continue;
|
|
573
|
+
const fp = resolveV4AEntryPath(basePath, s.path);
|
|
574
|
+
const key = v4aLinesCacheKey(fp);
|
|
575
|
+
if (seenUpd.has(key)) dupUpdatePaths.add(key); else seenUpd.add(key);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
555
578
|
for (const section of sections) {
|
|
556
579
|
const displayPath = section.path.replace(/\\/g, '/');
|
|
557
580
|
if (section.kind === 'add') {
|
|
@@ -646,6 +669,16 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
646
669
|
out.push(`+++ b/${displayPath}`);
|
|
647
670
|
for (const line of sectionHunks) out.push(line);
|
|
648
671
|
}
|
|
672
|
+
// If this path is edited again later, the next section must resolve
|
|
673
|
+
// against this section's applied result, not the original file — apply
|
|
674
|
+
// these hunks to the cached lines so duplicate V4A blocks convert to a
|
|
675
|
+
// sequentially-appliable unified patch. Best-effort: on any mismatch we
|
|
676
|
+
// keep the original cache and let native wave application surface it.
|
|
677
|
+
if (dupUpdatePaths.has(v4aLinesCacheKey(fullPath))) {
|
|
678
|
+
try {
|
|
679
|
+
v4aLinesCache.set(v4aLinesCacheKey(fullPath), applyV4AHunksToLines(sourceLines, section.hunks, { fuzzy }));
|
|
680
|
+
} catch { /* leave original cached lines */ }
|
|
681
|
+
}
|
|
649
682
|
}
|
|
650
683
|
return out.join('\n') + '\n';
|
|
651
684
|
}
|
|
@@ -37,7 +37,7 @@ import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
|
37
37
|
// transition). shell-jobs.mjs imports stripAnsi from this module, so this is
|
|
38
38
|
// a static cycle — safe because neither binding is touched at module-eval
|
|
39
39
|
// time, only when the respective functions actually run.
|
|
40
|
-
import { adoptForegroundShellJob } from './builtin/shell-jobs.mjs';
|
|
40
|
+
import { adoptForegroundShellJob, killShellJob } from './builtin/shell-jobs.mjs';
|
|
41
41
|
import {
|
|
42
42
|
_maybeEncodePowerShellCommand,
|
|
43
43
|
extractPowerShellCommandInner,
|
|
@@ -494,6 +494,7 @@ export function execShellCommand({
|
|
|
494
494
|
autoBackgroundMs,
|
|
495
495
|
onProgress,
|
|
496
496
|
clientHostPid,
|
|
497
|
+
backgroundOnTimeout,
|
|
497
498
|
}) {
|
|
498
499
|
return new Promise(async (resolve) => {
|
|
499
500
|
const taskId = `shell_${randomUUID().slice(0, 8)}`;
|
|
@@ -723,16 +724,20 @@ export function execShellCommand({
|
|
|
723
724
|
if (grace.unref) grace.unref();
|
|
724
725
|
});
|
|
725
726
|
|
|
726
|
-
// Auto-background transition (CC
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
|
|
727
|
+
// Auto-background transition (CC startBackgrounding analogue). Two triggers
|
|
728
|
+
// resolve the call immediately with a 'backgrounded' result while the
|
|
729
|
+
// child keeps running, adopted into the shell-jobs registry but still
|
|
730
|
+
// owned by this CLI process:
|
|
731
|
+
// 1. the optional autoBackgroundMs soft threshold (MIXDOG_SHELL_AUTO_
|
|
732
|
+
// BACKGROUND_MS opt-in) — an EARLIER promotion before the timeout, and
|
|
733
|
+
// 2. the foreground timeout deadline (backgroundOnTimeout) — the default
|
|
734
|
+
// promote-on-timeout that replaces the old tree-kill.
|
|
735
|
+
// Either way the adopted job runs UNLIMITED (timeoutMs 0, matching the
|
|
736
|
+
// async default): the original foreground timeout no longer bounds it, and
|
|
737
|
+
// the adopted-job cap poll still enforces the 100 MB output ceiling.
|
|
738
|
+
// Mutually exclusive with settle() via the autoBackgrounded flag set
|
|
739
|
+
// synchronously at the top before any await.
|
|
740
|
+
const _autoBackground = async ({ reason = 'threshold' } = {}) => {
|
|
736
741
|
// Win the race: bail if a terminal transition already happened, and
|
|
737
742
|
// claim the transition synchronously so a concurrently-queued settle()
|
|
738
743
|
// (which checks autoBackgrounded) becomes inert.
|
|
@@ -740,16 +745,18 @@ export function execShellCommand({
|
|
|
740
745
|
if (child.exitCode != null || child.signalCode != null) return;
|
|
741
746
|
autoBackgrounded = true;
|
|
742
747
|
// The foreground capture is over; stop the local watchdogs/timers so
|
|
743
|
-
// they cannot treeKill the now-adopted child. The
|
|
744
|
-
//
|
|
748
|
+
// they cannot treeKill the now-adopted child. The adopted job runs
|
|
749
|
+
// unlimited; refreshShellJob only enforces the output cap.
|
|
745
750
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
746
751
|
_clearProgressTimer();
|
|
747
752
|
if (sizeWatchdog) { clearInterval(sizeWatchdog); sizeWatchdog = null; }
|
|
748
753
|
if (autoBgTimer) { clearTimeout(autoBgTimer); autoBgTimer = null; }
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
754
|
+
// Keep the abort handler ATTACHED through the promotion window. A user
|
|
755
|
+
// cancel racing in after promotion starts must still bring the adopted
|
|
756
|
+
// child down — the handler's treeKill(child) does exactly that (settle()
|
|
757
|
+
// is inert once autoBackgrounded, but the kill itself still lands, and
|
|
758
|
+
// refreshShellJob then flags the job failed). We only detach on a real
|
|
759
|
+
// settle() or on the adoption-failure fallback below.
|
|
753
760
|
// Every subsequent stdout/stderr chunk must hit disk — the call is
|
|
754
761
|
// about to resolve and nobody will drain the in-memory buffers again.
|
|
755
762
|
try { taskOutput.forceSpill(); } catch {}
|
|
@@ -766,7 +773,9 @@ export function execShellCommand({
|
|
|
766
773
|
command,
|
|
767
774
|
cwd,
|
|
768
775
|
pid: child.pid,
|
|
769
|
-
|
|
776
|
+
// Unlimited: the promoted job is no longer bounded by the foreground
|
|
777
|
+
// timeout (matches the async omitted-default behavior).
|
|
778
|
+
timeoutMs: 0,
|
|
770
779
|
mergeStderr: false,
|
|
771
780
|
stdoutPath,
|
|
772
781
|
stderrPath,
|
|
@@ -777,6 +786,18 @@ export function execShellCommand({
|
|
|
777
786
|
} catch {
|
|
778
787
|
job = null;
|
|
779
788
|
}
|
|
789
|
+
// Adoption failed AFTER the foreground timers/size-watchdog were already
|
|
790
|
+
// torn down. Do NOT resolve as backgrounded — that would leave the child
|
|
791
|
+
// running unlimited with no task_id and no watcher. Release the claim and
|
|
792
|
+
// fall back to the old kill path so the command never outlives a failed
|
|
793
|
+
// promotion. (The abort handler is still attached, so an in-flight cancel
|
|
794
|
+
// is honored by the kill path too.)
|
|
795
|
+
if (!job) {
|
|
796
|
+
autoBackgrounded = false;
|
|
797
|
+
if (reason === 'timeout') timedOut = true; else killed = true;
|
|
798
|
+
_treeKillForceSettle();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
780
801
|
// Wire the lifecycle: on close, write the exit-code file FIRST then
|
|
781
802
|
// touch donePath STRICTLY AFTER — the exact ordering refreshShellJob()
|
|
782
803
|
// gates completion on (donePath visible ⇒ exit file fully flushed).
|
|
@@ -787,6 +808,14 @@ export function execShellCommand({
|
|
|
787
808
|
try { writeFileSync(job.donePath, ''); } catch {}
|
|
788
809
|
});
|
|
789
810
|
}
|
|
811
|
+
// Adoption committed. If a cancel already fired (before or during the
|
|
812
|
+
// adoption window), bring the now-adopted child down via its jobId so the
|
|
813
|
+
// promotion can't outlive a user abort. Idempotent with the still-attached
|
|
814
|
+
// abort handler's treeKill.
|
|
815
|
+
if (abortSignal && abortSignal.aborted) {
|
|
816
|
+
try { killShellJob(job.jobId); } catch {}
|
|
817
|
+
try { treeKill(child); } catch {}
|
|
818
|
+
}
|
|
790
819
|
// Snapshot the partial output captured so far for the immediate result.
|
|
791
820
|
let stdout = '';
|
|
792
821
|
let stderr = '';
|
|
@@ -795,7 +824,10 @@ export function execShellCommand({
|
|
|
795
824
|
try { stderr = await taskOutput.getStderr(); }
|
|
796
825
|
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
797
826
|
const jobId = job ? job.jobId : null;
|
|
798
|
-
const secs = Math.round(
|
|
827
|
+
const secs = Math.max(0, Math.round((Date.now() - _startMs) / 1000));
|
|
828
|
+
const _verb = reason === 'timeout'
|
|
829
|
+
? `moved to background at timeout after ${secs}s`
|
|
830
|
+
: `auto-backgrounded after ${secs}s`;
|
|
799
831
|
resolve(
|
|
800
832
|
new ExecResult({
|
|
801
833
|
stdout,
|
|
@@ -814,14 +846,31 @@ export function execShellCommand({
|
|
|
814
846
|
backgrounded: true,
|
|
815
847
|
jobId,
|
|
816
848
|
backgroundMessage: jobId
|
|
817
|
-
?
|
|
818
|
-
:
|
|
849
|
+
? `${_verb}; still running — completion will be delivered as a background task notification. Use task with task_id:${jobId} only for manual wait/status/read/cancel.`
|
|
850
|
+
: `${_verb}; still running`,
|
|
819
851
|
}),
|
|
820
852
|
);
|
|
821
853
|
};
|
|
822
854
|
|
|
823
855
|
if (timeoutMs > 0) {
|
|
824
856
|
timer = setTimeout(() => {
|
|
857
|
+
// Promote-on-timeout: if the caller allows backgrounding and the child
|
|
858
|
+
// is still running (not a trailing-`&` detach), adopt it as a tracked
|
|
859
|
+
// background job instead of tree-killing it. Falls through to the old
|
|
860
|
+
// kill path for disallowed/opted-out commands (backgroundOnTimeout
|
|
861
|
+
// false) or when a terminal transition already won the race.
|
|
862
|
+
if (
|
|
863
|
+
backgroundOnTimeout &&
|
|
864
|
+
!_isBackground &&
|
|
865
|
+
!settled &&
|
|
866
|
+
!autoBackgrounded &&
|
|
867
|
+
!killed &&
|
|
868
|
+
child.exitCode == null &&
|
|
869
|
+
child.signalCode == null
|
|
870
|
+
) {
|
|
871
|
+
_autoBackground({ reason: 'timeout' });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
825
874
|
timedOut = true;
|
|
826
875
|
_treeKillForceSettle();
|
|
827
876
|
}, timeoutMs);
|
|
@@ -23,8 +23,9 @@ function isChannelsDegraded() { return _channelsDegraded; }
|
|
|
23
23
|
try {
|
|
24
24
|
process.stderr.on('error', (e) => {
|
|
25
25
|
if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
|
|
26
|
+
// Parent stdio pipe loss stops stderr writes only. It is NOT runtime
|
|
27
|
+
// corruption, so it must not degrade tool dispatch (_channelsDegraded).
|
|
26
28
|
_stderrBroken = true;
|
|
27
|
-
_channelsDegraded = true;
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
} catch {}
|
|
@@ -75,7 +76,8 @@ ${err instanceof Error ? err.stack : ""}
|
|
|
75
76
|
_writeCrashLine(crashLog, msg);
|
|
76
77
|
}
|
|
77
78
|
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
78
|
-
|
|
79
|
+
// EPIPE = a broken pipe (parent stdio/IPC gone), not corrupted runtime
|
|
80
|
+
// state. Stop writing to the dead pipe but keep serving tool calls.
|
|
79
81
|
_stderrBroken = true;
|
|
80
82
|
}
|
|
81
83
|
crashLogging = false;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatToolSurface,
|
|
7
7
|
isExplorerSurface,
|
|
8
8
|
isMemorySurface,
|
|
9
|
+
stripToolPrefix,
|
|
9
10
|
} from "../../shared/tool-surface.mjs";
|
|
10
11
|
import {
|
|
11
12
|
cwdToProjectSlug,
|
|
@@ -726,7 +727,7 @@ ${_bt.trim()}` : _bt.trim();
|
|
|
726
727
|
// The non-set checks are inlined rather than delegated to the imported
|
|
727
728
|
// isHidden, because that helper would re-consult the module-local
|
|
728
729
|
// HIDDEN_TOOLS Set and ignore the OutputForwarder static.
|
|
729
|
-
if (OutputForwarder.HIDDEN_TOOLS.has(name)) return true;
|
|
730
|
+
if (OutputForwarder.HIDDEN_TOOLS.has(name) || OutputForwarder.HIDDEN_TOOLS.has(stripToolPrefix(name))) return true;
|
|
730
731
|
if (name === "reply" || name === "fetch") return true;
|
|
731
732
|
return false;
|
|
732
733
|
};
|