mixdog 0.9.96 → 0.9.97
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/README.md +0 -17
- package/package.json +8 -7
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +17 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +52 -28
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +8 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +8 -8
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +154 -127
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +7 -7
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
- package/src/tui/{engine → session}/session-api.mjs +7 -60
- package/src/tui/{engine → session}/session-flow.mjs +1 -1
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
2
|
import { executeSingleReadTool } from './read-single-tool.mjs';
|
|
3
3
|
import { imageMimeForPath, readImageAsContent } from './read-image.mjs';
|
|
4
4
|
import { readEntryCoalescedDiskWindow } from './read-batch.mjs';
|
|
@@ -6,6 +6,8 @@ import { readPathStringGuardError } from './read-open.mjs';
|
|
|
6
6
|
import { parseReadLineNumberArg } from './read-args.mjs';
|
|
7
7
|
import { assertPathsReachable } from './fs-reachability.mjs';
|
|
8
8
|
import { coerceReadFamilyPathArg } from './path-utils.mjs';
|
|
9
|
+
import { readIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
|
|
10
|
+
import { currentToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
|
|
9
11
|
|
|
10
12
|
function hasLineCoordinate(path) {
|
|
11
13
|
return typeof path === 'string' && /(?:#L\d+|:\d+(?:-\d+)?(?::|$))/i.test(path);
|
|
@@ -50,15 +52,14 @@ function _guardedReadError(p, helpers) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
|
|
53
|
-
// run before any
|
|
54
|
-
//
|
|
55
|
-
// the 630s dispatch ceiling.
|
|
55
|
+
// run before any filesystem access. Besides bounding a dead mount, its Stats
|
|
56
|
+
// objects seed the real read so the common path does not immediately re-stat.
|
|
56
57
|
async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
57
58
|
const {
|
|
58
59
|
normalizeInputPath, resolveAgainstCwd,
|
|
59
60
|
} = helpers;
|
|
60
61
|
// A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
|
|
61
|
-
// REJECTED here, not skipped: skipping would let the later
|
|
62
|
+
// REJECTED here, not skipped: skipping would let the later guard/open
|
|
62
63
|
// path touch it and trigger NTLM/raw-device access or hang. Reject up front
|
|
63
64
|
// with the same message the inline guards emit.
|
|
64
65
|
// normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
|
|
@@ -71,7 +72,7 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
71
72
|
const stripped = _stripLineCoordForReach(normalizeInputPath(raw));
|
|
72
73
|
const full = resolveAgainstCwd(stripped, workDir);
|
|
73
74
|
const guardMsg = _guardedReadError(stripped, helpers) || _guardedReadError(full, helpers);
|
|
74
|
-
if (guardMsg) return guardMsg;
|
|
75
|
+
if (guardMsg) return { error: guardMsg, statsByPath: null };
|
|
75
76
|
// Dedup by resolved path so a batch repeating the same file (or the
|
|
76
77
|
// same union window) issues one stat probe, not one per entry —
|
|
77
78
|
// bounding the preflight's FS work to the distinct target set.
|
|
@@ -79,9 +80,17 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
79
80
|
seenFull.add(full);
|
|
80
81
|
candidates.push(full);
|
|
81
82
|
}
|
|
82
|
-
if (candidates.length === 0) return null;
|
|
83
|
-
try {
|
|
84
|
-
|
|
83
|
+
if (candidates.length === 0) return { error: null, statsByPath: new Map() };
|
|
84
|
+
try {
|
|
85
|
+
const stats = await assertPathsReachable(candidates);
|
|
86
|
+
const statsByPath = new Map();
|
|
87
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
88
|
+
if (stats[i]) statsByPath.set(candidates[i], stats[i]);
|
|
89
|
+
}
|
|
90
|
+
return { error: null, statsByPath };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { error: `Error: ${e?.message || e}`, statsByPath: null };
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
|
|
@@ -118,8 +127,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
121
|
-
// Reachability preflight up front (all shapes) — before
|
|
122
|
-
// image stat
|
|
130
|
+
// Reachability preflight up front (all shapes) — before
|
|
131
|
+
// readPathStringGuardError / image stat.
|
|
123
132
|
// options._skipReachPreflight: set only by the batch dispatcher on its
|
|
124
133
|
// child reads (below). The parent batch call already ran this exact
|
|
125
134
|
// preflight over EVERY candidate path in the array (_collectReachCandidates
|
|
@@ -128,8 +137,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
128
137
|
// child (readPathStringGuardError / image fast-path) — only the async
|
|
129
138
|
// reachability stat is skipped, never the security guards.
|
|
130
139
|
if (options?._skipReachPreflight !== true) {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
140
|
+
const _preflight = await _readReachPreflight(args.path, workDir, helpers);
|
|
141
|
+
if (_preflight.error) return _preflight.error;
|
|
142
|
+
options = { ...options, _preflightStats: _preflight.statsByPath };
|
|
133
143
|
}
|
|
134
144
|
// Image files (png/jpg/jpeg/gif/webp): return an MCP image block so the
|
|
135
145
|
// model can actually SEE the image. native Read does this, but mixdog's
|
|
@@ -140,7 +150,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
140
150
|
if (options?.mediaTextOnly !== true && typeof args.path === 'string' && imageMimeForPath(args.path)) {
|
|
141
151
|
const _imgNorm = normalizeInputPath(args.path);
|
|
142
152
|
// W1 H: device-file / UNC / Windows-device / ADS guards must run
|
|
143
|
-
// BEFORE the image fast-path so
|
|
153
|
+
// BEFORE the image fast-path so stat/readFile of a
|
|
144
154
|
// UNC/device path can't bypass the checks the normal read path
|
|
145
155
|
// enforces (NTLM hash leak, raw-device access, ADS).
|
|
146
156
|
if (typeof isUncPath === 'function' && isUncPath(_imgNorm))
|
|
@@ -158,7 +168,11 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
158
168
|
return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(_imgFull)}`;
|
|
159
169
|
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(_imgFull))
|
|
160
170
|
return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(_imgFull)}`;
|
|
161
|
-
const _imgResult = await readImageAsContent(
|
|
171
|
+
const _imgResult = await readImageAsContent(
|
|
172
|
+
_imgFull,
|
|
173
|
+
normalizeOutputPath(_imgNorm),
|
|
174
|
+
options?._preflightStats?.get?.(_imgFull) || null,
|
|
175
|
+
);
|
|
162
176
|
if (_imgResult) return _imgResult;
|
|
163
177
|
}
|
|
164
178
|
// Unified-read dispatch (v0.6.283+):
|
|
@@ -399,7 +413,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
399
413
|
let rangeHashes = rangeHashesByPath.get(fullPath) || [];
|
|
400
414
|
if (rangeHashes.length === 0 && mergedRanges.length > 0) {
|
|
401
415
|
try {
|
|
402
|
-
const rawLines =
|
|
416
|
+
const rawLines = (await readFile(fullPath, 'utf-8')).split('\n');
|
|
403
417
|
rangeHashes = mergedRanges.map((range) => {
|
|
404
418
|
const startIdx = Math.max(0, range.startLine - 1);
|
|
405
419
|
const endIdx = Math.min(rawLines.length, range.endLine);
|
|
@@ -608,11 +622,16 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
608
622
|
// Mode routing. A window already dropped any conflicting head/tail/summary
|
|
609
623
|
// glance above (so the window is served by executeSingleReadTool); what
|
|
610
624
|
// remains here is a mode-only read, or count/hex which are not text windows.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
625
|
+
const runReadIo = (task) => readIoAdmission.run(
|
|
626
|
+
options?.callerSessionId || options?.sessionId || currentToolExecutionOwner(),
|
|
627
|
+
task,
|
|
628
|
+
{ signal: options?.signal || options?.abortSignal || null },
|
|
629
|
+
);
|
|
630
|
+
if (args.mode === 'head') return runReadIo(() => executeChildBuiltinTool('head', { path: args.path, n: args.n }, workDir));
|
|
631
|
+
if (args.mode === 'tail') return runReadIo(() => executeChildBuiltinTool('tail', { path: args.path, n: args.n }, workDir));
|
|
632
|
+
if (args.mode === 'count') return runReadIo(() => executeChildBuiltinTool('wc', { path: args.path }, workDir));
|
|
633
|
+
if (args.mode === 'summary') return runReadIo(() => executeChildBuiltinTool('summary', { path: args.path, n: args.n, limit: args.limit }, workDir));
|
|
634
|
+
if (args.mode === 'hex') return runReadIo(() => executeChildBuiltinTool('hex', { path: args.path, n: args.n, offset: args.offset }, workDir));
|
|
635
|
+
return runReadIo(() => executeSingleReadTool(args, workDir, readStateScope, options, helpers));
|
|
617
636
|
|
|
618
637
|
}
|
|
@@ -423,7 +423,9 @@ function spawnRg(argsList, execOptions) {
|
|
|
423
423
|
// spawn below, so queue-wait time is excluded from the 20s deadline. No
|
|
424
424
|
// node guardian is started: rg owns its full SIGTERM→grace→force-kill +
|
|
425
425
|
// force-settle teardown, so the 1:1 guardian process was pure overhead.
|
|
426
|
-
return acquireChildSpawnSlot(gateSignal, 'search'
|
|
426
|
+
return acquireChildSpawnSlot(gateSignal, 'search', {
|
|
427
|
+
ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
|
|
428
|
+
}).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
427
429
|
// Probe after acquisition: this spawn counts toward inflight. The
|
|
428
430
|
// answer is best-effort and only selects the per-process rg cap.
|
|
429
431
|
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
@@ -605,7 +607,9 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
605
607
|
// Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
|
|
606
608
|
// excluded from the rg timeout (armed after spawn). releaseSlot is fired
|
|
607
609
|
// once via .finally on every settle path.
|
|
608
|
-
return acquireChildSpawnSlot(gateSignal, 'search'
|
|
610
|
+
return acquireChildSpawnSlot(gateSignal, 'search', {
|
|
611
|
+
ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
|
|
612
|
+
}).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
609
613
|
// As above, probe only after this spawn has acquired its gate slot.
|
|
610
614
|
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
611
615
|
cwd: execOptions?.cwd,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// code-graph.mjs.
|
|
5
5
|
import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename } from 'node:path';
|
|
6
6
|
import { homedir as osHomedir } from 'node:os';
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, statSync } from 'node:fs';
|
|
8
8
|
import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
|
|
9
9
|
import { findFileByBasename } from '../builtin/path-diagnostics.mjs';
|
|
10
10
|
import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
_CALLEES_BRACE_LANGS,
|
|
41
41
|
_formatRelated,
|
|
42
42
|
_formatImpact,
|
|
43
|
+
_impactSourceNodes,
|
|
43
44
|
_resolveReferenceLanguageNode,
|
|
45
|
+
_prewarmSourceTextNodes,
|
|
44
46
|
_prewarmReferenceSourceText,
|
|
45
47
|
_cheapReferenceSearch,
|
|
46
48
|
_formatCallerReferences,
|
|
@@ -375,13 +377,19 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
375
377
|
const depsAll = [...(graph.reverse.get(depRel) || [])].sort();
|
|
376
378
|
if (!depsAll.length) return '(no dependents)';
|
|
377
379
|
const deps = depsAll.slice(0, GRAPH_LIST_CAP);
|
|
380
|
+
await _prewarmSourceTextNodes(
|
|
381
|
+
graph,
|
|
382
|
+
deps.map((dep) => graph.nodes.get(dep)).filter(Boolean),
|
|
383
|
+
{ signal },
|
|
384
|
+
);
|
|
378
385
|
const basename = depRel.split('/').pop();
|
|
379
386
|
const stem = basename.replace(/\.[^/.]+$/, '');
|
|
380
387
|
const enriched = deps.map((dep) => {
|
|
381
388
|
const depNode = graph.nodes.get(dep);
|
|
382
389
|
if (!depNode) return dep;
|
|
383
|
-
|
|
384
|
-
|
|
390
|
+
const cached = graph._sourceTextCache?.get(depNode.rel);
|
|
391
|
+
if (!cached || cached.fingerprint !== (depNode.fingerprint || '')) return dep;
|
|
392
|
+
const text = cached.text;
|
|
385
393
|
const linesArr = text.split(/\r?\n/);
|
|
386
394
|
for (let i = 0; i < linesArr.length; i++) {
|
|
387
395
|
const ln = linesArr[i];
|
|
@@ -407,6 +415,8 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
407
415
|
if (mode === 'impact') {
|
|
408
416
|
if (!node) return _appendSameBasenameHint(`Error: code_graph impact: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
|
|
409
417
|
const targetSymbol = String(args?.symbol || '').trim();
|
|
418
|
+
await _prewarmSourceTextNodes(graph, [node], { signal });
|
|
419
|
+
await _prewarmSourceTextNodes(graph, _impactSourceNodes(node, graph, targetSymbol), { signal });
|
|
410
420
|
return _formatImpact(node, graph, cwd, targetSymbol);
|
|
411
421
|
}
|
|
412
422
|
|
|
@@ -425,6 +435,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
425
435
|
if (!_CALLEES_BRACE_LANGS.has(declHit.lang)) {
|
|
426
436
|
return `(callees unsupported for ${declHit.lang})`;
|
|
427
437
|
}
|
|
438
|
+
await _prewarmSourceTextNodes(graph, [graph.nodes.get(declHit.rel)].filter(Boolean), { signal });
|
|
428
439
|
const rows = _extractCallees(graph, declHit, cwd, {
|
|
429
440
|
cap: 200,
|
|
430
441
|
callerSymbol: symbol,
|
|
@@ -438,9 +449,11 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
438
449
|
|
|
439
450
|
if (mode === 'symbols') {
|
|
440
451
|
if (!node) return _appendSameBasenameHint(`Error: code_graph symbols: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
452
|
+
await _prewarmSourceTextNodes(graph, [node], { signal });
|
|
453
|
+
const cached = graph._sourceTextCache?.get(node.rel);
|
|
454
|
+
return cached && cached.fingerprint === (node.fingerprint || '')
|
|
455
|
+
? _extractSymbolsCheap(cached.text, node.lang)
|
|
456
|
+
: '(no symbols)';
|
|
444
457
|
}
|
|
445
458
|
|
|
446
459
|
if (mode === 'find_symbol') {
|
|
@@ -449,6 +462,15 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
449
462
|
const language = String(args?.language || '').trim() || null;
|
|
450
463
|
const limit = Math.max(1, Math.min(50, Number(args?.limit || 20)));
|
|
451
464
|
if (rel && !node) return _appendSameBasenameHint(`Error: code_graph find_symbol: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
|
|
465
|
+
if (args?.body !== false) {
|
|
466
|
+
const hits = _findSymbolHits(graph, symbol, { language });
|
|
467
|
+
const primary = hits.find((hit) => hit.declarationLike) || hits[0];
|
|
468
|
+
await _prewarmSourceTextNodes(
|
|
469
|
+
graph,
|
|
470
|
+
[primary?.rel ? graph.nodes.get(primary.rel) : null].filter(Boolean),
|
|
471
|
+
{ signal },
|
|
472
|
+
);
|
|
473
|
+
}
|
|
452
474
|
return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel: rel, body: args?.body !== false });
|
|
453
475
|
}
|
|
454
476
|
|
|
@@ -461,6 +483,14 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
461
483
|
const keyword = String(args?.symbol || '').trim();
|
|
462
484
|
const keywords = symbolsList.length ? symbolsList : (keyword ? [keyword] : []);
|
|
463
485
|
if (!keywords.length) throw new Error('code_graph symbol_search: "symbol" (or "symbols[]") is required.');
|
|
486
|
+
// Native graph symbols answer without source text. Nodes lacking native
|
|
487
|
+
// symbols fall back to cheap text extraction, so warm exactly that subset
|
|
488
|
+
// asynchronously before the synchronous formatter scans it.
|
|
489
|
+
await _prewarmSourceTextNodes(
|
|
490
|
+
graph,
|
|
491
|
+
[...graph.nodes.values()].filter((candidate) => !Array.isArray(candidate?.symbols) || candidate.symbols.length === 0),
|
|
492
|
+
{ signal },
|
|
493
|
+
);
|
|
464
494
|
if (keywords.length === 1) {
|
|
465
495
|
return _searchSymbolsByKeyword(graph, keywords[0], cwd, { language, limit });
|
|
466
496
|
}
|
|
@@ -487,6 +517,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
487
517
|
}
|
|
488
518
|
}
|
|
489
519
|
const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
|
|
520
|
+
if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
|
|
490
521
|
const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
|
|
491
522
|
if (rel && resolved.kind === 'file-not-found') {
|
|
492
523
|
return _appendSameBasenameHint(`Error: code_graph references: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
|
|
@@ -501,7 +532,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
501
532
|
const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
502
533
|
? Math.min(500, Math.floor(rawLimit))
|
|
503
534
|
: null;
|
|
504
|
-
const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
535
|
+
const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
|
|
505
536
|
const refResult = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix, nodes: _refNodes });
|
|
506
537
|
return narrowedByCaller ? refResult : _augmentNoHitDiagnostic(refResult, '(no references)', graph, cwd, symbol);
|
|
507
538
|
}
|
|
@@ -517,6 +548,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
517
548
|
}
|
|
518
549
|
}
|
|
519
550
|
const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
|
|
551
|
+
if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
|
|
520
552
|
const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
|
|
521
553
|
if (rel && resolved.kind === 'file-not-found') {
|
|
522
554
|
return _appendSameBasenameHint(`Error: code_graph callers: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
|
|
@@ -531,7 +563,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
531
563
|
const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
532
564
|
? Math.min(500, Math.floor(rawLimit))
|
|
533
565
|
: null;
|
|
534
|
-
const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
566
|
+
const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
|
|
535
567
|
const depth = Math.max(1, Math.min(5, Math.floor(Number(args?.depth) || 1)));
|
|
536
568
|
if (depth > 1) {
|
|
537
569
|
return _formatTransitiveCallers(graph, symbol, cwd, { language: lang, depth, page: args?.page });
|
|
@@ -576,12 +608,23 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
|
|
|
576
608
|
if (!symbol) {
|
|
577
609
|
if (fileRel) {
|
|
578
610
|
const node = graph.nodes.get(fileRel);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return
|
|
611
|
+
await _prewarmSourceTextNodes(graph, [node], { signal });
|
|
612
|
+
const cached = graph._sourceTextCache?.get(node.rel);
|
|
613
|
+
return cached && cached.fingerprint === (node.fingerprint || '')
|
|
614
|
+
? _extractSymbolsCheap(cached.text, node.lang)
|
|
615
|
+
: '(no symbols)';
|
|
582
616
|
}
|
|
583
617
|
throw new Error('find_symbol: provide "symbol" (to locate) or "file" (to list its symbols).');
|
|
584
618
|
}
|
|
619
|
+
if (args?.body !== false) {
|
|
620
|
+
const hits = _findSymbolHits(graph, symbol, { language });
|
|
621
|
+
const primary = hits.find((hit) => hit.declarationLike) || hits[0];
|
|
622
|
+
await _prewarmSourceTextNodes(
|
|
623
|
+
graph,
|
|
624
|
+
[primary?.rel ? graph.nodes.get(primary.rel) : null].filter(Boolean),
|
|
625
|
+
{ signal },
|
|
626
|
+
);
|
|
627
|
+
}
|
|
585
628
|
return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel, body: args?.body !== false });
|
|
586
629
|
}
|
|
587
630
|
|
|
@@ -632,6 +675,7 @@ export async function resolveSymbolReadSpan(cwd, { symbol, path = null, language
|
|
|
632
675
|
}
|
|
633
676
|
if (!Number.isFinite(endLine) || endLine < startLine) {
|
|
634
677
|
const node = graph.nodes.get(primary.rel);
|
|
678
|
+
await _prewarmSourceTextNodes(graph, [node].filter(Boolean));
|
|
635
679
|
const srcText = node ? _getSourceTextForNode(graph, node) : null;
|
|
636
680
|
const inferred = srcText ? _inferSpanEndByIndent(srcText.split('\n'), startLine) : null;
|
|
637
681
|
if (inferred) {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Symbol search / callers / callees / references / impact query layer over a
|
|
3
3
|
// built graph. Pure over {graph,cwd,args}; owns no cache state. Extracted
|
|
4
4
|
// verbatim from code-graph.mjs.
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
5
|
import { _isJsLike } from './lang-predicates.mjs';
|
|
7
6
|
import { _maskNonCodeText } from './text-mask.mjs';
|
|
8
7
|
import {
|
|
@@ -61,6 +60,18 @@ function _collectImpactSymbols(node, graph) {
|
|
|
61
60
|
return [...names];
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
export function _impactSourceNodes(node, graph, targetSymbol = '') {
|
|
64
|
+
const symbols = targetSymbol ? [targetSymbol] : _collectImpactSymbols(node, graph).slice(0, 8);
|
|
65
|
+
const out = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
for (const candidate of [node, ...symbols.flatMap((symbol) => _lookupCandidateNodes(graph, symbol, node?.lang || null))]) {
|
|
68
|
+
if (!candidate?.rel || seen.has(candidate.rel)) continue;
|
|
69
|
+
seen.add(candidate.rel);
|
|
70
|
+
out.push(candidate);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
function _buildImpactSummary(node, graph, cwd, targetSymbol = '') {
|
|
65
76
|
const imports = node.resolvedImports.map((p) => _graphRel(p, cwd));
|
|
66
77
|
const dependents = [...(graph.reverse.get(node.rel) || [])].sort();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Symbol search / callers / callees / references / impact query layer over a
|
|
2
2
|
// built graph. Pure over {graph,cwd,args}; owns no cache state. Extracted
|
|
3
3
|
// verbatim from code-graph.mjs.
|
|
4
|
-
import {
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { codeGraphSourceIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
|
|
5
6
|
import { _isJsLike } from './lang-predicates.mjs';
|
|
6
7
|
import { _maskNonCodeText } from './text-mask.mjs';
|
|
7
8
|
import {
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
_keywordMatchesSymbolName,
|
|
31
32
|
} from './keyword-match.mjs';
|
|
32
33
|
|
|
33
|
-
export { _formatRelated, _formatImpact, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
|
|
34
|
+
export { _formatRelated, _formatImpact, _impactSourceNodes, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
|
|
34
35
|
export function _extractCallees(graph, declHit, _cwd, { cap = 200, callerSymbol = null, language = null } = {}) {
|
|
35
36
|
if (!declHit || !_CALLEES_BRACE_LANGS.has(declHit.lang)) return [];
|
|
36
37
|
const declNode = graph.nodes.get(declHit.rel);
|
|
@@ -253,28 +254,64 @@ export function _formatCalleeRow(row) {
|
|
|
253
254
|
const enclosing = row.enclosing ? `(in ${row.enclosing})` : '(in ?)';
|
|
254
255
|
return `${row.name}\t${callsite}\t${decl}\t${enclosing}`;
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
const CODE_GRAPH_SOURCE_READ_CONCURRENCY = Math.max(
|
|
258
|
+
1,
|
|
259
|
+
Math.min(32, Math.floor(Number(process.env.MIXDOG_CODE_GRAPH_SOURCE_READ_CONCURRENCY) || 8)),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
export async function _prewarmSourceTextNodes(graph, nodes, {
|
|
263
|
+
concurrency = CODE_GRAPH_SOURCE_READ_CONCURRENCY,
|
|
264
|
+
readFileImpl = readFile,
|
|
265
|
+
signal = null,
|
|
266
|
+
ownerKey = null,
|
|
267
|
+
} = {}) {
|
|
268
|
+
const sourceNodes = [];
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
271
|
+
if (!node?.rel || !node?.abs || seen.has(node.rel)) continue;
|
|
272
|
+
seen.add(node.rel);
|
|
273
|
+
sourceNodes.push(node);
|
|
274
|
+
}
|
|
263
275
|
const uncached = [];
|
|
264
|
-
for (const node of
|
|
276
|
+
for (const node of sourceNodes) {
|
|
265
277
|
const cached = graph._sourceTextCache?.get(node.rel);
|
|
266
278
|
if (!cached || cached.fingerprint !== (node.fingerprint || '')) {
|
|
267
279
|
uncached.push(node);
|
|
268
280
|
}
|
|
269
281
|
}
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
let next = 0;
|
|
283
|
+
const worker = async () => {
|
|
284
|
+
while (!signal?.aborted) {
|
|
285
|
+
const index = next++;
|
|
286
|
+
if (index >= uncached.length) return;
|
|
287
|
+
const node = uncached[index];
|
|
288
|
+
try {
|
|
289
|
+
const text = await codeGraphSourceIoAdmission.run(
|
|
290
|
+
ownerKey,
|
|
291
|
+
() => readFileImpl(node.abs, 'utf8'),
|
|
292
|
+
{ signal },
|
|
293
|
+
);
|
|
294
|
+
graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
|
|
295
|
+
} catch { /* skip unreadable/aborted file */ }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const workerCount = Math.min(
|
|
299
|
+
Math.max(1, Math.floor(Number(concurrency) || CODE_GRAPH_SOURCE_READ_CONCURRENCY)),
|
|
300
|
+
Math.max(1, uncached.length),
|
|
301
|
+
);
|
|
302
|
+
if (uncached.length > 0) {
|
|
303
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
304
|
+
}
|
|
305
|
+
return sourceNodes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function _prewarmReferenceSourceText(graph, symbol, language, options = {}) {
|
|
309
|
+
const candidateNodes = _lookupCandidateNodes(graph, symbol, language);
|
|
310
|
+
// Return the resolved candidate set so the immediately-following
|
|
311
|
+
// _cheapReferenceSearch (references/callers dispatch) can reuse it instead
|
|
312
|
+
// of recomputing _lookupCandidateNodes for the same (symbol, language) —
|
|
313
|
+
// which on a token-index miss is a full-graph scan run twice per symbol.
|
|
314
|
+
await _prewarmSourceTextNodes(graph, candidateNodes, options);
|
|
278
315
|
return candidateNodes;
|
|
279
316
|
}
|
|
280
317
|
|
|
@@ -14,6 +14,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
14
14
|
limit: { type: 'number', minimum: 1, description: 'Max results.' },
|
|
15
15
|
depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
|
|
16
16
|
page: { type: 'number', minimum: 1, description: 'Caller page.' },
|
|
17
|
+
cwd: { type: 'string', description: 'Explicit root.' },
|
|
17
18
|
},
|
|
18
19
|
required: ['mode'],
|
|
19
20
|
additionalProperties: false,
|
|
@@ -120,8 +120,7 @@ export function scrubRuntimeRootVars(env) {
|
|
|
120
120
|
// Daemon IDENTITY leak: user-facing children must not inherit host/worker
|
|
121
121
|
// ownership or the daemon's terminal-lead PID. Internal daemon/worker
|
|
122
122
|
// spawns set every identity value explicitly after this boundary.
|
|
123
|
-
delete env.
|
|
124
|
-
delete env.MIXDOG_CHANNEL_DAEMON;
|
|
123
|
+
delete env.MIXDOG_DAEMON_HOST;
|
|
125
124
|
delete env.MIXDOG_WORKER_MODE;
|
|
126
125
|
delete env.MIXDOG_DAEMON_SPAWNED_FOR;
|
|
127
126
|
delete env.MIXDOG_SUPERVISOR_PID;
|
|
@@ -43,11 +43,11 @@ export function _resolvePwshPoolTarget({
|
|
|
43
43
|
if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
|
|
44
44
|
}
|
|
45
45
|
if (platform !== 'win32') return 0;
|
|
46
|
-
// Tools now execute in the machine-global
|
|
46
|
+
// Tools now execute in the machine-global daemon. Keep client/test
|
|
47
47
|
// processes at zero by default, but let that ONE host maintain a short-lived
|
|
48
48
|
// warm burst sized to the machine instead of making every shell call pay the
|
|
49
49
|
// ~300ms pwsh startup cost.
|
|
50
|
-
if (env?.
|
|
50
|
+
if (env?.MIXDOG_DAEMON_HOST !== '1') return 0;
|
|
51
51
|
let cores = Number(parallelism);
|
|
52
52
|
if (!Number.isFinite(cores) || cores < 1) {
|
|
53
53
|
try { cores = availableParallelism(); } catch { cores = 1; }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// mode gating, binary resolution, prewarm/idle lifecycle, and the char-indexed
|
|
3
3
|
// EDIT client. Split out of patch.mjs; behavior is identical.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Executor: NATIVE-ONLY. Every supported apply/edit case is dispatched to the
|
|
6
6
|
// mixdog-patch Rust engine via the persistent stdio server. There is NO JS
|
|
7
7
|
// apply fallback: unsupported / unsafe input returns a clean Error string.
|
|
8
8
|
|