mixdog 0.9.94 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -34
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +53 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +148 -15
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +15 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1534 -9801
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +76 -3
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +81 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +8 -1
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +40 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
shellJobEnforcedPath,
|
|
30
30
|
shellJobGuardianReceiptPath,
|
|
31
31
|
resolveJobOwnerHostPid,
|
|
32
|
+
resolveJobOwnerSessionId,
|
|
32
33
|
trimShellJobSpill,
|
|
33
34
|
writeShellJobDetail,
|
|
34
35
|
readShellJobDetail,
|
|
@@ -563,10 +564,24 @@ export function endShellJobWait(jobId) {
|
|
|
563
564
|
export function clearShellJobNotifyCtx(jobId) {
|
|
564
565
|
jobNotifyCtxByJobId.delete(jobId);
|
|
565
566
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
567
|
+
// scope: reap only the jobs a single session owns (`{ ownerSessionId }`).
|
|
568
|
+
// Omitted => the process-wide sweep, which is correct ONLY on a real process
|
|
569
|
+
// exit: the registries here are process-global, so an unscoped sweep run by
|
|
570
|
+
// one engine's dispose killed other sessions' live jobs (user report).
|
|
571
|
+
// A scope without an owner id matches nothing — never everything.
|
|
572
|
+
export function shutdownShellJobs(reason = 'runtime-close', { sync = false, scope = null } = {}) {
|
|
573
|
+
const scoped = Boolean(scope);
|
|
574
|
+
const scopeSessionId = scoped ? String(scope.ownerSessionId || '') : '';
|
|
575
|
+
if (scoped && !scopeSessionId) return { killed: 0, cancelledJobs: 0, cancelledWatchers: 0 };
|
|
576
|
+
const ownsJob = (jobId) => {
|
|
577
|
+
if (!scoped) return true;
|
|
578
|
+
if (!jobId) return false;
|
|
579
|
+
try { return String(readShellJobDetail(jobId)?.ownerSessionId || '') === scopeSessionId; }
|
|
580
|
+
catch { return false; }
|
|
581
|
+
};
|
|
582
|
+
const livePids = [..._liveJobPids].filter((pid) => ownsJob(_liveJobIdsByPid.get(pid)));
|
|
583
|
+
const jobIds = new Set([..._liveJobIdsByPid.values()].filter((jobId) => jobId && ownsJob(jobId)));
|
|
584
|
+
const watcherJobIds = [...backgroundShellJobWatchers.keys()].filter((jobId) => ownsJob(jobId));
|
|
570
585
|
for (const jobId of watcherJobIds) {
|
|
571
586
|
jobIds.add(jobId);
|
|
572
587
|
try { cancelBackgroundShellJobWatch(jobId); } catch { /* ignore */ }
|
|
@@ -576,6 +591,18 @@ export function shutdownShellJobs(reason = 'runtime-close', { sync = false } = {
|
|
|
576
591
|
for (const jobId of jobIds) {
|
|
577
592
|
try { if (markShellJobCancelledByShutdown(jobId, reason)) marked += 1; } catch { /* ignore */ }
|
|
578
593
|
}
|
|
594
|
+
if (scoped) {
|
|
595
|
+
// Per-job teardown only: another session's watchers, safety pollers and
|
|
596
|
+
// notify contexts must survive this dispose so its completions still fire.
|
|
597
|
+
for (const jobId of jobIds) {
|
|
598
|
+
unpersistedTerminalJobs.delete(jobId);
|
|
599
|
+
backgroundShellJobWatchers.delete(jobId);
|
|
600
|
+
clearShellTaskSafetyNet(jobId);
|
|
601
|
+
jobNotifyCtxByJobId.delete(jobId);
|
|
602
|
+
jobWaitWaiterCountByJobId.delete(jobId);
|
|
603
|
+
}
|
|
604
|
+
return { killed: livePids.length, cancelledJobs: marked, cancelledWatchers: watcherJobIds.length };
|
|
605
|
+
}
|
|
579
606
|
unpersistedTerminalJobs.clear();
|
|
580
607
|
backgroundShellJobWatchers.clear();
|
|
581
608
|
for (const jobId of [...shellTaskSafetyPollers.keys()]) clearShellTaskSafetyNet(jobId);
|
|
@@ -780,7 +807,7 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
780
807
|
// depends on holds for adopted jobs exactly as it does for staged wrappers.
|
|
781
808
|
// This function only allocates the jobId/paths and writes the initial
|
|
782
809
|
// 'running' detail.
|
|
783
|
-
export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStderr, stdoutPath, stderrPath, clientHostPid }) {
|
|
810
|
+
export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStderr, stdoutPath, stderrPath, clientHostPid, ownerSessionId }) {
|
|
784
811
|
const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
785
812
|
const exitPath = shellJobExitPath(jobId);
|
|
786
813
|
const donePath = shellJobDonePath(jobId);
|
|
@@ -805,6 +832,9 @@ export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStd
|
|
|
805
832
|
// dispatching terminal's claude.exe pid (resolveJobOwnerHostPid falls
|
|
806
833
|
// back to the single-client env only when unset).
|
|
807
834
|
ownerHostPid: resolveJobOwnerHostPid(clientHostPid),
|
|
835
|
+
// Dispatching session (see resolveJobOwnerSessionId): a pane counts
|
|
836
|
+
// only the jobs its own session started.
|
|
837
|
+
ownerSessionId: resolveJobOwnerSessionId(ownerSessionId),
|
|
808
838
|
startedAt: new Date().toISOString(),
|
|
809
839
|
};
|
|
810
840
|
writeShellJobDetail(detail);
|
|
@@ -14,6 +14,9 @@ const readFilesByScope = new Map(); // scope → Map(fullPath → { mtimeMs, siz
|
|
|
14
14
|
// apply_patch call may reuse a path correction proven by a read in the live
|
|
15
15
|
// session, but must never inherit a guessed redirect from another process.
|
|
16
16
|
const readRedirectsByScope = new Map(); // scope → Map(requestedPathKey → targetPath)
|
|
17
|
+
export const READ_SNAPSHOT_SCOPE_CACHE_LIMIT = 32;
|
|
18
|
+
export const READ_SNAPSHOT_FILES_PER_SCOPE_LIMIT = 1024;
|
|
19
|
+
export const READ_SNAPSHOT_REDIRECTS_PER_SCOPE_LIMIT = 256;
|
|
17
20
|
|
|
18
21
|
function snapshotPathKey(fullPath) {
|
|
19
22
|
const value = String(fullPath || '');
|
|
@@ -166,6 +169,69 @@ function persistScopeSync(scopeKey, { exitDrain = false } = {}) {
|
|
|
166
169
|
} catch {}
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
export function releaseReadSnapshotScope(scope, {
|
|
173
|
+
deletePersisted = false,
|
|
174
|
+
persist = !deletePersisted,
|
|
175
|
+
} = {}) {
|
|
176
|
+
const scopeKey = readScopeKey(scope);
|
|
177
|
+
if (scopeKey === null) return false;
|
|
178
|
+
const timer = persistPending.get(scopeKey);
|
|
179
|
+
if (timer) {
|
|
180
|
+
try { clearTimeout(timer); } catch {}
|
|
181
|
+
persistPending.delete(scopeKey);
|
|
182
|
+
}
|
|
183
|
+
if (persist && readFilesByScope.has(scopeKey)) {
|
|
184
|
+
try { persistScopeSync(scopeKey, { exitDrain: true }); } catch {}
|
|
185
|
+
}
|
|
186
|
+
const released = readFilesByScope.delete(scopeKey)
|
|
187
|
+
|| readRedirectsByScope.has(scopeKey)
|
|
188
|
+
|| scopeHydrated.has(scopeKey);
|
|
189
|
+
readRedirectsByScope.delete(scopeKey);
|
|
190
|
+
scopeHydrated.delete(scopeKey);
|
|
191
|
+
if (deletePersisted) {
|
|
192
|
+
const path = snapshotScopeFilePath(scopeKey);
|
|
193
|
+
try { if (path && existsSync(path)) unlinkSync(path); } catch {}
|
|
194
|
+
}
|
|
195
|
+
return released;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pruneReadSnapshotScopes(protectedScopeKey) {
|
|
199
|
+
while (readFilesByScope.size > READ_SNAPSHOT_SCOPE_CACHE_LIMIT) {
|
|
200
|
+
let oldest = readFilesByScope.keys().next().value;
|
|
201
|
+
if (oldest === protectedScopeKey) {
|
|
202
|
+
oldest = [...readFilesByScope.keys()].find((key) => key !== protectedScopeKey);
|
|
203
|
+
}
|
|
204
|
+
if (oldest === undefined) break;
|
|
205
|
+
releaseReadSnapshotScope(oldest);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function dropRedirectsToPath(scopeKey, fullPath) {
|
|
210
|
+
const redirects = readRedirectsByScope.get(scopeKey);
|
|
211
|
+
if (!redirects) return;
|
|
212
|
+
const droppedKey = snapshotPathKey(fullPath);
|
|
213
|
+
for (const [requestedKey, targetPath] of redirects) {
|
|
214
|
+
if (requestedKey === droppedKey || snapshotPathKey(targetPath) === droppedKey) {
|
|
215
|
+
redirects.delete(requestedKey);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (redirects.size === 0) readRedirectsByScope.delete(scopeKey);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function rememberReadSnapshot(fullPath, snapshot, scope, knownReadFiles = null) {
|
|
222
|
+
const scopeKey = readScopeKey(scope);
|
|
223
|
+
const readFiles = knownReadFiles || readFilesForScope(scope);
|
|
224
|
+
readFiles.delete(fullPath);
|
|
225
|
+
readFiles.set(fullPath, snapshot);
|
|
226
|
+
while (readFiles.size > READ_SNAPSHOT_FILES_PER_SCOPE_LIMIT) {
|
|
227
|
+
const oldest = readFiles.keys().next().value;
|
|
228
|
+
if (oldest === undefined) break;
|
|
229
|
+
readFiles.delete(oldest);
|
|
230
|
+
if (scopeKey !== null) dropRedirectsToPath(scopeKey, oldest);
|
|
231
|
+
}
|
|
232
|
+
return readFiles;
|
|
233
|
+
}
|
|
234
|
+
|
|
169
235
|
export function deleteReadSnapshotPathEverywhere(fullPath) {
|
|
170
236
|
for (const [scopeKey, readFiles] of readFilesByScope.entries()) {
|
|
171
237
|
if (readFiles.delete(fullPath)) scheduleScopePersist(scopeKey);
|
|
@@ -241,7 +307,13 @@ export function recordReadPathRedirect(requestedFullPath, targetFullPath, scope)
|
|
|
241
307
|
redirects = new Map();
|
|
242
308
|
readRedirectsByScope.set(scopeKey, redirects);
|
|
243
309
|
}
|
|
310
|
+
redirects.delete(snapshotPathKey(requested));
|
|
244
311
|
redirects.set(snapshotPathKey(requested), target);
|
|
312
|
+
while (redirects.size > READ_SNAPSHOT_REDIRECTS_PER_SCOPE_LIMIT) {
|
|
313
|
+
const oldest = redirects.keys().next().value;
|
|
314
|
+
if (oldest === undefined) break;
|
|
315
|
+
redirects.delete(oldest);
|
|
316
|
+
}
|
|
245
317
|
return true;
|
|
246
318
|
}
|
|
247
319
|
|
|
@@ -278,6 +350,32 @@ export function readFilesForScope(scope) {
|
|
|
278
350
|
readFiles = scopeHydrated.has(key) ? new Map() : loadScopeFromDisk(key);
|
|
279
351
|
scopeHydrated.add(key);
|
|
280
352
|
readFilesByScope.set(key, readFiles);
|
|
353
|
+
pruneReadSnapshotScopes(key);
|
|
354
|
+
} else {
|
|
355
|
+
readFilesByScope.delete(key);
|
|
356
|
+
readFilesByScope.set(key, readFiles);
|
|
281
357
|
}
|
|
282
358
|
return readFiles;
|
|
283
359
|
}
|
|
360
|
+
|
|
361
|
+
export function _readSnapshotStoreStatsForTest(scope = null) {
|
|
362
|
+
const key = readScopeKey(scope);
|
|
363
|
+
const readFiles = key === null ? null : readFilesByScope.get(key);
|
|
364
|
+
return {
|
|
365
|
+
scopeCount: readFilesByScope.size,
|
|
366
|
+
hydratedScopeCount: scopeHydrated.size,
|
|
367
|
+
fileCount: readFiles?.size || 0,
|
|
368
|
+
redirectCount: key === null ? 0 : (readRedirectsByScope.get(key)?.size || 0),
|
|
369
|
+
pendingPersistCount: persistPending.size,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function _resetReadSnapshotStoreForTest() {
|
|
374
|
+
for (const timer of persistPending.values()) {
|
|
375
|
+
try { clearTimeout(timer); } catch {}
|
|
376
|
+
}
|
|
377
|
+
persistPending.clear();
|
|
378
|
+
readFilesByScope.clear();
|
|
379
|
+
readRedirectsByScope.clear();
|
|
380
|
+
scopeHydrated.clear();
|
|
381
|
+
}
|
|
@@ -443,8 +443,8 @@ function _locatorBudgetFooter(toolName, args, keptLines, capBytes) {
|
|
|
443
443
|
const target = targets[targetIndex] || targets[0] || '';
|
|
444
444
|
const remaining = targets.length - targetIndex - 1;
|
|
445
445
|
const capKb = Math.round((Number(capBytes) > 0 ? Number(capBytes) : LOCATOR_OUTPUT_MAX_BYTES) / 1024);
|
|
446
|
-
// Factual continuation only — no retry imperatives
|
|
447
|
-
//
|
|
446
|
+
// Factual continuation only — no retry imperatives: the marker states
|
|
447
|
+
// what was cut, never instructs another call.
|
|
448
448
|
const nextTarget = remaining > 0 ? `; ${remaining} target(s) not shown, next=${JSON.stringify(targets[targetIndex + 1])}` : '';
|
|
449
449
|
if (toolName === 'find') {
|
|
450
450
|
return `... [find output capped at ${capKb} KB; remaining matches for query=${JSON.stringify(target)} omitted${nextTarget}]`;
|
|
@@ -299,7 +299,9 @@ export function prewarmCodeGraphSymbols(cwd, symbols, { language = null } = {})
|
|
|
299
299
|
|
|
300
300
|
export function prewarmCodeGraphIfProject(cwd) {
|
|
301
301
|
if (!cwd) return false;
|
|
302
|
-
|
|
302
|
+
// Implicit walk (nobody asked for this build): never adopt a home/temp
|
|
303
|
+
// sentinel and prewarm the entire user profile in the background.
|
|
304
|
+
const root = _findDirProjectRoot(cwd, { stopAtUserBoundary: true });
|
|
303
305
|
if (!root) return false;
|
|
304
306
|
prewarmCodeGraph(root);
|
|
305
307
|
return true;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// resolveSymbolReadSpan, executeCodeGraphTool (entry with cwd re-rooting +
|
|
3
3
|
// batch fan-out + abort race), isCodeGraphTool. Extracted verbatim from
|
|
4
4
|
// code-graph.mjs.
|
|
5
|
-
import { resolve as pathResolve, isAbsolute, relative as pathRelative } from 'node:path';
|
|
5
|
+
import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename } from 'node:path';
|
|
6
|
+
import { homedir as osHomedir } from 'node:os';
|
|
6
7
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
7
8
|
import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
|
|
8
9
|
import { findFileByBasename } from '../builtin/path-diagnostics.mjs';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
_PROJECT_ROOT_SENTINELS,
|
|
21
22
|
_resolveFileProjectRoot,
|
|
22
23
|
_findDirProjectRoot,
|
|
24
|
+
_childProjectRoots,
|
|
23
25
|
_stripEmptyArgs,
|
|
24
26
|
} from './project-root.mjs';
|
|
25
27
|
import { buildCodeGraphAsync, prewarmCodeGraph, prewarmCodeGraphSymbols } from './build.mjs';
|
|
@@ -625,7 +627,11 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
625
627
|
const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
|
|
626
628
|
const hasAggregateFileArgs = _hasAggregateFileArgs(args);
|
|
627
629
|
let effectiveCwd = baseCwd;
|
|
628
|
-
|
|
630
|
+
// An explicit `cwd` argument is a deliberate target: honour whatever root it
|
|
631
|
+
// resolves to. A session cwd is a guess, so its ancestor walk stops at the
|
|
632
|
+
// home/temp boundary instead of adopting a stray sentinel found there.
|
|
633
|
+
const explicitCwdArg = !!(args && typeof args.cwd === 'string' && args.cwd.trim());
|
|
634
|
+
const baseProjectRoot = _findDirProjectRoot(baseCwd, { stopAtUserBoundary: !explicitCwdArg });
|
|
629
635
|
const filesystemRootCwd = !baseProjectRoot && _isFilesystemRootPath(baseCwd);
|
|
630
636
|
const aggregateFilesAtBase = _collectGraphFileList(args, { cap: false });
|
|
631
637
|
const rawModeAtBase = String(args?.mode || '').trim();
|
|
@@ -635,8 +641,24 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
635
641
|
const parentDotFederation = !baseProjectRoot
|
|
636
642
|
&& !filesystemRootCwd
|
|
637
643
|
&& exactDotFederation;
|
|
638
|
-
|
|
639
|
-
|
|
644
|
+
// Trusted graph targets living UNDER a sentinel-free cwd. Self is filtered
|
|
645
|
+
// out: federating a directory into itself would recurse forever.
|
|
646
|
+
const trustedRootsAtBase = baseProjectRoot
|
|
647
|
+
? []
|
|
648
|
+
: collectTrustedCodeGraphRoots(baseCwd)
|
|
649
|
+
.filter((root) => pathResolve(root) !== pathResolve(baseCwd));
|
|
650
|
+
// A sentinel-free cwd (multi-repo parent, vendored reference tree) is still
|
|
651
|
+
// routable when trusted project roots live under it — federate over those
|
|
652
|
+
// instead of refusing the call outright.
|
|
653
|
+
const sentinelFreeFederation = !baseProjectRoot
|
|
654
|
+
&& !filesystemRootCwd
|
|
655
|
+
&& !parentDotFederation
|
|
656
|
+
&& !fileArg
|
|
657
|
+
&& !hasAggregateFileArgs
|
|
658
|
+
&& ROOT_FEDERATED_MODES.has(rawModeAtBase)
|
|
659
|
+
&& trustedRootsAtBase.length > 0;
|
|
660
|
+
if (filesystemRootCwd || parentDotFederation || sentinelFreeFederation) {
|
|
661
|
+
const trustedRoots = trustedRootsAtBase;
|
|
640
662
|
const files = exactDotFederation ? [] : aggregateFilesAtBase;
|
|
641
663
|
const rawMode = rawModeAtBase;
|
|
642
664
|
const canFederate = files.length > 0 || ROOT_FEDERATED_MODES.has(rawMode);
|
|
@@ -756,15 +778,34 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
756
778
|
}
|
|
757
779
|
}
|
|
758
780
|
if (!fileArg && !(args && typeof args.cwd === 'string' && args.cwd.trim())) {
|
|
759
|
-
const projectRoot = _findDirProjectRoot(effectiveCwd);
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
781
|
+
const projectRoot = _findDirProjectRoot(effectiveCwd, { stopAtUserBoundary: true });
|
|
782
|
+
if (projectRoot) {
|
|
783
|
+
effectiveCwd = projectRoot;
|
|
784
|
+
} else {
|
|
785
|
+
// A sentinel-free SINGLE tree (vendored reference checkout, script
|
|
786
|
+
// folder) indexes as its own root — the same treatment an explicit
|
|
787
|
+
// 'cwd' argument already gets. Only genuinely unbounded or ambiguous
|
|
788
|
+
// targets stay refused: a filesystem root, the home directory, or a
|
|
789
|
+
// parent holding several separate repositories.
|
|
790
|
+
const childRoots = _childProjectRoots(effectiveCwd);
|
|
791
|
+
// Non-null only when the sole sentinel sits at/above the home or temp
|
|
792
|
+
// boundary — name it, so the refusal reads as a deliberate rule rather
|
|
793
|
+
// than a missing project.
|
|
794
|
+
const boundaryRoot = _findDirProjectRoot(effectiveCwd);
|
|
795
|
+
const unbounded = filesystemRootCwd
|
|
796
|
+
|| _isFilesystemRootPath(effectiveCwd)
|
|
797
|
+
|| pathResolve(effectiveCwd) === pathResolve(osHomedir());
|
|
798
|
+
if (unbounded || childRoots.length > 1) {
|
|
799
|
+
const listed = childRoots.slice(0, 5).map((root) => `"${pathBasename(root)}"`).join(', ');
|
|
800
|
+
throw new Error(
|
|
801
|
+
`${name}: cwd '${effectiveCwd}' is not inside a project (no `
|
|
802
|
+
+ `${_PROJECT_ROOT_SENTINELS.join('/')} at it or any ancestor)`
|
|
803
|
+
+ `${childRoots.length > 1 ? ` and holds ${childRoots.length} separate project roots (${listed})` : ''}. `
|
|
804
|
+
+ `${boundaryRoot ? `The nearest sentinel is at '${boundaryRoot}' (home/temp), which is never auto-adopted. ` : ''}`
|
|
805
|
+
+ `Refusing to index an arbitrary tree. Run 'cwd set <repo>', or pass an explicit `
|
|
806
|
+
+ `'cwd' (repo root) or a 'file' anchor.`);
|
|
807
|
+
}
|
|
766
808
|
}
|
|
767
|
-
effectiveCwd = projectRoot;
|
|
768
809
|
}
|
|
769
810
|
if (signal?.aborted) throw new Error('aborted');
|
|
770
811
|
const _work = (() => {
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
// Project-root sentinel resolution + empty-arg stripping. Extracted verbatim
|
|
2
2
|
// from code-graph.mjs. Used by the dispatcher to re-root file/dir queries.
|
|
3
3
|
import { resolve as pathResolve, dirname, join } from 'node:path';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
6
|
|
|
6
7
|
// P1: project-root sentinels. A directory containing any of these (or with one
|
|
7
8
|
// at an ancestor) is treated as a real project we may index.
|
|
8
9
|
export const _PROJECT_ROOT_SENTINELS = ['package.json', '.git', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'setup.py', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'build.sbt', 'Package.swift'];
|
|
9
10
|
|
|
11
|
+
// Directories an IMPLICIT ancestor walk must never cross. A stray npm
|
|
12
|
+
// `package.json` in the home directory (or in the temp root) would otherwise
|
|
13
|
+
// promote "index this folder" into "index the whole user profile" — observed
|
|
14
|
+
// as a 20s graph-binary timeout on a `%TEMP%` path. Explicit targets (a `cwd`
|
|
15
|
+
// argument, `cwd set`, a registered project) are unaffected: only the walk
|
|
16
|
+
// that guesses a root for the caller stops here.
|
|
17
|
+
function _userBoundaryDirs() {
|
|
18
|
+
const dirs = [];
|
|
19
|
+
try { dirs.push(homedir()); } catch { /* no home — nothing to guard */ }
|
|
20
|
+
try { dirs.push(tmpdir()); } catch { /* no temp — nothing to guard */ }
|
|
21
|
+
return dirs.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _dirKey(dir) {
|
|
25
|
+
const resolved = pathResolve(dir);
|
|
26
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
// P1: resolve a file to its nearest project root (sentinel ancestor).
|
|
11
30
|
// Returns null when no root found; caller throws rather than falling back.
|
|
12
31
|
export function _resolveFileProjectRoot(file) {
|
|
@@ -22,16 +41,42 @@ export function _resolveFileProjectRoot(file) {
|
|
|
22
41
|
|
|
23
42
|
// P1: nearest project root for a DIRECTORY (the dir itself or any ancestor).
|
|
24
43
|
// Returns null when the dir sits in no project.
|
|
25
|
-
|
|
44
|
+
// `stopAtUserBoundary` marks the walk as implicit: it then returns null at the
|
|
45
|
+
// home/temp boundary instead of adopting whatever sentinel happens to sit
|
|
46
|
+
// there. `boundaries` is injectable so the boundary rule stays testable.
|
|
47
|
+
export function _findDirProjectRoot(dir, { stopAtUserBoundary = false, boundaries = null } = {}) {
|
|
26
48
|
if (!dir) return null;
|
|
49
|
+
const stops = stopAtUserBoundary
|
|
50
|
+
? new Set((boundaries || _userBoundaryDirs()).map(_dirKey))
|
|
51
|
+
: null;
|
|
27
52
|
let d = pathResolve(dir);
|
|
28
53
|
while (d && d !== dirname(d)) {
|
|
54
|
+
if (stops && stops.has(_dirKey(d))) return null;
|
|
29
55
|
if (_PROJECT_ROOT_SENTINELS.some((s) => existsSync(join(d, s)))) return d;
|
|
30
56
|
d = dirname(d);
|
|
31
57
|
}
|
|
32
58
|
return null;
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
// Immediate child directories that are themselves project roots. Separates a
|
|
62
|
+
// sentinel-free SINGLE tree (a vendored reference checkout — indexable as its
|
|
63
|
+
// own root) from a multi-repo PARENT (ambiguous — indexing it would walk
|
|
64
|
+
// unrelated trees). Capped: the caller only needs "none/one" vs "many".
|
|
65
|
+
export function _childProjectRoots(dir, { cap = 32 } = {}) {
|
|
66
|
+
if (!dir) return [];
|
|
67
|
+
let entries;
|
|
68
|
+
try { entries = readdirSync(pathResolve(dir), { withFileTypes: true }); }
|
|
69
|
+
catch { return []; }
|
|
70
|
+
const roots = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
73
|
+
const child = join(pathResolve(dir), entry.name);
|
|
74
|
+
if (_PROJECT_ROOT_SENTINELS.some((s) => existsSync(join(child, s)))) roots.push(child);
|
|
75
|
+
if (roots.length >= cap) break;
|
|
76
|
+
}
|
|
77
|
+
return roots;
|
|
78
|
+
}
|
|
79
|
+
|
|
35
80
|
// MCP clients sometimes inject empty-string defaults for optional schema
|
|
36
81
|
// fields (e.g. `file: ""`). Strip empty/null optional path-like fields before
|
|
37
82
|
// dispatch so a literal "" doesn't trip the "file not found in graph" path.
|
|
@@ -56,7 +56,9 @@ export function collectTrustedCodeGraphRoots(filesystemRoot, {
|
|
|
56
56
|
],
|
|
57
57
|
cached = listCachedCodeGraphRoots,
|
|
58
58
|
directory = isDirectory,
|
|
59
|
-
|
|
59
|
+
// Selected paths are promoted to their nearest project root — but a home or
|
|
60
|
+
// temp sentinel must not widen a selected subfolder into the whole profile.
|
|
61
|
+
projectRoot = (dir) => _findDirProjectRoot(dir, { stopAtUserBoundary: true }),
|
|
60
62
|
} = {}) {
|
|
61
63
|
const root = resolve(filesystemRoot);
|
|
62
64
|
const rows = [
|
|
@@ -105,8 +105,7 @@ export function scrubProviderSecrets(env) {
|
|
|
105
105
|
// a dev-tree test run inside such a shell would resolve defaults/agents.json
|
|
106
106
|
// and friends from the INSTALLED app instead of the repo under test (plain
|
|
107
107
|
// node can't even read asar paths → ENOENT). Internal worker spawns that need
|
|
108
|
-
// the root re-set it explicitly and are unaffected
|
|
109
|
-
// call this.
|
|
108
|
+
// the root re-set it explicitly and are unaffected.
|
|
110
109
|
export function scrubRuntimeRootVars(env) {
|
|
111
110
|
if (!env || typeof env !== 'object') return env;
|
|
112
111
|
delete env.MIXDOG_ROOT;
|
|
@@ -118,5 +117,13 @@ export function scrubRuntimeRootVars(env) {
|
|
|
118
117
|
// env, so shells must not see it. A command that needs NODE_ENV sets it
|
|
119
118
|
// explicitly.
|
|
120
119
|
delete env.NODE_ENV;
|
|
120
|
+
// Daemon IDENTITY leak: user-facing children must not inherit host/worker
|
|
121
|
+
// ownership or the daemon's terminal-lead PID. Internal daemon/worker
|
|
122
|
+
// spawns set every identity value explicitly after this boundary.
|
|
123
|
+
delete env.MIXDOG_ENGINE_DAEMON_HOST;
|
|
124
|
+
delete env.MIXDOG_CHANNEL_DAEMON;
|
|
125
|
+
delete env.MIXDOG_WORKER_MODE;
|
|
126
|
+
delete env.MIXDOG_DAEMON_SPAWNED_FOR;
|
|
127
|
+
delete env.MIXDOG_SUPERVISOR_PID;
|
|
121
128
|
return env;
|
|
122
129
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
countHunkChanges,
|
|
25
25
|
} from './paths.mjs';
|
|
26
26
|
import {
|
|
27
|
-
|
|
27
|
+
runServerApply,
|
|
28
28
|
scheduleNativePatchIdleClose,
|
|
29
29
|
nativePatchTraceEnabled,
|
|
30
30
|
patchTraceEnabled,
|
|
@@ -99,7 +99,7 @@ export async function dispatchNativePatch({ entries, basePath, nativePatchStr, f
|
|
|
99
99
|
const nativeStart = performance.now();
|
|
100
100
|
let stats;
|
|
101
101
|
try {
|
|
102
|
-
stats = await
|
|
102
|
+
stats = await runServerApply(basePath, nativePatchStr, { fuzz, rejectPartial, dryRun, signal });
|
|
103
103
|
} catch (err) {
|
|
104
104
|
scheduleNativePatchIdleClose();
|
|
105
105
|
const msg = err?.message || String(err);
|
|
@@ -65,7 +65,7 @@ export function collectUnifiedNewLines(hunk) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Guard a replacement plan before it is applied in DESCENDING order (the
|
|
68
|
-
//
|
|
68
|
+
// `apply_replacements` shape). The plan must already be sorted
|
|
69
69
|
// ascending by `start`. Descending application is only content-safe when the
|
|
70
70
|
// replaced ranges are disjoint; the single well-defined exception is several
|
|
71
71
|
// zero-length insertions at one index, which descending application emits in
|
|
@@ -129,6 +129,11 @@ function decodeNativeFailures(hexPayload) {
|
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Error code for a request refused/failed because the server child is gone.
|
|
133
|
+
// Nothing was written when it is thrown from the pre-flight, so one respawned
|
|
134
|
+
// retry is safe.
|
|
135
|
+
export const NATIVE_PATCH_TRANSPORT_DEAD = 'ENATIVEPATCHDEAD';
|
|
136
|
+
|
|
132
137
|
class NativePatchServer {
|
|
133
138
|
constructor(binPath) {
|
|
134
139
|
this.binPath = binPath;
|
|
@@ -141,6 +146,7 @@ class NativePatchServer {
|
|
|
141
146
|
this.lines = [];
|
|
142
147
|
this.waiters = [];
|
|
143
148
|
this.exited = false;
|
|
149
|
+
this.transportError = null;
|
|
144
150
|
this.child.stderr.setEncoding('utf8');
|
|
145
151
|
this.child.stderr.on('data', (chunk) => { this.stderr += chunk; });
|
|
146
152
|
this.rl = createInterface({ input: this.child.stdout });
|
|
@@ -149,6 +155,12 @@ class NativePatchServer {
|
|
|
149
155
|
if (waiter) waiter.resolve(line);
|
|
150
156
|
else this.lines.push(line);
|
|
151
157
|
});
|
|
158
|
+
// A child that died mid-request turns every pending `stdin.write()` into an
|
|
159
|
+
// UNHANDLED 'error' event (EPIPE) that takes the whole host process down —
|
|
160
|
+
// a release validate job was lost exactly that way. Absorb it here: mark
|
|
161
|
+
// the transport dead and reject the waiters so callers see an ordinary
|
|
162
|
+
// rejection (and respawn) instead of a crash.
|
|
163
|
+
this.child.stdin.on('error', (err) => { this.failTransport(err); });
|
|
152
164
|
this.child.on('exit', (code, signal) => {
|
|
153
165
|
this.exited = true;
|
|
154
166
|
const err = new Error(`native patch server exited code=${code} signal=${signal} stderr=${this.stderr}`);
|
|
@@ -157,6 +169,29 @@ class NativePatchServer {
|
|
|
157
169
|
});
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
// Idempotent death record: the stdin error and the child exit usually both
|
|
173
|
+
// fire for the same death, and only the first one owns the reason.
|
|
174
|
+
failTransport(cause) {
|
|
175
|
+
this.exited = true;
|
|
176
|
+
if (!this.transportError) {
|
|
177
|
+
const err = cause instanceof Error ? cause : new Error(String(cause || 'native patch transport closed'));
|
|
178
|
+
err.code = NATIVE_PATCH_TRANSPORT_DEAD;
|
|
179
|
+
this.transportError = err;
|
|
180
|
+
}
|
|
181
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(this.transportError);
|
|
182
|
+
try { this.rl.close(); } catch {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Pre-flight for every request: refuse BEFORE a single byte is written, so
|
|
186
|
+
// the caller may respawn and retry without any risk of double-applying.
|
|
187
|
+
assertAlive(action) {
|
|
188
|
+
if (!this.exited && !this.transportError) return;
|
|
189
|
+
const reason = this.transportError?.message || this.stderr || 'exited';
|
|
190
|
+
const err = new Error(`native patch server is not running (${action}): ${reason}`);
|
|
191
|
+
err.code = NATIVE_PATCH_TRANSPORT_DEAD;
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
abort(signal) {
|
|
161
196
|
const err = new Error(signal?.reason?.message || signal?.reason || 'native patch aborted');
|
|
162
197
|
err.name = 'AbortError';
|
|
@@ -188,6 +223,7 @@ class NativePatchServer {
|
|
|
188
223
|
|
|
189
224
|
async ping() {
|
|
190
225
|
this.ref();
|
|
226
|
+
this.assertAlive('ping');
|
|
191
227
|
const linePromise = this.nextLine();
|
|
192
228
|
this.child.stdin.write('PING\n');
|
|
193
229
|
const line = await linePromise;
|
|
@@ -198,6 +234,7 @@ class NativePatchServer {
|
|
|
198
234
|
|
|
199
235
|
async apply(basePath, patchText, { fuzz = 2, rejectPartial = true, dryRun = false, signal = null } = {}) {
|
|
200
236
|
this.ref();
|
|
237
|
+
this.assertAlive('apply');
|
|
201
238
|
if (signal?.aborted) {
|
|
202
239
|
const err = new Error(signal.reason?.message || signal.reason || 'native patch aborted');
|
|
203
240
|
err.name = 'AbortError';
|
|
@@ -294,6 +331,7 @@ class NativePatchServer {
|
|
|
294
331
|
// matched tier.
|
|
295
332
|
async edit(fullPath, oldBuf, newBuf, { replaceAll = false, dryRun = false, signal = null } = {}) {
|
|
296
333
|
this.ref();
|
|
334
|
+
this.assertAlive('edit');
|
|
297
335
|
if (signal?.aborted) {
|
|
298
336
|
const err = new Error(signal.reason?.message || signal.reason || 'native edit aborted');
|
|
299
337
|
err.name = 'AbortError';
|
|
@@ -384,8 +422,25 @@ function getNativeEditServer() {
|
|
|
384
422
|
// NativePatchServer transport but runs on a DEDICATED instance so edit and
|
|
385
423
|
// patch requests never interleave their stdin framing on one stdout stream.
|
|
386
424
|
export async function runServerEdit({ fullPath, oldBuf, newBuf, replaceAll = false, dryRun = false, signal = null }) {
|
|
387
|
-
|
|
388
|
-
|
|
425
|
+
try {
|
|
426
|
+
return await getNativeEditServer().edit(fullPath, oldBuf, newBuf, { replaceAll, dryRun, signal });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err?.code !== NATIVE_PATCH_TRANSPORT_DEAD) throw err;
|
|
429
|
+
// The dead instance was refused before any byte went out; the getter
|
|
430
|
+
// respawns because the previous one is marked exited.
|
|
431
|
+
return getNativeEditServer().edit(fullPath, oldBuf, newBuf, { replaceAll, dryRun, signal });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Same one-shot respawn for APPLY: an idle-watchdog exit or an external kill
|
|
436
|
+
// between requests must cost a respawn, not the request.
|
|
437
|
+
export async function runServerApply(basePath, patchText, options = {}) {
|
|
438
|
+
try {
|
|
439
|
+
return await getNativePatchServer().apply(basePath, patchText, options);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err?.code !== NATIVE_PATCH_TRANSPORT_DEAD) throw err;
|
|
442
|
+
return getNativePatchServer().apply(basePath, patchText, options);
|
|
443
|
+
}
|
|
389
444
|
}
|
|
390
445
|
|
|
391
446
|
export function scheduleNativePatchPrewarm() {
|