mixdog 0.9.96 → 0.9.99
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 +16 -8
- 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/agent/30-explorer.md +43 -32
- 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/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- 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 +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- 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-resize.mjs +103 -12
- 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-special-files.mjs +10 -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 +94 -105
- 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/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- 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 +4 -194
- 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 +5 -34
- 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 +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- 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/http-wire.mjs +39 -4
- 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/json-metrics.mjs +94 -0
- 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 +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- 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 +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- 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 +28 -4
- 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} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -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 +9 -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} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- 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 +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- 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 +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- 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 +28 -14
- 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 +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- 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} +72 -78
- 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,3 @@
|
|
|
1
|
-
import { lstatSync, realpathSync, readdirSync, statSync } from 'fs';
|
|
2
1
|
import * as fsPromises from 'fs/promises';
|
|
3
2
|
import { readFile } from 'fs/promises';
|
|
4
3
|
import { extname } from 'path';
|
|
@@ -7,7 +6,11 @@ import { buildNotFoundHint, finalizeReadFamilyEnoentTail, tryReadFamilyEnoentRed
|
|
|
7
6
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
8
7
|
import { recordReadPathRedirect } from './snapshot-store.mjs';
|
|
9
8
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
formatBinaryReadPreviewFromBuffer,
|
|
11
|
+
inspectBinaryFile,
|
|
12
|
+
isBinaryBuffer,
|
|
13
|
+
} from './binary-file.mjs';
|
|
11
14
|
|
|
12
15
|
function snapshotBodyWasReturnedByRead(snapshot) {
|
|
13
16
|
return String(snapshot?.source || '').startsWith('read');
|
|
@@ -27,22 +30,27 @@ function withSymbolReadNote(text, args) {
|
|
|
27
30
|
// decoding. utf8-with-BOM (EF BB BF) keeps the utf-8 decoder; its leading
|
|
28
31
|
// U+FEFF is stripped for display downstream, so bomLen is reported but not
|
|
29
32
|
// applied for utf8.
|
|
33
|
+
function detectReadEncodingFromBuffer(head) {
|
|
34
|
+
const n = head?.length || 0;
|
|
35
|
+
if (n >= 2 && head[0] === 0xff && head[1] === 0xfe) {
|
|
36
|
+
return { encoding: 'utf16le', bomLen: 2 };
|
|
37
|
+
}
|
|
38
|
+
if (n >= 2 && head[0] === 0xfe && head[1] === 0xff) {
|
|
39
|
+
return { encoding: 'utf16be', bomLen: 2 };
|
|
40
|
+
}
|
|
41
|
+
if (n >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
|
|
42
|
+
return { encoding: 'utf8', bomLen: 3 };
|
|
43
|
+
}
|
|
44
|
+
return { encoding: 'utf8', bomLen: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
async function detectReadEncoding(fullPath) {
|
|
31
48
|
let fh;
|
|
32
49
|
try {
|
|
33
50
|
fh = await fsPromises.open(fullPath, 'r');
|
|
34
51
|
const head = Buffer.alloc(3);
|
|
35
52
|
const { bytesRead: n } = await fh.read(head, 0, 3, 0);
|
|
36
|
-
|
|
37
|
-
return { encoding: 'utf16le', bomLen: 2 };
|
|
38
|
-
}
|
|
39
|
-
if (n >= 2 && head[0] === 0xfe && head[1] === 0xff) {
|
|
40
|
-
return { encoding: 'utf16be', bomLen: 2 };
|
|
41
|
-
}
|
|
42
|
-
if (n >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
|
|
43
|
-
return { encoding: 'utf8', bomLen: 3 };
|
|
44
|
-
}
|
|
45
|
-
return { encoding: 'utf8', bomLen: 0 };
|
|
53
|
+
return detectReadEncodingFromBuffer(head.subarray(0, n));
|
|
46
54
|
} catch {
|
|
47
55
|
return { encoding: 'utf8', bomLen: 0 };
|
|
48
56
|
} finally {
|
|
@@ -164,7 +172,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
164
172
|
let st;
|
|
165
173
|
let _statErr;
|
|
166
174
|
try {
|
|
167
|
-
st =
|
|
175
|
+
st = options?._preflightStats?.get?.(fullPath) || await fsPromises.stat(fullPath);
|
|
168
176
|
} catch (err) {
|
|
169
177
|
// Fall through to the existing similar-file recovery path below.
|
|
170
178
|
st = null;
|
|
@@ -174,7 +182,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
174
182
|
if (st.isDirectory()) {
|
|
175
183
|
let entries = [];
|
|
176
184
|
try {
|
|
177
|
-
entries =
|
|
185
|
+
entries = (await fsPromises.readdir(fullPath, { withFileTypes: true }))
|
|
178
186
|
.slice(0, 20)
|
|
179
187
|
.map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
|
|
180
188
|
} catch { /* best-effort preview */ }
|
|
@@ -191,14 +199,14 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
191
199
|
return `Error: cannot read special file (FIFO / character / block device / socket): ${normalizeOutputPath(filePath)}`;
|
|
192
200
|
// R1+R2: realpath the resolved path so a symlink → /dev/zero (or any
|
|
193
201
|
// other blocked device, UNC, or Windows reserved name) is caught on
|
|
194
|
-
// the REAL target, not the symlink name.
|
|
195
|
-
// the entry IS a symlink first so
|
|
202
|
+
// the REAL target, not the symlink name. lstat detects whether
|
|
203
|
+
// the entry IS a symlink first so realpath is only called when
|
|
196
204
|
// it would actually differ — saves a syscall on the common case.
|
|
197
205
|
try {
|
|
198
|
-
const _lst =
|
|
206
|
+
const _lst = await fsPromises.lstat(fullPath);
|
|
199
207
|
if (_lst && typeof _lst.isSymbolicLink === 'function' && _lst.isSymbolicLink()) {
|
|
200
208
|
let _realTarget = null;
|
|
201
|
-
try { _realTarget =
|
|
209
|
+
try { _realTarget = await fsPromises.realpath(fullPath); } catch { _realTarget = null; }
|
|
202
210
|
if (_realTarget && _realTarget !== fullPath) {
|
|
203
211
|
if (isBlockedDevicePath(_realTarget))
|
|
204
212
|
return `Error: cannot read device file via symlink (would block or produce infinite output): ${normalizeOutputPath(filePath)} → ${normalizeOutputPath(_realTarget)}`;
|
|
@@ -211,7 +219,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
211
219
|
// the target stat could differ from the link stat in
|
|
212
220
|
// pathological cases (replaced under us).
|
|
213
221
|
try {
|
|
214
|
-
const _rst =
|
|
222
|
+
const _rst = await fsPromises.stat(_realTarget);
|
|
215
223
|
if (typeof isSpecialFileStat === 'function' && isSpecialFileStat(_rst))
|
|
216
224
|
return `Error: cannot read special file via symlink (FIFO / character / block device / socket): ${normalizeOutputPath(filePath)} → ${normalizeOutputPath(_realTarget)}`;
|
|
217
225
|
} catch { /* if the target is gone, let the normal path surface ENOENT */ }
|
|
@@ -430,11 +438,42 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
430
438
|
// large UTF-16LE+BOM file is recognized as text up front and routed to
|
|
431
439
|
// the bounded in-memory utf16le path below, never mis-decoded as utf-8
|
|
432
440
|
// or rejected by the utf-8 streaming/binary branch (Bug 1).
|
|
433
|
-
const
|
|
441
|
+
const _preferRangeStream = hasRangeArgs && !wantFull
|
|
442
|
+
&& (options?.forceReadRangeStream === true || st.size > READ_STREAM_RANGE_MIN_BYTES);
|
|
443
|
+
const _preferSmartStream = !hasRangeArgs && !wantFull
|
|
444
|
+
&& st.size >= READ_SMART_STREAM_MIN_BYTES;
|
|
445
|
+
const _preferBufferedRead = st.size <= READ_MAX_SIZE_BYTES
|
|
446
|
+
&& !_preferRangeStream
|
|
447
|
+
&& !_preferSmartStream;
|
|
448
|
+
let _prefetchedRawBuf = null;
|
|
449
|
+
let _prefetchedRawFromCache = false;
|
|
450
|
+
if (_preferBufferedRead) {
|
|
451
|
+
const _cachedRaw = _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
452
|
+
if (_cachedRaw) {
|
|
453
|
+
_prefetchedRawBuf = _cachedRaw;
|
|
454
|
+
_prefetchedRawFromCache = true;
|
|
455
|
+
} else {
|
|
456
|
+
try { _prefetchedRawBuf = await readFile(fullPath); } catch { _prefetchedRawBuf = null; }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const _readEnc = _prefetchedRawBuf
|
|
460
|
+
? detectReadEncodingFromBuffer(_prefetchedRawBuf.subarray(0, Math.min(3, _prefetchedRawBuf.length)))
|
|
461
|
+
: await detectReadEncoding(fullPath);
|
|
434
462
|
// UTF-16 (LE or BE) reads share one constraint: the streaming/binary
|
|
435
463
|
// paths decode chunks as utf-8, so a BOM-flagged UTF-16 file must route
|
|
436
464
|
// to the bounded in-memory decode below regardless of byte order.
|
|
437
465
|
const _isUtf16 = _readEnc.encoding === 'utf16le' || _readEnc.encoding === 'utf16be';
|
|
466
|
+
let _binaryInspection = null;
|
|
467
|
+
const _inspectBinary = async () => {
|
|
468
|
+
if (_binaryInspection) return _binaryInspection;
|
|
469
|
+
_binaryInspection = _prefetchedRawBuf
|
|
470
|
+
? {
|
|
471
|
+
isBinary: isBinaryBuffer(_prefetchedRawBuf, st.size),
|
|
472
|
+
preview: _prefetchedRawBuf.subarray(0, Math.min(256, _prefetchedRawBuf.length)),
|
|
473
|
+
}
|
|
474
|
+
: await inspectBinaryFile(fullPath, st.size);
|
|
475
|
+
return _binaryInspection;
|
|
476
|
+
};
|
|
438
477
|
if (st.size > READ_MAX_SIZE_BYTES) {
|
|
439
478
|
// .pdf/.ipynb were already dispatched up front (MEDIA-WINS), so this
|
|
440
479
|
// >10MiB branch only handles the non-media text path from here on.
|
|
@@ -450,8 +489,13 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
450
489
|
if (!hasRangeArgs) {
|
|
451
490
|
return `Error: file size ${st.size} bytes exceeds ${READ_MAX_SIZE_BYTES}-byte cap.`;
|
|
452
491
|
}
|
|
453
|
-
|
|
454
|
-
|
|
492
|
+
const _binary = await _inspectBinary();
|
|
493
|
+
if (_binary.isBinary) {
|
|
494
|
+
const { text, snapshotMeta } = formatBinaryReadPreviewFromBuffer(
|
|
495
|
+
_binary.preview,
|
|
496
|
+
normalizeOutputPath(filePath),
|
|
497
|
+
st.size,
|
|
498
|
+
);
|
|
455
499
|
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
456
500
|
_cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
457
501
|
return withSymbolReadNote(text, args);
|
|
@@ -510,11 +554,18 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
510
554
|
// unambiguous TEXT signal, so classify it as utf16le up front and skip
|
|
511
555
|
// NUL rejection. (_readEnc was detected above, before the >10MiB size
|
|
512
556
|
// branch, so the large-file path can classify utf16le.)
|
|
513
|
-
if (!_isUtf16
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
557
|
+
if (!_isUtf16) {
|
|
558
|
+
const _binary = await _inspectBinary();
|
|
559
|
+
if (_binary.isBinary) {
|
|
560
|
+
const { text, snapshotMeta } = formatBinaryReadPreviewFromBuffer(
|
|
561
|
+
_binary.preview,
|
|
562
|
+
normalizeOutputPath(filePath),
|
|
563
|
+
st.size,
|
|
564
|
+
);
|
|
565
|
+
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
566
|
+
_cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
567
|
+
return withSymbolReadNote(text, args);
|
|
568
|
+
}
|
|
518
569
|
}
|
|
519
570
|
// Whole-file reads above READ_WHOLE_FILE_MAX_BYTES use stream smart-elide
|
|
520
571
|
// (then READ_MAX_OUTPUT_BYTES truncation) instead of refusing. Absolute
|
|
@@ -588,8 +639,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
588
639
|
}
|
|
589
640
|
}
|
|
590
641
|
try {
|
|
591
|
-
const
|
|
592
|
-
const rawBuf = cachedRawBuf || await readFile(fullPath);
|
|
642
|
+
const rawBuf = _prefetchedRawBuf || await readFile(fullPath);
|
|
593
643
|
// Encoding-aware decode (fresh read AND raw-content cache hit both
|
|
594
644
|
// flow through here). For a BOM-flagged UTF-16LE file, strip the
|
|
595
645
|
// 2-byte FF FE BOM and decode as utf16le so it reverses the write
|
|
@@ -614,7 +664,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
614
664
|
// the cache + snapshot record stale bytes.
|
|
615
665
|
let _stPostRead;
|
|
616
666
|
let _readStableForRawCache = true;
|
|
617
|
-
if (
|
|
667
|
+
if (_prefetchedRawFromCache) {
|
|
618
668
|
_stPostRead = st;
|
|
619
669
|
} else {
|
|
620
670
|
try { _stPostRead = await fsPromises.stat(fullPath); } catch { _stPostRead = st; }
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { readFile, stat } from 'fs/promises';
|
|
2
2
|
import { open } from 'fs/promises';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
3
|
import { READ_MAX_SIZE_BYTES } from './read-constants.mjs';
|
|
5
4
|
import { imageBlocksFromBuffer } from './read-image-resize.mjs';
|
|
5
|
+
import { inspectPdfBuffer } from '../../../../attachments/pdf-extract.mjs';
|
|
6
6
|
|
|
7
|
-
const requireCjs = createRequire(import.meta.url);
|
|
8
7
|
const DEFAULT_READ_MAX_OUTPUT_BYTES = 100 * 1024;
|
|
9
8
|
|
|
10
9
|
// PDFs at or under this size are emitted as an Anthropic base64 document
|
|
11
10
|
// block (the model reads the rendered PDF directly): 20MB raw → ~27MB
|
|
12
11
|
// base64, which stays under the 32MB request cap.
|
|
13
|
-
// Larger PDFs fall back to
|
|
12
|
+
// Larger PDFs fall back to bounded PDF.js text extraction.
|
|
14
13
|
const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
15
14
|
|
|
16
15
|
// %PDF- magic bytes (0x25 0x50 0x44 0x46 0x2D). A document block must only be
|
|
@@ -66,37 +65,18 @@ function parsePagesArg(pagesArg) {
|
|
|
66
65
|
return { filter: { from, to } };
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
//
|
|
68
|
+
// PDF.js text extraction. Fallback path for PDFs over the document-block
|
|
70
69
|
// size cap, and the always-path when a page range is requested (a base64
|
|
71
70
|
// document block can't be page-filtered, so a narrowed read keeps using text).
|
|
72
71
|
async function extractPdfTextBody(fullPath, pageFilter, maxOutputBytes) {
|
|
73
|
-
const pdfParse = requireCjs('pdf-parse');
|
|
74
72
|
const buf = await readFile(fullPath);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// pageNumber dropped/duplicated pages when pdf.js renumbered with
|
|
81
|
-
// annotations or oddball page trees. Use pageNumber first.
|
|
82
|
-
const pageNum = (typeof pageData.pageNumber === 'number')
|
|
83
|
-
? pageData.pageNumber
|
|
84
|
-
: ((pageData._pageIndex ?? pageData.pageIndex ?? 0) + 1);
|
|
85
|
-
if (pageFilter && (pageNum < pageFilter.from || pageNum > pageFilter.to)) return Promise.resolve('');
|
|
86
|
-
return pageData.getTextContent().then((tc) => {
|
|
87
|
-
const text = tc.items.map((i) => i.str).join(' ');
|
|
88
|
-
pageTexts.push({ page: pageNum, text });
|
|
89
|
-
return text;
|
|
90
|
-
});
|
|
91
|
-
},
|
|
73
|
+
const result = await inspectPdfBuffer(buf, {
|
|
74
|
+
extractText: true,
|
|
75
|
+
maxPages: Infinity,
|
|
76
|
+
maxOutputBytes,
|
|
77
|
+
pageRange: pageFilter,
|
|
92
78
|
});
|
|
93
|
-
|
|
94
|
-
? pageTexts.map((p) => `--- Page ${p.page} ---\n${p.text}`).join('\n\n')
|
|
95
|
-
: (data.text || '');
|
|
96
|
-
if (out.length > maxOutputBytes) {
|
|
97
|
-
out = out.slice(0, maxOutputBytes) + `\n\n... [PDF output truncated at ${Math.round(maxOutputBytes / 1024)} KB; use pages param to narrow]`;
|
|
98
|
-
}
|
|
99
|
-
return out || '(no text content extracted from PDF)';
|
|
79
|
+
return result.text || '(no text content extracted from PDF)';
|
|
100
80
|
}
|
|
101
81
|
|
|
102
82
|
export async function extractPdfText(fullPath, pagesArg, { maxOutputBytes = DEFAULT_READ_MAX_OUTPUT_BYTES, textOnly = false } = {}) {
|
|
@@ -136,7 +116,7 @@ export async function extractPdfText(fullPath, pagesArg, { maxOutputBytes = DEFA
|
|
|
136
116
|
// TEXT fallback: >20MB PDFs, page-filtered reads, or non-magic files.
|
|
137
117
|
return await extractPdfTextBody(fullPath, pages.filter, maxOutputBytes);
|
|
138
118
|
} catch (err) {
|
|
139
|
-
return `Error:
|
|
119
|
+
return `Error: PDF extraction failed — ${err instanceof Error ? err.message : String(err)}`;
|
|
140
120
|
}
|
|
141
121
|
}
|
|
142
122
|
|
|
@@ -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();
|