mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readdirSync, readFileSync, statSync, existsSync, mkdirSync, renameSync, unlinkSync, openSync, readSync, closeSync,
|
|
9
9
|
} from 'node:fs';
|
|
10
10
|
import { getPluginData } from '../../config.mjs';
|
|
11
|
-
import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
|
|
11
|
+
import { writeJsonAtomicSync, withFileLockSync } from '../../../../shared/atomic-file.mjs';
|
|
12
12
|
import {
|
|
13
13
|
canonicalGraphCwd as _canonicalGraphCwd,
|
|
14
14
|
registerCodeGraphDrain,
|
|
@@ -264,60 +264,63 @@ export function _persistDiskCodeGraphCacheNow({
|
|
|
264
264
|
writeJson = writeJsonAtomicSync,
|
|
265
265
|
} = {}) {
|
|
266
266
|
try {
|
|
267
|
-
_loadDiskCodeGraphCache();
|
|
268
|
-
_pruneDiskCodeGraphEntries();
|
|
269
267
|
const dir = _codeGraphDiskDir();
|
|
270
268
|
mkdirSync(dir, { recursive: true });
|
|
269
|
+
// Graph BUILD work remains fully parallel. Only the short disk COMMIT is
|
|
270
|
+
// serialized across workers/processes so each writer re-reads and merges
|
|
271
|
+
// the latest manifest while holding one common lock. Per-file/manifest
|
|
272
|
+
// atomic writers run lock:false inside this outer critical section.
|
|
273
|
+
//
|
|
274
|
+
// Main/TUI debounced writes stay try-once (never Atomics.wait on the event
|
|
275
|
+
// loop). Worker strict drains may wait because they run off-thread and must
|
|
276
|
+
// not fail a successful graph build merely because a sibling root finished
|
|
277
|
+
// at the same instant.
|
|
278
|
+
withFileLockSync(join(dir, '.persist.lock'), () => {
|
|
279
|
+
_loadDiskCodeGraphCache();
|
|
280
|
+
_pruneDiskCodeGraphEntries();
|
|
281
|
+
|
|
282
|
+
// Read under the common lock so concurrently completed roots cannot
|
|
283
|
+
// overwrite one another with manifests based on the same stale snapshot.
|
|
284
|
+
let preserved = {};
|
|
285
|
+
try {
|
|
286
|
+
const raw = readFileSync(join(dir, 'manifest.json'), 'utf8');
|
|
287
|
+
const parsed = JSON.parse(raw);
|
|
288
|
+
if (parsed && typeof parsed === 'object') preserved = parsed;
|
|
289
|
+
} catch { /* no existing manifest yet */ }
|
|
290
|
+
|
|
291
|
+
let manifest = { ...preserved };
|
|
292
|
+
const validHashes = new Set();
|
|
293
|
+
for (const [cwd, entry] of _diskCodeGraphCache) {
|
|
294
|
+
const hash = _hashCwd(cwd);
|
|
295
|
+
const file = join(dir, `${hash}.json`);
|
|
296
|
+
writeJson(file, entry, { compact: true, lock: false });
|
|
297
|
+
let bytes = null;
|
|
298
|
+
try { bytes = statSync(file).size; } catch { /* written entry may be unavailable */ }
|
|
299
|
+
manifest[cwd] = {
|
|
300
|
+
hash,
|
|
301
|
+
builtAt: entry.builtAt || Date.now(),
|
|
302
|
+
bytes: Number.isFinite(bytes) ? bytes : undefined,
|
|
303
|
+
maxFiles: Number.isFinite(entry.maxFiles) ? entry.maxFiles : undefined,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const pruned = _pruneCodeGraphManifestForBudget(manifest, dir);
|
|
307
|
+
manifest = pruned.manifest;
|
|
308
|
+
for (const row of pruned.evicted) {
|
|
309
|
+
_diskCodeGraphCache.delete(row.cwd);
|
|
310
|
+
}
|
|
311
|
+
for (const meta of Object.values(manifest)) {
|
|
312
|
+
if (meta && typeof meta === 'object' && meta.hash) validHashes.add(meta.hash);
|
|
313
|
+
}
|
|
271
314
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// per-cwd files just because we don't have them in our in-memory map.
|
|
276
|
-
let preserved = {};
|
|
277
|
-
try {
|
|
278
|
-
const raw = readFileSync(join(dir, 'manifest.json'), 'utf8');
|
|
279
|
-
const parsed = JSON.parse(raw);
|
|
280
|
-
if (parsed && typeof parsed === 'object') preserved = parsed;
|
|
281
|
-
} catch { /* no existing manifest yet */ }
|
|
282
|
-
|
|
283
|
-
let manifest = { ...preserved };
|
|
284
|
-
const validHashes = new Set();
|
|
285
|
-
for (const [cwd, entry] of _diskCodeGraphCache) {
|
|
286
|
-
const hash = _hashCwd(cwd);
|
|
287
|
-
const file = join(dir, `${hash}.json`);
|
|
288
|
-
// Try-once (timeoutMs:0): the debounced flush runs on the lead/TUI main
|
|
289
|
-
// process (codeGraph tool + prewarm). withFileLockSync's Atomics.wait
|
|
290
|
-
// would freeze the renderer on cross-process cache contention. Cache is
|
|
291
|
-
// rebuildable — a busy lock just skips this flush; _scheduleDiskCodeGraphCacheFlush
|
|
292
|
-
// re-schedules on the next build. Exit drain still runs (unref'd timer
|
|
293
|
-
// cancelled + direct call).
|
|
294
|
-
writeJson(file, entry, { compact: true, lock: true, timeoutMs: 0 });
|
|
295
|
-
let bytes = null;
|
|
296
|
-
try { bytes = statSync(file).size; } catch { /* written entry may be unavailable */ }
|
|
297
|
-
manifest[cwd] = {
|
|
298
|
-
hash,
|
|
299
|
-
builtAt: entry.builtAt || Date.now(),
|
|
300
|
-
bytes: Number.isFinite(bytes) ? bytes : undefined,
|
|
301
|
-
maxFiles: Number.isFinite(entry.maxFiles) ? entry.maxFiles : undefined,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
const pruned = _pruneCodeGraphManifestForBudget(manifest, dir);
|
|
305
|
-
manifest = pruned.manifest;
|
|
306
|
-
for (const row of pruned.evicted) {
|
|
307
|
-
_diskCodeGraphCache.delete(row.cwd);
|
|
308
|
-
}
|
|
309
|
-
for (const meta of Object.values(manifest)) {
|
|
310
|
-
if (meta && typeof meta === 'object' && meta.hash) validHashes.add(meta.hash);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const manifestFile = join(dir, 'manifest.json');
|
|
314
|
-
writeJson(manifestFile, manifest, { compact: true, lock: true, timeoutMs: 0 });
|
|
315
|
-
_diskManifest = manifest;
|
|
315
|
+
const manifestFile = join(dir, 'manifest.json');
|
|
316
|
+
writeJson(manifestFile, manifest, { compact: true, lock: false });
|
|
317
|
+
_diskManifest = manifest;
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
// Sweep orphan per-cwd files. validHashes now includes every hash in
|
|
320
|
+
// the merged manifest (preserved + ours) so cross-instance cache files
|
|
321
|
+
// are never collateral damage.
|
|
322
|
+
_sweepCodeGraphCacheDir(dir, validHashes, { sweepJson: true });
|
|
323
|
+
}, { timeoutMs: strict ? 30_000 : 0 });
|
|
321
324
|
} catch (err) {
|
|
322
325
|
process.stderr.write(`[code-graph] disk cache persist failed (target: ${_codeGraphDiskDir()}): ${err?.message || err}\n`);
|
|
323
326
|
if (strict) throw err;
|
|
@@ -62,6 +62,15 @@ const ROOT_FEDERATED_MODES = new Set([
|
|
|
62
62
|
'references', 'callers', 'callees', 'symbols', 'prewarm',
|
|
63
63
|
]);
|
|
64
64
|
|
|
65
|
+
export async function _runCodeGraphFederation(roots, runOne, projectArgs) {
|
|
66
|
+
return Promise.all((roots || []).map(async (root) => {
|
|
67
|
+
let body;
|
|
68
|
+
try { body = await runOne(root, projectArgs); }
|
|
69
|
+
catch (err) { body = `Error: ${err?.message || String(err)}`; }
|
|
70
|
+
return `# project ${formatFederatedProjectLabel(root)}\n${body}`;
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
// Absorb: file/files arriving as a JSON-stringified array
|
|
66
75
|
// (file:"[\"a.mjs\",\"b.mjs\"]") — parse to a real array so the graph lookup
|
|
67
76
|
// batches per file instead of treating the JSON text as one (missing) path.
|
|
@@ -649,8 +658,7 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
|
|
|
649
658
|
}
|
|
650
659
|
const projectArgs = { ...args };
|
|
651
660
|
delete projectArgs.cwd;
|
|
652
|
-
const sections =
|
|
653
|
-
for (const { file, abs, root } of routed) {
|
|
661
|
+
const sections = await Promise.all(routed.map(async ({ file, abs, root }) => {
|
|
654
662
|
const excludedProjectRoots = trustedRoots.filter((candidate) => candidate !== root
|
|
655
663
|
&& owningTrustedCodeGraphRoot(candidate, [root]) === root);
|
|
656
664
|
let body;
|
|
@@ -665,8 +673,8 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
|
|
|
665
673
|
} catch (err) {
|
|
666
674
|
body = `Error: ${err?.message || String(err)}`;
|
|
667
675
|
}
|
|
668
|
-
|
|
669
|
-
}
|
|
676
|
+
return `# ${rawMode} ${file}\n# project ${formatFederatedProjectLabel(root)}\n${body}`;
|
|
677
|
+
}));
|
|
670
678
|
return sections.join('\n\n');
|
|
671
679
|
}
|
|
672
680
|
if (trustedRoots.length && canFederate) {
|
|
@@ -687,17 +695,8 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
|
|
|
687
695
|
&& owningTrustedCodeGraphRoot(candidate, [root]) === root),
|
|
688
696
|
},
|
|
689
697
|
);
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const sections = [];
|
|
693
|
-
for (const root of trustedRoots) {
|
|
694
|
-
let body;
|
|
695
|
-
try { body = await runOne(root, projectArgs); }
|
|
696
|
-
catch (err) { body = `Error: ${err?.message || String(err)}`; }
|
|
697
|
-
sections.push(`# project ${formatFederatedProjectLabel(root)}\n${body}`);
|
|
698
|
-
}
|
|
699
|
-
return sections.join('\n\n');
|
|
700
|
-
})();
|
|
698
|
+
const federationWork = _runCodeGraphFederation(trustedRoots, runOne, projectArgs)
|
|
699
|
+
.then((sections) => sections.join('\n\n'));
|
|
701
700
|
if (federationWork) {
|
|
702
701
|
if (!signal) return federationWork;
|
|
703
702
|
let onAbort = null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// verbatim from patch.mjs; native protocol, cache/snapshot side effects, and
|
|
3
3
|
// output formatting are unchanged.
|
|
4
4
|
|
|
5
|
-
import { readFileSync, lstatSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { unlink } from 'node:fs/promises';
|
|
7
7
|
import { dirname as pathDirname } from 'node:path';
|
|
8
8
|
import { performance } from 'node:perf_hooks';
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
isResolvedPathOutsideBase,
|
|
24
24
|
countHunkChanges,
|
|
25
25
|
} from './paths.mjs';
|
|
26
|
-
import { applyV4AHunksToLines } from './v4a-convert.mjs';
|
|
27
26
|
import {
|
|
28
27
|
getNativePatchServer,
|
|
29
28
|
scheduleNativePatchIdleClose,
|
|
@@ -35,10 +34,14 @@ import {
|
|
|
35
34
|
extractNativeFailurePath,
|
|
36
35
|
nativeFailureMatchesEntry,
|
|
37
36
|
findFirstFailingUnifiedHunk,
|
|
37
|
+
findUnifiedHunkMatch,
|
|
38
|
+
collectUnifiedNewLines,
|
|
39
|
+
assertSafeReplacementPlan,
|
|
38
40
|
firstFailingUnifiedHunkLineDetail,
|
|
39
41
|
firstMeaningfulUnifiedHunkLine,
|
|
40
42
|
firstMeaningfulUnifiedEntryLine,
|
|
41
43
|
compactPatchPreviewLine,
|
|
44
|
+
formatPatchSourceExcerpt,
|
|
42
45
|
nearestPatchLineHint,
|
|
43
46
|
normalizeTypographic,
|
|
44
47
|
splitBufferLinesForPatch,
|
|
@@ -187,8 +190,110 @@ function entryPathKey(fullPath) {
|
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
function joinTextLinesForPatch(lines) {
|
|
190
|
-
const
|
|
191
|
-
|
|
193
|
+
const eol = lines?.eol || '\n';
|
|
194
|
+
const body = (lines || []).join(eol);
|
|
195
|
+
return lines?.hasFinalNewline !== false ? `${body}${eol}` : body;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The file's dominant line terminator. splitTextLinesForPatch() folds CRLF into
|
|
199
|
+
// LF, so without this an out-of-base CRLF file would be rewritten as LF.
|
|
200
|
+
function detectDominantEol(text) {
|
|
201
|
+
const body = String(text ?? '');
|
|
202
|
+
let lf = 0;
|
|
203
|
+
let crlf = 0;
|
|
204
|
+
for (let i = 0; i < body.length; i++) {
|
|
205
|
+
if (body[i] !== '\n') continue;
|
|
206
|
+
lf++;
|
|
207
|
+
if (i > 0 && body[i - 1] === '\r') crlf++;
|
|
208
|
+
}
|
|
209
|
+
return lf > 0 && crlf * 2 >= lf ? '\r\n' : '\n';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloneTextLinesForPatch(sourceLines, eol) {
|
|
213
|
+
const lines = [...(sourceLines || [])];
|
|
214
|
+
lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
|
|
215
|
+
lines.eol = eol || sourceLines?.eol || '\n';
|
|
216
|
+
return lines;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Which side of the hunk, if any, carries an explicit
|
|
220
|
+
// `` marker. Both transitions must be honoured:
|
|
221
|
+
// a marker on the NEW side drops the terminator, a marker on the OLD side
|
|
222
|
+
// only (patch re-adds it) restores one.
|
|
223
|
+
function unifiedHunkEofIntent(hunk) {
|
|
224
|
+
let lastSide = null;
|
|
225
|
+
let oldNoNewline = false;
|
|
226
|
+
let newNoNewline = false;
|
|
227
|
+
for (const raw of hunk?.lines || []) {
|
|
228
|
+
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
229
|
+
const tag = raw[0];
|
|
230
|
+
if (tag === '\\') {
|
|
231
|
+
if (lastSide === 'context') { oldNoNewline = true; newNoNewline = true; }
|
|
232
|
+
else if (lastSide === 'old') oldNoNewline = true;
|
|
233
|
+
else if (lastSide === 'new') newNoNewline = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (tag === ' ') lastSide = 'context';
|
|
237
|
+
else if (tag === '-') lastSide = 'old';
|
|
238
|
+
else if (tag === '+') lastSide = 'new';
|
|
239
|
+
else lastSide = null;
|
|
240
|
+
}
|
|
241
|
+
return { oldNoNewline, newNoNewline };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Same recovery shape the native failure path emits: expected line, nearest
|
|
245
|
+
// candidate, and a bounded verbatim excerpt of the current file.
|
|
246
|
+
function unifiedHunkMissMessage(displayPath, hunk, sourceLines) {
|
|
247
|
+
const detail = firstFailingUnifiedHunkLineDetail(sourceLines, hunk)
|
|
248
|
+
|| firstMeaningfulUnifiedHunkLine(hunk);
|
|
249
|
+
const expected = detail?.line || '';
|
|
250
|
+
const preferred = Number.isFinite(detail?.preferredLine) ? detail.preferredLine : 0;
|
|
251
|
+
const nearest = expected ? nearestPatchLineHint(sourceLines, expected, preferred) : '';
|
|
252
|
+
const excerpt = formatPatchSourceExcerpt(sourceLines, preferred, (hunk?.lines || []).length);
|
|
253
|
+
return `apply_patch: hunk context not found in ${displayPath}`
|
|
254
|
+
+ (expected ? `; expected first old/context line: ${JSON.stringify(compactPatchPreviewLine(expected))}` : '')
|
|
255
|
+
+ (nearest ? `; ${nearest}` : '')
|
|
256
|
+
+ '; use exact current lines, no stubs.'
|
|
257
|
+
+ excerpt;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Apply already-parsed UNIFIED hunks with their declared coordinates: each
|
|
261
|
+
// hunk is first tried at its own `@@` position and only then fuzz-matched
|
|
262
|
+
// forward, mirroring the native engine. Insert-only hunks (`-N,0`) land at
|
|
263
|
+
// their declared index (end-of-file for a converted V4A addition) and never
|
|
264
|
+
// advance the cursor, so a later hunk can still resolve before them. The plan
|
|
265
|
+
// is validated for overlaps/equal starts BEFORE any mutation, then applied in
|
|
266
|
+
// descending order so earlier positions stay valid.
|
|
267
|
+
function applyUnifiedHunksToLines(sourceLines, hunks, { fuzz, displayPath, eol }) {
|
|
268
|
+
const lines = cloneTextLinesForPatch(sourceLines, eol);
|
|
269
|
+
const replacements = [];
|
|
270
|
+
let minStartIdx = 0;
|
|
271
|
+
for (const hunk of hunks || []) {
|
|
272
|
+
const match = findUnifiedHunkMatch(lines, hunk, minStartIdx, fuzz);
|
|
273
|
+
if (!match) throw new Error(unifiedHunkMissMessage(displayPath, hunk, lines));
|
|
274
|
+
const { newLines } = collectUnifiedNewLines(hunk);
|
|
275
|
+
const intent = unifiedHunkEofIntent(hunk);
|
|
276
|
+
replacements.push({
|
|
277
|
+
start: match.start,
|
|
278
|
+
oldLen: match.end - match.start,
|
|
279
|
+
newLines,
|
|
280
|
+
intent,
|
|
281
|
+
atEof: match.end >= lines.length,
|
|
282
|
+
});
|
|
283
|
+
minStartIdx = Math.max(minStartIdx, match.end);
|
|
284
|
+
}
|
|
285
|
+
replacements.sort((a, b) => a.start - b.start);
|
|
286
|
+
assertSafeReplacementPlan(replacements, `apply_patch: ${displayPath}`);
|
|
287
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
288
|
+
const rep = replacements[i];
|
|
289
|
+
lines.splice(rep.start, rep.oldLen, ...rep.newLines);
|
|
290
|
+
// Explicit markers win in BOTH directions; a hunk that is silent about the
|
|
291
|
+
// terminator leaves the file's existing state untouched (native parity).
|
|
292
|
+
if (!rep.atEof) continue;
|
|
293
|
+
if (rep.intent.newNoNewline) lines.hasFinalNewline = false;
|
|
294
|
+
else if (rep.intent.oldNoNewline) lines.hasFinalNewline = true;
|
|
295
|
+
}
|
|
296
|
+
return lines;
|
|
192
297
|
}
|
|
193
298
|
|
|
194
299
|
function lstatRegularPatchFile(fullPath, displayPath) {
|
|
@@ -201,6 +306,18 @@ function lstatRegularPatchFile(fullPath, displayPath) {
|
|
|
201
306
|
return st;
|
|
202
307
|
}
|
|
203
308
|
|
|
309
|
+
// Existence probe for an Add File target: every existing path shape is
|
|
310
|
+
// refused, matching the native in-base guard (`create target already exists`).
|
|
311
|
+
function assertCreateTargetAbsent(fullPath, displayPath) {
|
|
312
|
+
try {
|
|
313
|
+
lstatSync(fullPath);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
if (err?.code === 'ENOENT') return;
|
|
316
|
+
throw new Error(`apply_patch: create target unreadable: ${normalizeOutputPath(displayPath)} (${err?.code || err?.message || String(err)})`);
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`apply_patch: create target already exists: ${normalizeOutputPath(displayPath)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
204
321
|
function findParsedForRow(row, parsed, basePath) {
|
|
205
322
|
const key = entryPathKey(row.fullPath);
|
|
206
323
|
for (const entry of parsed || []) {
|
|
@@ -219,6 +336,9 @@ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateSco
|
|
|
219
336
|
}
|
|
220
337
|
|
|
221
338
|
if (kind === 'create') {
|
|
339
|
+
// Refuse before any write, in dry runs too, so an Add File section can
|
|
340
|
+
// never clobber (or follow) an existing out-of-base path.
|
|
341
|
+
assertCreateTargetAbsent(fullPath, displayPath);
|
|
222
342
|
const addedLines = [];
|
|
223
343
|
for (const hunk of entry.hunks || []) {
|
|
224
344
|
for (const line of hunk.lines || []) {
|
|
@@ -228,7 +348,7 @@ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateSco
|
|
|
228
348
|
const content = joinTextLinesForPatch(addedLines);
|
|
229
349
|
if (!dryRun) {
|
|
230
350
|
mkdirSync(pathDirname(fullPath), { recursive: true });
|
|
231
|
-
|
|
351
|
+
writeFileSync(fullPath, content);
|
|
232
352
|
invalidateBuiltinResultCache([fullPath]);
|
|
233
353
|
markCodeGraphDirtyPaths([fullPath]);
|
|
234
354
|
recordReadSnapshotForPath(fullPath, readStateScope, { source: 'apply_patch_js', isPartialView: false });
|
|
@@ -251,9 +371,13 @@ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateSco
|
|
|
251
371
|
|
|
252
372
|
lstatRegularPatchFile(fullPath, displayPath);
|
|
253
373
|
const raw = readFileSync(fullPath);
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const updatedLines =
|
|
374
|
+
const rawText = raw.toString('utf8');
|
|
375
|
+
const sourceLines = splitTextLinesForPatch(rawText);
|
|
376
|
+
const updatedLines = applyUnifiedHunksToLines(sourceLines, entry.hunks || [], {
|
|
377
|
+
fuzz: fuzzy ? 2 : 0,
|
|
378
|
+
displayPath,
|
|
379
|
+
eol: detectDominantEol(rawText),
|
|
380
|
+
});
|
|
257
381
|
const content = joinTextLinesForPatch(updatedLines);
|
|
258
382
|
if (!dryRun) {
|
|
259
383
|
await atomicWrite(fullPath, content, { sessionId: readStateScope });
|
|
@@ -39,6 +39,62 @@ export function collectUnifiedOps(hunk) {
|
|
|
39
39
|
return ops;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// New-side lines of a unified hunk (context + additions), plus whether the
|
|
43
|
+
// hunk marks the NEW content as lacking a terminating newline
|
|
44
|
+
// (`` directly after a ' ' / '+' line).
|
|
45
|
+
export function collectUnifiedNewLines(hunk) {
|
|
46
|
+
const newLines = [];
|
|
47
|
+
let lastWasNew = false;
|
|
48
|
+
let noFinalNewline = false;
|
|
49
|
+
for (const raw of hunk?.lines || []) {
|
|
50
|
+
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
51
|
+
const tag = raw[0];
|
|
52
|
+
if (tag === ' ' || tag === '+') {
|
|
53
|
+
newLines.push(raw.slice(1));
|
|
54
|
+
lastWasNew = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (tag === '\\') {
|
|
58
|
+
if (lastWasNew) noFinalNewline = true;
|
|
59
|
+
lastWasNew = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
lastWasNew = false;
|
|
63
|
+
}
|
|
64
|
+
return { newLines, noFinalNewline };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Guard a replacement plan before it is applied in DESCENDING order (the
|
|
68
|
+
// Codex `apply_replacements` shape). The plan must already be sorted
|
|
69
|
+
// ascending by `start`. Descending application is only content-safe when the
|
|
70
|
+
// replaced ranges are disjoint; the single well-defined exception is several
|
|
71
|
+
// zero-length insertions at one index, which descending application emits in
|
|
72
|
+
// their original source order. Everything else (an overlap, or an insertion
|
|
73
|
+
// sharing an index with a replacement) would silently truncate or reorder
|
|
74
|
+
// content, so it is rejected before any write.
|
|
75
|
+
export function assertSafeReplacementPlan(replacements, label = 'apply_patch') {
|
|
76
|
+
const plan = Array.isArray(replacements) ? replacements : [];
|
|
77
|
+
for (let i = 1; i < plan.length; i++) {
|
|
78
|
+
const prev = plan[i - 1];
|
|
79
|
+
const cur = plan[i];
|
|
80
|
+
const prevEnd = prev.start + prev.oldLen;
|
|
81
|
+
if (cur.start === prev.start) {
|
|
82
|
+
if (prev.oldLen === 0 && cur.oldLen === 0) continue;
|
|
83
|
+
throw new Error(
|
|
84
|
+
`${label}: two hunks resolve to the same position (line ${cur.start + 1}) and at least one of them replaces existing lines; `
|
|
85
|
+
+ 'widen their context or split them into separate patches.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (cur.start < prevEnd) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${label}: hunks overlap — line ${cur.start + 1} falls inside lines ${prev.start + 1}-${prevEnd} already replaced by an earlier hunk; `
|
|
91
|
+
+ 'widen their context or split them into separate patches.',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return plan;
|
|
96
|
+
}
|
|
97
|
+
|
|
42
98
|
export function computeUnifiedChangeBand(ops) {
|
|
43
99
|
let first = null;
|
|
44
100
|
let last = null;
|
|
@@ -143,7 +199,12 @@ export function unifiedOldLinesMatchAt(sourceLines, oldLines, startIdx, fuzz, ba
|
|
|
143
199
|
const expectedBytes = toLineBytes(expected.line);
|
|
144
200
|
const expectedNL = expected.hasNewline !== false;
|
|
145
201
|
const actualNL = !(actualIdx === lastSrcIdx && !srcFinalNewline);
|
|
146
|
-
|
|
202
|
+
// Mirrors the native `newline_flags_compatible`: a hunk that ends on the
|
|
203
|
+
// file's LAST line encodes it with the default "has newline" unless the
|
|
204
|
+
// patch carries a `` marker, so that single
|
|
205
|
+
// EOF mismatch is tolerated for the hunk's last old line only.
|
|
206
|
+
const newlineOk = expectedNL === actualNL
|
|
207
|
+
|| (offset + 1 === oldLines.length && actualIdx === lastSrcIdx && expectedNL && !actualNL);
|
|
147
208
|
if (newlineOk && actualBytes.equals(expectedBytes)) continue;
|
|
148
209
|
if (newlineOk && fuzz > 0 && (expected.tag === ' ' || expected.tag === '-') && byteTrimPatchWhitespace(actualBytes).equals(byteTrimPatchWhitespace(expectedBytes))) continue;
|
|
149
210
|
if (
|
|
@@ -176,12 +237,15 @@ export function unifiedOldLinesMatchAt(sourceLines, oldLines, startIdx, fuzz, ba
|
|
|
176
237
|
return { fuzzUsed, normCount };
|
|
177
238
|
}
|
|
178
239
|
|
|
179
|
-
function findUnifiedHunkMatch(sourceLines, hunk, minStartIdx, fuzz) {
|
|
240
|
+
export function findUnifiedHunkMatch(sourceLines, hunk, minStartIdx, fuzz) {
|
|
180
241
|
const oldLines = collectUnifiedOldLines(hunk);
|
|
181
242
|
const band = computeUnifiedChangeBand(collectUnifiedOps(hunk));
|
|
182
243
|
const oldStart = Math.max(0, (Number(hunk?.oldStart) || 1) - 1);
|
|
183
244
|
if (oldLines.length === 0) {
|
|
184
|
-
|
|
245
|
+
// jsdiff bumps `oldStart` by one for a zero-old-count hunk (the unified
|
|
246
|
+
// "insert after line N" quirk), so the 0-based insertion index is
|
|
247
|
+
// `oldStart - 1`.
|
|
248
|
+
const insertIdx = Math.max(0, (Number(hunk?.oldStart) || 0) - 1);
|
|
185
249
|
return insertIdx >= minStartIdx && insertIdx <= sourceLines.length ? { start: insertIdx, end: insertIdx } : null;
|
|
186
250
|
}
|
|
187
251
|
if (oldStart >= minStartIdx && unifiedOldLinesMatchAt(sourceLines, oldLines, oldStart, 0, band) !== null) {
|
|
@@ -375,6 +439,25 @@ export function findLineSequence(lines, needle, fromLine, preferredLine = 0, opt
|
|
|
375
439
|
return starts[0];
|
|
376
440
|
}
|
|
377
441
|
}
|
|
442
|
+
// Whole-file unique-verbatim fallback: an @@ anchor that matched a LATER
|
|
443
|
+
// occurrence, or a previous hunk that advanced the cursor, leaves the
|
|
444
|
+
// forward-only search blind to a block that exists verbatim EARLIER in the
|
|
445
|
+
// file (measured: the majority of real context-miss failures). Accept that
|
|
446
|
+
// block ONLY when it matches exactly once in the entire file — zero
|
|
447
|
+
// ambiguity about where it belongs — and never for `*** End of File` hunks.
|
|
448
|
+
// assertSafeReplacementPlan still rejects any overlap with another hunk's
|
|
449
|
+
// replacement, so a rewound match can not silently collide.
|
|
450
|
+
if (fuzzy && !eof && minStart > 0) {
|
|
451
|
+
const starts = [];
|
|
452
|
+
for (let i = 0; i + needle.length <= lines.length && starts.length < 2; i++) {
|
|
453
|
+
let ok = true;
|
|
454
|
+
for (let k = 0; k < needle.length; k++) {
|
|
455
|
+
if (lines[i + k] !== needle[k]) { ok = false; break; }
|
|
456
|
+
}
|
|
457
|
+
if (ok) starts.push(i);
|
|
458
|
+
}
|
|
459
|
+
if (starts.length === 1) return starts[0];
|
|
460
|
+
}
|
|
378
461
|
if (fuzzy && needle.length === 1) {
|
|
379
462
|
const want = String(needle[0] ?? '').replace(/\s+/g, ' ').trim();
|
|
380
463
|
if (want.length >= 40) {
|
|
@@ -17,7 +17,12 @@ import { getPluginData } from '../../config.mjs';
|
|
|
17
17
|
import { ensurePatchBinary, findCachedPatchBinary } from '../patch-binary-fetcher.mjs';
|
|
18
18
|
|
|
19
19
|
const PLUGIN_ROOT = process.env.MIXDOG_ROOT
|
|
20
|
-
|
|
20
|
+
// This module lives at src/runtime/agent/orchestrator/tools/patch/, so the
|
|
21
|
+
// repo root is SIX levels up. Five levels stopped at src/ and made the
|
|
22
|
+
// documented "local cargo build first" rule dead code: the local
|
|
23
|
+
// native/mixdog-patch/target/release build was never found and a stale
|
|
24
|
+
// cached prebuilt was used instead, so native fixes could not be exercised.
|
|
25
|
+
|| pathResolve(pathDirname(fileURLToPath(import.meta.url)), '../../../../../..');
|
|
21
26
|
const NATIVE_PATCH_DEFAULT_BIN = pathJoin(
|
|
22
27
|
PLUGIN_ROOT,
|
|
23
28
|
'native/mixdog-patch/target/release',
|