mixdog 0.9.134 → 0.9.136
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 +3 -1
- package/scripts/tool-stress.mjs +8 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/defaults/skills/setup/SKILL.md +8 -43
- package/src/headless-exec.mjs +26 -9
- package/src/headless-exec.test.mjs +55 -2
- package/src/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +12 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +25 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +30 -7
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +60 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +138 -19
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +80 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +3 -19
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +25 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +165 -17
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/channels/lib/config.mjs +14 -101
- package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
- package/src/runtime/channels/lib/scheduler.mjs +75 -18
- package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
- package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
- package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
- package/src/runtime/channels/lib/worker-main.mjs +22 -307
- package/src/runtime/media/renditions.mjs +68 -5
- package/src/runtime/media/renditions.test.mjs +45 -0
- package/src/runtime/media/store.mjs +1 -0
- package/src/runtime/memory/index.mjs +4 -3
- package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/runtime/shared/config.mjs +8 -49
- package/src/runtime/shared/schedule-time.mjs +25 -0
- package/src/runtime/shared/schedule-time.test.mjs +29 -0
- package/src/runtime/shared/schedules-db.mjs +73 -1
- package/src/runtime/shared/tool-card-model.mjs +1 -1
- package/src/runtime/shared/transcript-writer.mjs +10 -17
- package/src/runtime/shared/update-checker.mjs +7 -2
- package/src/runtime/shared/update-checker.test.mjs +33 -0
- package/src/session-runtime/channel-config-api.mjs +3 -20
- package/src/session-runtime/config-lifecycle.mjs +1 -42
- package/src/session-runtime/lifecycle-api.mjs +10 -21
- package/src/session-runtime/model-route-api.mjs +0 -2
- package/src/session-runtime/model-settings-persist.test.mjs +0 -1
- package/src/session-runtime/prewarm.mjs +11 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +20 -186
- package/src/session-runtime/session-lifecycle.mjs +4 -17
- package/src/session-runtime/session-transcript.mjs +54 -0
- package/src/session-runtime/session-turn-api.mjs +4 -44
- package/src/session-runtime/settings-api.mjs +0 -45
- package/src/session-runtime/tool-catalog-schema.mjs +1 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +4 -0
- package/src/standalone/channel-admin.mjs +8 -150
- package/src/standalone/channel-transport.mjs +13 -0
- package/src/standalone/daemon.mjs +19 -49
- package/src/standalone/seeds.mjs +18 -1
- package/src/standalone/session-protocol.mjs +0 -8
- package/src/standalone/session-service.mjs +1 -3
- package/src/tui/App.jsx +0 -5
- package/src/tui/app/app-view.jsx +1 -2
- package/src/tui/app/create-app-pickers.mjs +0 -20
- package/src/tui/app/doctor.mjs +2 -6
- package/src/tui/app/prompt-submit.mjs +2 -49
- package/src/tui/app/settings-picker.mjs +22 -96
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +0 -17
- package/src/tui/app/use-prompt-handlers.mjs +4 -1
- package/src/tui/components/StatusLine.jsx +4 -4
- package/src/tui/dist/index.mjs +1003 -1461
- package/src/tui/session/context-state.mjs +0 -1
- package/src/tui/session/queue-helpers.mjs +1 -1
- package/src/tui/session/queue-helpers.test.mjs +10 -1
- package/src/tui/session/session-api-ext.mjs +0 -89
- package/src/tui/session/session-api.mjs +0 -2
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/tool-result-status.mjs +3 -3
- package/src/tui/session/tool-result-status.test.mjs +3 -3
- package/src/tui/session-local.mjs +0 -26
- package/src/workflows/default/WORKFLOW.md +13 -15
- package/src/workflows/solo/WORKFLOW.md +6 -7
- package/src/runtime/channels/lib/config-lock.mjs +0 -13
- package/src/runtime/channels/lib/drop-trace.mjs +0 -71
- package/src/runtime/channels/lib/format.mjs +0 -190
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
- package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
- package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
- package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
- package/src/runtime/channels/lib/memory-client.mjs +0 -289
- package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
- package/src/runtime/channels/lib/session-control.mjs +0 -36
- package/src/runtime/channels/lib/session-discovery.mjs +0 -217
- package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
- package/src/runtime/channels/lib/telegram-format.mjs +0 -280
- package/src/runtime/channels/lib/tool-format.mjs +0 -125
- package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
- package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
- package/src/runtime/channels/providers/discord-access.mjs +0 -30
- package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
- package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
- package/src/runtime/channels/providers/discord.mjs +0 -730
- package/src/runtime/channels/providers/telegram.mjs +0 -598
- package/src/runtime/channels/providers/telegram.test.mjs +0 -75
- package/src/session-runtime/remote-control.mjs +0 -126
- package/src/session-runtime/remote-transcript.mjs +0 -134
- package/src/session-runtime/remote-transition-queue.mjs +0 -14
- package/src/tui/app/channel-pickers.mjs +0 -384
|
@@ -476,6 +476,7 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
476
476
|
};
|
|
477
477
|
|
|
478
478
|
const deadline = Date.now() + READ_STREAM_TIMEOUT_MS;
|
|
479
|
+
let scanCompleted = false;
|
|
479
480
|
try {
|
|
480
481
|
while (position < st.size) {
|
|
481
482
|
if (Date.now() > deadline) throw new Error(`read timed out after ${READ_STREAM_TIMEOUT_MS}ms`);
|
|
@@ -513,10 +514,12 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
513
514
|
: Buffer.concat(pendingParts, pendingBytes);
|
|
514
515
|
finishLine(lineBuf, currentLineStartByte, st.size);
|
|
515
516
|
}
|
|
517
|
+
scanCompleted = true;
|
|
516
518
|
} finally {
|
|
517
|
-
await fh.close().catch(() => {});
|
|
519
|
+
if (!scanCompleted) await fh.close().catch(() => {});
|
|
518
520
|
}
|
|
519
521
|
|
|
522
|
+
try {
|
|
520
523
|
const tailLen = Math.min(tailCount, SMART_READ_TAIL_LINES);
|
|
521
524
|
const tailOffsets = [];
|
|
522
525
|
for (let i = tailCount - tailLen; i < tailCount; i++) {
|
|
@@ -530,14 +533,9 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
530
533
|
const byteLen = Math.max(0, lastEnd - firstStart);
|
|
531
534
|
let tailWindow = Buffer.alloc(0);
|
|
532
535
|
if (byteLen > 0) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const { bytesRead } = await tailFh.read(tailWindow, 0, byteLen, firstStart);
|
|
537
|
-
if (bytesRead < byteLen) tailWindow = tailWindow.subarray(0, bytesRead);
|
|
538
|
-
} finally {
|
|
539
|
-
await tailFh.close().catch(() => {});
|
|
540
|
-
}
|
|
536
|
+
tailWindow = Buffer.allocUnsafe(byteLen);
|
|
537
|
+
const { bytesRead } = await fh.read(tailWindow, 0, byteLen, firstStart);
|
|
538
|
+
if (bytesRead < byteLen) tailWindow = tailWindow.subarray(0, bytesRead);
|
|
541
539
|
}
|
|
542
540
|
for (const entry of tailOffsets) {
|
|
543
541
|
let rawBuf = tailWindow.subarray(entry.startByte - firstStart, entry.endByte - firstStart);
|
|
@@ -604,4 +602,7 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
604
602
|
rangeHashes,
|
|
605
603
|
},
|
|
606
604
|
};
|
|
605
|
+
} finally {
|
|
606
|
+
await fh.close().catch(() => {});
|
|
607
|
+
}
|
|
607
608
|
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
cacheSet,
|
|
61
61
|
runResultCacheInFlight,
|
|
62
62
|
statPathsForMtime,
|
|
63
|
+
visitPathsForMtime,
|
|
63
64
|
} from './cache-layers.mjs';
|
|
64
65
|
import { recordLocalSearchCacheHit } from './local-search-telemetry.mjs';
|
|
65
66
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
|
|
@@ -121,6 +122,58 @@ function _globDefaultHeadLimit() {
|
|
|
121
122
|
return parsed > 0 ? parsed : 100;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function compareGlobMtimeEntries(a, b) {
|
|
126
|
+
const dm = b.mtime - a.mtime;
|
|
127
|
+
if (dm !== 0) return dm;
|
|
128
|
+
return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function retainBestEntries(heap, candidate, limit, compare) {
|
|
132
|
+
if (heap.length < limit) {
|
|
133
|
+
heap.push(candidate);
|
|
134
|
+
for (let index = heap.length - 1; index > 0;) {
|
|
135
|
+
const parent = Math.floor((index - 1) / 2);
|
|
136
|
+
if (compare(heap[index], heap[parent]) <= 0) break;
|
|
137
|
+
[heap[index], heap[parent]] = [heap[parent], heap[index]];
|
|
138
|
+
index = parent;
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (compare(candidate, heap[0]) >= 0) return;
|
|
143
|
+
heap[0] = candidate;
|
|
144
|
+
for (let index = 0;;) {
|
|
145
|
+
const left = index * 2 + 1;
|
|
146
|
+
const right = left + 1;
|
|
147
|
+
let worst = index;
|
|
148
|
+
if (left < heap.length && compare(heap[left], heap[worst]) > 0) worst = left;
|
|
149
|
+
if (right < heap.length && compare(heap[right], heap[worst]) > 0) worst = right;
|
|
150
|
+
if (worst === index) break;
|
|
151
|
+
[heap[index], heap[worst]] = [heap[worst], heap[index]];
|
|
152
|
+
index = worst;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function _createGlobMtimeTopK(limit) {
|
|
157
|
+
const cap = Math.max(1, Math.floor(Number(limit) || 1));
|
|
158
|
+
const statted = [];
|
|
159
|
+
const unstatted = [];
|
|
160
|
+
return {
|
|
161
|
+
add(entry, index) {
|
|
162
|
+
if (entry?.stat != null) {
|
|
163
|
+
retainBestEntries(statted, entry, cap, compareGlobMtimeEntries);
|
|
164
|
+
} else {
|
|
165
|
+
retainBestEntries(unstatted, { ...entry, _walkIndex: index }, cap,
|
|
166
|
+
(a, b) => a._walkIndex - b._walkIndex);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
values() {
|
|
170
|
+
statted.sort(compareGlobMtimeEntries);
|
|
171
|
+
unstatted.sort((a, b) => a._walkIndex - b._walkIndex);
|
|
172
|
+
return [...statted, ...unstatted].slice(0, cap);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
function _grepContextCharBudget(options = {}) {
|
|
125
178
|
const explicit = Number(options?._grepContextCharBudget);
|
|
126
179
|
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
@@ -338,11 +391,27 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
338
391
|
let rgStdoutPartial = false;
|
|
339
392
|
let rgWindowIncomplete = false;
|
|
340
393
|
let rgCacheUnsafe = false;
|
|
394
|
+
let nativeMtimeTotal = null;
|
|
341
395
|
const accumCap = 50000;
|
|
342
396
|
const canWindowNatural = sortMode === 'natural' && headLimit !== Infinity;
|
|
397
|
+
const canWindowMtime = sortMode === 'mtime'
|
|
398
|
+
&& headLimit !== Infinity;
|
|
343
399
|
const groupRuns = await Promise.all(globGroups.map(async ([root, rels]) => {
|
|
344
400
|
const rgArgs = ['--files', '--hidden'];
|
|
345
|
-
|
|
401
|
+
// Explicit literal basenames (no glob magic in the final segment)
|
|
402
|
+
// name a concrete file: honor rg's later-glob-wins contract and let
|
|
403
|
+
// the lookup descend dependency-noise dirs, which the native walker
|
|
404
|
+
// would otherwise prune before the pattern could ever match. Wildcard
|
|
405
|
+
// basenames keep the noise prunes; device-name globs (no trailing
|
|
406
|
+
// /**) always apply.
|
|
407
|
+
const explicitBasenames = rels.length > 0 && rels.every((rel) => {
|
|
408
|
+
const base = String(rel).split('/').filter(Boolean).pop() || '';
|
|
409
|
+
return base !== '' && !hasGlobMagic(base);
|
|
410
|
+
});
|
|
411
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) {
|
|
412
|
+
if (explicitBasenames && /^!\*\*\/[^/]+\/\*\*$/.test(ex)) continue;
|
|
413
|
+
rgArgs.push('--glob', ex);
|
|
414
|
+
}
|
|
346
415
|
for (const ex of extraIgnoreGlobs) rgArgs.push('--glob', ex);
|
|
347
416
|
for (const rel of rels) rgArgs.push('--glob', rel);
|
|
348
417
|
const rgCwd = resolvedForSearchRoot(root);
|
|
@@ -378,6 +447,24 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
378
447
|
};
|
|
379
448
|
}
|
|
380
449
|
try {
|
|
450
|
+
if (canWindowMtime) {
|
|
451
|
+
const served = await runRgWindowedLines(
|
|
452
|
+
rgArgs,
|
|
453
|
+
{ cwd: rgCwd, signal: sharedSignal },
|
|
454
|
+
{ offset: 0, limit: offset + headLimit + 1, mtimeTopK: true },
|
|
455
|
+
);
|
|
456
|
+
return {
|
|
457
|
+
error: null,
|
|
458
|
+
paths: served.lines.map((line) =>
|
|
459
|
+
isAbsolute(line) ? line : resolveAgainstCwd(line, rgCwd)),
|
|
460
|
+
stdoutTruncated: false,
|
|
461
|
+
stdoutPartial: served.partial === true,
|
|
462
|
+
windowIncomplete: false,
|
|
463
|
+
cacheSafe: served.cacheSafe !== false,
|
|
464
|
+
nativeMtime: true,
|
|
465
|
+
totalSeen: served.totalSeen,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
381
468
|
if (canWindowNatural) {
|
|
382
469
|
const served = await runRgWindowedLines(
|
|
383
470
|
rgArgs,
|
|
@@ -427,6 +514,9 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
427
514
|
if (run.stdoutPartial) rgStdoutPartial = true;
|
|
428
515
|
if (run.windowIncomplete) rgWindowIncomplete = true;
|
|
429
516
|
if (run.cacheSafe === false) rgCacheUnsafe = true;
|
|
517
|
+
if (run.nativeMtime) {
|
|
518
|
+
nativeMtimeTotal = (nativeMtimeTotal ?? 0) + Math.max(0, Number(run.totalSeen) || 0);
|
|
519
|
+
}
|
|
430
520
|
for (const p of run.paths) {
|
|
431
521
|
allFiles.push(p);
|
|
432
522
|
if (allFiles.length >= accumCap) {
|
|
@@ -441,34 +531,59 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
441
531
|
|
|
442
532
|
const unique = Array.from(new Set(allFiles));
|
|
443
533
|
let orderedPaths;
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
534
|
+
if (nativeMtimeTotal !== null && globGroups.length === 1) {
|
|
535
|
+
orderedPaths = unique;
|
|
536
|
+
} else if (nativeMtimeTotal !== null) {
|
|
537
|
+
const withStat = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
449
538
|
const statted = [];
|
|
450
539
|
const unstatted = [];
|
|
451
|
-
for (const entry of
|
|
540
|
+
for (const entry of withStat) {
|
|
452
541
|
if (!entry) continue;
|
|
453
542
|
if (entry.stat != null) statted.push(entry);
|
|
454
543
|
else unstatted.push(entry);
|
|
455
544
|
}
|
|
456
|
-
statted.sort(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
545
|
+
statted.sort(compareGlobMtimeEntries);
|
|
546
|
+
orderedPaths = [...statted, ...unstatted]
|
|
547
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
548
|
+
} else if (sortMode === 'mtime') {
|
|
549
|
+
// Default mtime sorting stats every match. Bound the post-rg stat
|
|
550
|
+
// phase so a hung mount cannot pin glob until the agent stall
|
|
551
|
+
// watchdog fires.
|
|
552
|
+
if (headLimit === Infinity) {
|
|
553
|
+
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
554
|
+
const statted = [];
|
|
555
|
+
const unstatted = [];
|
|
556
|
+
for (const entry of withStatAll) {
|
|
557
|
+
if (!entry) continue;
|
|
558
|
+
if (entry.stat != null) statted.push(entry);
|
|
559
|
+
else unstatted.push(entry);
|
|
560
|
+
}
|
|
561
|
+
statted.sort(compareGlobMtimeEntries);
|
|
562
|
+
// A deadline-expired or failed stat degrades ORDERING only, never
|
|
563
|
+
// completeness: unsortable entries keep their walk order at the tail.
|
|
564
|
+
orderedPaths = [...statted, ...unstatted]
|
|
565
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
566
|
+
} else {
|
|
567
|
+
// Scan and stat every match for exact global ordering, but retain
|
|
568
|
+
// only the requested page plus one lookahead row. This changes
|
|
569
|
+
// O(N log N) sorting and O(N) result retention to O(N log K) and
|
|
570
|
+
// O(K) without narrowing the searched candidate set.
|
|
571
|
+
const selector = _createGlobMtimeTopK(
|
|
572
|
+
Math.min(unique.length, offset + headLimit + 1),
|
|
573
|
+
);
|
|
574
|
+
await visitPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 },
|
|
575
|
+
(entry, index) => selector.add(entry, index));
|
|
576
|
+
orderedPaths = selector.values()
|
|
577
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
578
|
+
}
|
|
464
579
|
} else {
|
|
465
580
|
orderedPaths = unique.map((entry) => isAbsolute(entry) ? resolve(entry) : resolveAgainstCwd(entry, workDir));
|
|
466
581
|
}
|
|
467
|
-
const totalBeforeOffset =
|
|
582
|
+
const totalBeforeOffset = nativeMtimeTotal ?? unique.length;
|
|
468
583
|
const windowed = offset > 0 ? orderedPaths.slice(offset) : orderedPaths;
|
|
469
584
|
const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit))
|
|
470
585
|
.map((abs) => relativeSearchResultPath(abs, workDir));
|
|
471
|
-
const remaining =
|
|
586
|
+
const remaining = Math.max(0, totalBeforeOffset - offset - capped.length);
|
|
472
587
|
const truncSuffix = accumTruncated
|
|
473
588
|
? '\n... [truncated at accumulation cap (50000)]'
|
|
474
589
|
: (rgStdoutTruncated ? '\n... [truncated at rg stdout cap (20MB); results incomplete]' : '')
|
|
@@ -494,8 +609,12 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
494
609
|
if (options?.scopedCacheOutcome && (accumTruncated || rgStdoutTruncated || rgStdoutPartial || rgWindowIncomplete || remaining > 0)) {
|
|
495
610
|
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
496
611
|
}
|
|
497
|
-
|
|
498
|
-
|
|
612
|
+
// Pagination is not an incomplete computation: limit/offset are part of
|
|
613
|
+
// the cache key, and the native watcher invalidates the scoped page on any
|
|
614
|
+
// relevant namespace/mtime change. Cache exact pages even when more rows
|
|
615
|
+
// exist; only a genuinely partial scan must bypass reuse.
|
|
616
|
+
const globComputationIncomplete = accumTruncated || rgStdoutTruncated || rgStdoutPartial || rgErrors.length > 0;
|
|
617
|
+
if (!globComputationIncomplete && !rgCacheUnsafe) {
|
|
499
618
|
cacheSet(cacheKey, out, { scopes: [...groups.keys()].map((root) => resolvedForSearchRoot(root)) });
|
|
500
619
|
}
|
|
501
620
|
// ② completion progress (claude "Found N" parity). Best-effort, no-op
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { _createGlobMtimeTopK } from './search-glob-tool.mjs';
|
|
5
|
+
|
|
6
|
+
function entry(path, mtime, stat = {}) {
|
|
7
|
+
return { path, full: path, mtime, mtimeMs: mtime, stat };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('glob mtime top-K exactly matches full ordering and preserves failed-stat walk order', () => {
|
|
11
|
+
const entries = [
|
|
12
|
+
entry('z-old', 1),
|
|
13
|
+
entry('b-tie', 9),
|
|
14
|
+
entry('failed-first', 0, null),
|
|
15
|
+
entry('a-tie', 9),
|
|
16
|
+
entry('newest', 12),
|
|
17
|
+
entry('failed-second', 0, null),
|
|
18
|
+
entry('middle', 5),
|
|
19
|
+
];
|
|
20
|
+
const selector = _createGlobMtimeTopK(6);
|
|
21
|
+
const completionOrder = [5, 1, 6, 2, 4, 0, 3];
|
|
22
|
+
for (const index of completionOrder) selector.add(entries[index], index);
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
selector.values().map((item) => item.path),
|
|
26
|
+
['newest', 'a-tie', 'b-tie', 'middle', 'z-old', 'failed-first'],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('glob mtime top-K retains only the requested bound', () => {
|
|
31
|
+
const selector = _createGlobMtimeTopK(3);
|
|
32
|
+
for (let index = 0; index < 100; index += 1) {
|
|
33
|
+
selector.add(entry(`file-${String(index).padStart(3, '0')}`, index), index);
|
|
34
|
+
}
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
selector.values().map((item) => item.path),
|
|
37
|
+
['file-099', 'file-098', 'file-097'],
|
|
38
|
+
);
|
|
39
|
+
});
|
|
@@ -520,7 +520,13 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
520
520
|
// Skipped for pattern arrays and when `-i` is already set.
|
|
521
521
|
const caseHintSuffix = async () => {
|
|
522
522
|
const caseInsensitive = args['-i'] === true;
|
|
523
|
-
|
|
523
|
+
// A broad no-match must not trigger a second whole-tree scan just to
|
|
524
|
+
// produce an advisory casing hint. The requested case-sensitive result
|
|
525
|
+
// is already complete; retain the hint only for explicit file scopes.
|
|
526
|
+
if (grepStat.isDirectory()
|
|
527
|
+
|| caseInsensitive
|
|
528
|
+
|| patterns.length !== 1
|
|
529
|
+
|| !/[A-Za-z]/.test(patterns[0])) return '';
|
|
524
530
|
try {
|
|
525
531
|
const probeArgs = buildGrepRgArgs({
|
|
526
532
|
patterns,
|
|
@@ -568,7 +568,10 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
|
|
|
568
568
|
// regex/search args are never mistaken for real syntax.
|
|
569
569
|
const masked = maskQuotedRegions(rewritten);
|
|
570
570
|
|
|
571
|
-
|
|
571
|
+
// Only command heads matter here. Scan the quote-masked copy so PowerShell
|
|
572
|
+
// literals use PowerShell escaping rules (`\` is literal, backtick escapes)
|
|
573
|
+
// and nested bash snippets remain opaque.
|
|
574
|
+
for (const segment of shellSplitSegments(masked)) {
|
|
572
575
|
for (const stage of shellSplitPipelineSegments(segment)) {
|
|
573
576
|
const tokens = stripShellProbeWrappers(shellTokenize(stage) || []);
|
|
574
577
|
if (tokens.length === 0) continue;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import {
|
|
3
|
+
modelVisibleToolCompletionMessage,
|
|
3
4
|
normalizeToolNotifyContext,
|
|
4
5
|
notifyToolCompletion,
|
|
5
6
|
} from '../../../../shared/tool-execution-contract.mjs';
|
|
7
|
+
// Runtime-only bindings (used inside completion delivery, never at module
|
|
8
|
+
// eval), so the static cycle through session/manager.mjs is safe — same
|
|
9
|
+
// pattern as loop/tool-exec.mjs.
|
|
10
|
+
import {
|
|
11
|
+
enqueuePendingMessage,
|
|
12
|
+
markCompletionEntry,
|
|
13
|
+
} from '../../session/manager.mjs';
|
|
6
14
|
import {
|
|
7
15
|
completeBackgroundTask,
|
|
8
16
|
} from '../../../../shared/background-tasks.mjs';
|
|
@@ -22,6 +30,8 @@ import {
|
|
|
22
30
|
} from './lib/shell-job-insights.mjs';
|
|
23
31
|
import {
|
|
24
32
|
completeShellJobRecord,
|
|
33
|
+
listShellJobRecords,
|
|
34
|
+
pidAlive,
|
|
25
35
|
publishShellJobRecord,
|
|
26
36
|
} from './lib/shell-job-records.mjs';
|
|
27
37
|
import {
|
|
@@ -86,7 +96,7 @@ function shellJobTaskStatus(status) {
|
|
|
86
96
|
return 'failed';
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
function buildShellCompletion(jobId, detail) {
|
|
99
|
+
export function buildShellCompletion(jobId, detail) {
|
|
90
100
|
const startedAtMs = Date.parse(detail?.startedAt || '');
|
|
91
101
|
const finishedAtMs = Date.parse(detail?.finishedAt || '') || Date.now();
|
|
92
102
|
const elapsedMs = Number.isFinite(startedAtMs)
|
|
@@ -247,7 +257,12 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
247
257
|
instruction: completion.instruction,
|
|
248
258
|
terminalReason: 'shell-native-event',
|
|
249
259
|
});
|
|
250
|
-
if (!completedTask
|
|
260
|
+
if (!completedTask) {
|
|
261
|
+
// Delivery order: owner notifyFn when present, else the owner
|
|
262
|
+
// session's pending queue. A ctx-less finish (daemon restarted
|
|
263
|
+
// between job start and completion, or a watcher armed without a
|
|
264
|
+
// notify context) previously dropped the completion silently.
|
|
265
|
+
const owner = String(ctx?.callerSessionId || detail.ownerSessionId || '');
|
|
251
266
|
notifyToolCompletion({
|
|
252
267
|
surface: 'shell',
|
|
253
268
|
id: jobId,
|
|
@@ -255,7 +270,19 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
255
270
|
text: completion.body,
|
|
256
271
|
resultType: 'shell_task_result',
|
|
257
272
|
instruction: completion.instruction,
|
|
258
|
-
context: ctx,
|
|
273
|
+
context: ctx || { callerSessionId: owner },
|
|
274
|
+
enqueueFallback: (sessionId, message, meta) => {
|
|
275
|
+
let visible = modelVisibleToolCompletionMessage(message, meta);
|
|
276
|
+
// Bodyless envelopes (a finished command with no output)
|
|
277
|
+
// fail the persistence gate's result-body requirement;
|
|
278
|
+
// retry with an explicit "(no output)" section rather
|
|
279
|
+
// than dropping the completion.
|
|
280
|
+
if (!visible && !/\n\s*\n/.test(String(message || ''))) {
|
|
281
|
+
visible = modelVisibleToolCompletionMessage(`${message}\n\n(no output)`, meta);
|
|
282
|
+
}
|
|
283
|
+
if (!visible) return false;
|
|
284
|
+
return enqueuePendingMessage(sessionId, markCompletionEntry(visible)) > 0;
|
|
285
|
+
},
|
|
259
286
|
logPrefix: 'shell-jobs',
|
|
260
287
|
});
|
|
261
288
|
}
|
|
@@ -287,6 +314,56 @@ export async function adoptForegroundShellJob({
|
|
|
287
314
|
return native;
|
|
288
315
|
}
|
|
289
316
|
|
|
317
|
+
// Daemon-restart recovery: records left 'running' by a dead daemon can never
|
|
318
|
+
// finish through the live watcher path — their completion (and output) used to
|
|
319
|
+
// vanish silently. Finalize each dead-pid record and push one completion
|
|
320
|
+
// notice to the owner session's pending queue. Content-addressed completion
|
|
321
|
+
// ids keep multi-shard reconciliation idempotent.
|
|
322
|
+
let _recoveredCompletionsReconciled = false;
|
|
323
|
+
export async function reconcileRecoveredShellJobCompletions() {
|
|
324
|
+
if (_recoveredCompletionsReconciled) return 0;
|
|
325
|
+
_recoveredCompletionsReconciled = true;
|
|
326
|
+
let records = [];
|
|
327
|
+
try { records = await listShellJobRecords(); } catch { return 0; }
|
|
328
|
+
let notified = 0;
|
|
329
|
+
for (const record of records) {
|
|
330
|
+
if (!record || record.terminal) continue;
|
|
331
|
+
if (getNativeTask(record.jobId)) continue; // live in this daemon
|
|
332
|
+
if (pidAlive(Number(record.pid) || 0)) continue; // survived the restart
|
|
333
|
+
const detail = {
|
|
334
|
+
...record,
|
|
335
|
+
status: 'failed',
|
|
336
|
+
error: 'daemon restarted while this shell task was running; its outcome was lost. Re-run the command if the result is still needed.',
|
|
337
|
+
};
|
|
338
|
+
try { await completeShellJobRecord(record.jobId, detail); } catch { /* best-effort */ }
|
|
339
|
+
const owner = String(record.ownerSessionId || '');
|
|
340
|
+
if (!owner) continue;
|
|
341
|
+
try {
|
|
342
|
+
const completion = buildShellCompletion(record.jobId, detail);
|
|
343
|
+
const delivered = notifyToolCompletion({
|
|
344
|
+
surface: 'shell',
|
|
345
|
+
id: record.jobId,
|
|
346
|
+
status: completion.taskStatus,
|
|
347
|
+
// The pending-queue persistence gate requires a blank-line
|
|
348
|
+
// separated result body; a bodyless bracketed envelope would
|
|
349
|
+
// be dropped. The restart notice IS the result here.
|
|
350
|
+
text: `${completion.body}\n\n${detail.error}`,
|
|
351
|
+
resultType: 'shell_task_result',
|
|
352
|
+
instruction: completion.instruction,
|
|
353
|
+
context: { callerSessionId: owner },
|
|
354
|
+
enqueueFallback: (sessionId, message, meta) => {
|
|
355
|
+
const visible = modelVisibleToolCompletionMessage(message, meta);
|
|
356
|
+
if (!visible) return false;
|
|
357
|
+
return enqueuePendingMessage(sessionId, markCompletionEntry(visible)) > 0;
|
|
358
|
+
},
|
|
359
|
+
logPrefix: 'shell-jobs-recovery',
|
|
360
|
+
});
|
|
361
|
+
if (delivered) notified += 1;
|
|
362
|
+
} catch { /* best-effort */ }
|
|
363
|
+
}
|
|
364
|
+
return notified;
|
|
365
|
+
}
|
|
366
|
+
|
|
290
367
|
export async function shutdownShellJobs(_reason = 'runtime-close', { scope = null } = {}) {
|
|
291
368
|
const scoped = Boolean(scope);
|
|
292
369
|
const ownerSessionId = scoped ? String(scope.ownerSessionId || '') : '';
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// race), isCodeGraphTool. Extracted verbatim from code-graph.mjs.
|
|
4
4
|
import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename, extname } from 'node:path';
|
|
5
5
|
import { homedir as osHomedir } from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { existsSync, statSync } from 'node:fs';
|
|
7
8
|
import { readFile } from 'node:fs/promises';
|
|
8
9
|
import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
|
|
@@ -29,6 +30,8 @@ import {
|
|
|
29
30
|
_stripEmptyArgs,
|
|
30
31
|
} from './project-root.mjs';
|
|
31
32
|
import { buildCodeGraphAsync, prewarmCodeGraph, prewarmCodeGraphSymbols } from './build.mjs';
|
|
33
|
+
import { _fileInfoFromRustRecord, _runGraphFiles } from './graph-binary.mjs';
|
|
34
|
+
import { _attachGraphRuntimeCaches } from './graph-model.mjs';
|
|
32
35
|
import {
|
|
33
36
|
_isFilesystemRootPath,
|
|
34
37
|
collectTrustedCodeGraphRoots,
|
|
@@ -87,6 +90,74 @@ function _outlineLanguageForPath(file) {
|
|
|
87
90
|
return null;
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
const _exactFileGraphInflight = new Map();
|
|
94
|
+
const _exactFileGraphCache = new Map();
|
|
95
|
+
const EXACT_FILE_GRAPH_CACHE_MAX = 64;
|
|
96
|
+
|
|
97
|
+
async function _buildExactFileGraph(cwd, abs, signal = null) {
|
|
98
|
+
const root = pathResolve(cwd);
|
|
99
|
+
const file = pathResolve(abs);
|
|
100
|
+
const rel = _graphRel(file, root);
|
|
101
|
+
const key = `${root}\0${rel}`;
|
|
102
|
+
const sourceText = await readFile(file, { encoding: 'utf8', signal: signal || undefined });
|
|
103
|
+
const sourceHash = createHash('sha256').update(sourceText).digest('hex');
|
|
104
|
+
const cached = _exactFileGraphCache.get(key);
|
|
105
|
+
if (cached?.sourceHash === sourceHash) {
|
|
106
|
+
_exactFileGraphCache.delete(key);
|
|
107
|
+
_exactFileGraphCache.set(key, cached);
|
|
108
|
+
return cached.graph;
|
|
109
|
+
}
|
|
110
|
+
const existing = _exactFileGraphInflight.get(key);
|
|
111
|
+
if (existing?.sourceHash === sourceHash) return existing.promise;
|
|
112
|
+
const pending = (async () => {
|
|
113
|
+
const records = await _runGraphFiles(root, [rel], [], signal);
|
|
114
|
+
const record = records.find((item) => _graphRel(pathResolve(root, item.rel), root) === rel);
|
|
115
|
+
if (!record) return null;
|
|
116
|
+
const info = _fileInfoFromRustRecord(record, root);
|
|
117
|
+
const node = {
|
|
118
|
+
abs: info.abs,
|
|
119
|
+
rel: info.rel,
|
|
120
|
+
lang: info.lang,
|
|
121
|
+
fingerprint: info.fingerprint,
|
|
122
|
+
rawImports: info.rawImports,
|
|
123
|
+
resolvedImportsRel: [],
|
|
124
|
+
resolvedImports: [],
|
|
125
|
+
importedBy: [],
|
|
126
|
+
packageName: info.packageName,
|
|
127
|
+
namespaceName: info.namespaceName,
|
|
128
|
+
goPackageName: info.goPackageName,
|
|
129
|
+
topLevelTypes: info.topLevelTypes,
|
|
130
|
+
tokenSymbols: info.tokenSymbols,
|
|
131
|
+
symbols: Array.isArray(info.symbols) ? info.symbols : [],
|
|
132
|
+
};
|
|
133
|
+
const graph = _attachGraphRuntimeCaches({
|
|
134
|
+
cwd: root,
|
|
135
|
+
nodes: new Map([[node.rel, node]]),
|
|
136
|
+
reverse: new Map(),
|
|
137
|
+
builtAt: Date.now(),
|
|
138
|
+
signature: info.fingerprint || '',
|
|
139
|
+
truncated: false,
|
|
140
|
+
});
|
|
141
|
+
graph._sourceTextCache.set(node.rel, {
|
|
142
|
+
fingerprint: node.fingerprint || '',
|
|
143
|
+
text: sourceText,
|
|
144
|
+
});
|
|
145
|
+
_exactFileGraphCache.delete(key);
|
|
146
|
+
_exactFileGraphCache.set(key, { sourceHash, graph });
|
|
147
|
+
while (_exactFileGraphCache.size > EXACT_FILE_GRAPH_CACHE_MAX) {
|
|
148
|
+
_exactFileGraphCache.delete(_exactFileGraphCache.keys().next().value);
|
|
149
|
+
}
|
|
150
|
+
return graph;
|
|
151
|
+
})();
|
|
152
|
+
const inflight = { sourceHash, promise: pending };
|
|
153
|
+
_exactFileGraphInflight.set(key, inflight);
|
|
154
|
+
try {
|
|
155
|
+
return await pending;
|
|
156
|
+
} finally {
|
|
157
|
+
if (_exactFileGraphInflight.get(key) === inflight) _exactFileGraphInflight.delete(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
90
161
|
async function _mapWithConcurrency(values, mapper) {
|
|
91
162
|
const out = new Array(values.length);
|
|
92
163
|
let cursor = 0;
|
|
@@ -693,9 +764,17 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
|
|
|
693
764
|
else prewarmCodeGraph(cwd);
|
|
694
765
|
return `prewarm scheduled: cwd=${cwd} symbols=${symbols.length}${symbols.length ? ` (${symbols.slice(0, 5).join(',')}${symbols.length > 5 ? `,+${symbols.length - 5}` : ''})` : ''}`;
|
|
695
766
|
}
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
767
|
+
const normFile = normalizeInputPath(args?.file);
|
|
768
|
+
const abs = normFile ? (isAbsolute(normFile) ? pathResolve(normFile) : pathResolve(cwd, normFile)) : null;
|
|
769
|
+
let exactFile = false;
|
|
770
|
+
if (abs && _outlineLanguageForPath(abs)) {
|
|
771
|
+
try { exactFile = statSync(abs).isFile(); } catch { exactFile = false; }
|
|
772
|
+
}
|
|
773
|
+
const graph = exactFile
|
|
774
|
+
? await _buildExactFileGraph(cwd, abs, signal)
|
|
775
|
+
: await buildCodeGraphAsync(cwd, signal, {
|
|
776
|
+
excludedProjectRoots: options?.excludedProjectRoots,
|
|
777
|
+
});
|
|
699
778
|
if (!graph) throw new Error(`find_symbol: cwd '${cwd}' is not an indexed/known project root or contains zero eligible files`);
|
|
700
779
|
if (options?.scopedCacheOutcome && graph.truncated) {
|
|
701
780
|
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
@@ -703,8 +782,6 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
|
|
|
703
782
|
const symbol = String(args?.symbol || '').trim();
|
|
704
783
|
const language = String(args?.language || '').trim() || null;
|
|
705
784
|
const limit = Math.max(1, Math.min(50, Number(args?.limit || 20)));
|
|
706
|
-
const normFile = normalizeInputPath(args?.file);
|
|
707
|
-
const abs = normFile ? (isAbsolute(normFile) ? pathResolve(normFile) : pathResolve(cwd, normFile)) : null;
|
|
708
785
|
const fileRel = abs ? _graphRel(abs, cwd) : null;
|
|
709
786
|
if (fileRel && !graph.nodes.get(fileRel)) {
|
|
710
787
|
return _appendSameBasenameHint(`Error: find_symbol: file not found in graph: ${normFile}`, normFile, graph);
|
|
@@ -248,7 +248,7 @@ export function _runGraphWalk(absRoot) { return _runGraphBinaryJsonl(absRoot, []
|
|
|
248
248
|
// topLevelTypes}. Rust builds the index + resolves over ALL nodes (fresh +
|
|
249
249
|
// reused) and emits fresh rels as full records, reused rels as lightweight
|
|
250
250
|
// {rel, resolvedImports, importedBy}.
|
|
251
|
-
export function _runGraphFiles(absRoot, rels, reusedMetas) {
|
|
251
|
+
export function _runGraphFiles(absRoot, rels, reusedMetas, signal = null) {
|
|
252
252
|
const lines = Array.isArray(reusedMetas)
|
|
253
253
|
? reusedMetas.map((m) => JSON.stringify({
|
|
254
254
|
rel: m.rel,
|
|
@@ -260,7 +260,7 @@ export function _runGraphFiles(absRoot, rels, reusedMetas) {
|
|
|
260
260
|
topLevelTypes: Array.isArray(m.topLevelTypes) ? m.topLevelTypes : [],
|
|
261
261
|
}))
|
|
262
262
|
: [];
|
|
263
|
-
return _runGraphBinaryJsonl(absRoot, ['--files', ...rels], lines);
|
|
263
|
+
return _runGraphBinaryJsonl(absRoot, ['--files', ...rels], lines, signal);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
// Map a Rust FileRecord (rel/lang/fp/tokens/rawImports/resolvedImports/
|
|
@@ -78,26 +78,10 @@ export function scrubLoaderVars(env) {
|
|
|
78
78
|
return _continueScrubLoaderVars(env);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// at a dead loopback endpoint. curl/wget/git-http/pip/apt honor these and
|
|
85
|
-
// fail fast on public hosts, while NO_PROXY keeps loopback traffic
|
|
86
|
-
// (task-local servers) working. Raw-socket clients bypass proxy variables —
|
|
87
|
-
// this is a uniform egress policy for the common tooling path, not a sandbox.
|
|
88
|
-
// The runtime's own provider calls are unaffected: this mutates only the
|
|
89
|
-
// child spawn env, never process.env.
|
|
90
|
-
const SHELL_EGRESS_DEAD_PROXY = 'http://127.0.0.1:1';
|
|
91
|
-
const SHELL_EGRESS_PROXY_VARS = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'FTP_PROXY', 'RSYNC_PROXY'];
|
|
81
|
+
// Web-search availability is independent from ordinary shell networking.
|
|
82
|
+
// Keep this boundary explicit so disabling the search tool never blocks
|
|
83
|
+
// package managers, source-control clients, or user-configured proxies.
|
|
92
84
|
export function applyShellEgressPolicy(env) {
|
|
93
|
-
if (!env || typeof env !== 'object') return env;
|
|
94
|
-
if (String(process.env.MIXDOG_FEATURE_WEB_SEARCH ?? '') !== '0') return env;
|
|
95
|
-
for (const name of SHELL_EGRESS_PROXY_VARS) {
|
|
96
|
-
env[name] = SHELL_EGRESS_DEAD_PROXY;
|
|
97
|
-
env[name.toLowerCase()] = SHELL_EGRESS_DEAD_PROXY;
|
|
98
|
-
}
|
|
99
|
-
env.NO_PROXY = 'localhost,127.0.0.1,::1';
|
|
100
|
-
env.no_proxy = env.NO_PROXY;
|
|
101
85
|
return env;
|
|
102
86
|
}
|
|
103
87
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { applyShellEgressPolicy } from './env-scrub.mjs';
|
|
5
|
+
|
|
6
|
+
test('web-search disabled preserves ordinary shell network environment', () => {
|
|
7
|
+
const previous = process.env.MIXDOG_FEATURE_WEB_SEARCH;
|
|
8
|
+
process.env.MIXDOG_FEATURE_WEB_SEARCH = '0';
|
|
9
|
+
try {
|
|
10
|
+
const env = {
|
|
11
|
+
HTTP_PROXY: 'http://proxy.example:8080',
|
|
12
|
+
HTTPS_PROXY: 'http://proxy.example:8080',
|
|
13
|
+
NO_PROXY: 'localhost',
|
|
14
|
+
};
|
|
15
|
+
assert.equal(applyShellEgressPolicy(env), env);
|
|
16
|
+
assert.deepEqual(env, {
|
|
17
|
+
HTTP_PROXY: 'http://proxy.example:8080',
|
|
18
|
+
HTTPS_PROXY: 'http://proxy.example:8080',
|
|
19
|
+
NO_PROXY: 'localhost',
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
if (previous === undefined) delete process.env.MIXDOG_FEATURE_WEB_SEARCH;
|
|
23
|
+
else process.env.MIXDOG_FEATURE_WEB_SEARCH = previous;
|
|
24
|
+
}
|
|
25
|
+
});
|