sweet-search 2.6.17 → 2.7.0
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 +7 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/search/agent-pack-completion.js +493 -0
- package/core/search/agent-span-client.js +87 -0
- package/core/search/agent-span-ledger.js +447 -0
- package/core/search/context-expander.js +29 -0
- package/core/search/grep-output-shaping.js +28 -0
- package/core/search/regex-dialect.js +328 -0
- package/core/search/search-format.js +96 -0
- package/core/search/search-pattern.js +62 -10
- package/core/search/search-read.js +57 -10
- package/core/search/search-server.js +375 -7
- package/core/search/session-daemon-prewarm.mjs +75 -0
- package/core/search/sweet-search.js +7 -1
- package/core/search/unread-symbol-ranking.js +90 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +242 -43
- package/mcp/read-tool.js +34 -3
- package/mcp/server.js +55 -9
- package/mcp/tool-handlers.js +60 -7
- package/package.json +9 -9
- package/scripts/init.js +30 -5
- package/scripts/uninstall.js +21 -19
|
@@ -15,6 +15,17 @@ import { LATE_INTERACTION_CONFIG } from '../infrastructure/config/index.js';
|
|
|
15
15
|
import { clearCache } from '../embedding/embedding-cache.js';
|
|
16
16
|
import { launchMaintainer } from '../indexing/maintainer-launcher.mjs';
|
|
17
17
|
import { projectSocketPath, projectPidFile, tcpPort, resolveProjectRoot } from './server-identity.js';
|
|
18
|
+
import {
|
|
19
|
+
AgentSpanLedger,
|
|
20
|
+
applyReadOmissionDecisions,
|
|
21
|
+
collectAgentShownSpans,
|
|
22
|
+
collectReadShownSpans,
|
|
23
|
+
collectSemanticShownSpans,
|
|
24
|
+
exactRereadOmissionEnabled,
|
|
25
|
+
flagEnabled,
|
|
26
|
+
resolveAgentSessionId,
|
|
27
|
+
validAgentSessionId,
|
|
28
|
+
} from './agent-span-ledger.js';
|
|
18
29
|
import {
|
|
19
30
|
upsertSelf as registryUpsertSelf,
|
|
20
31
|
touchSelf as registryTouchSelf,
|
|
@@ -22,6 +33,7 @@ import {
|
|
|
22
33
|
pruneAndList as registryPruneAndList,
|
|
23
34
|
selectEvictionTargets as registrySelectEvictionTargets,
|
|
24
35
|
} from './daemon-registry.js';
|
|
36
|
+
import { renderRegexDialectHint } from './regex-dialect.js';
|
|
25
37
|
|
|
26
38
|
// =============================================================================
|
|
27
39
|
// Server constants
|
|
@@ -36,9 +48,19 @@ export const SEARCH_SERVER_TIMEOUT_MS = 30_000;
|
|
|
36
48
|
export const SEARCH_SERVER_MAX_URL_LENGTH = 16_384;
|
|
37
49
|
export const SEARCH_SERVER_MAX_QUERY_LENGTH = 2_000;
|
|
38
50
|
export const SEARCH_SERVER_MAX_READ_PATH_LENGTH = 8_192;
|
|
51
|
+
const AGENT_SPAN_BODY_MAX_BYTES = 64 * 1024;
|
|
39
52
|
|
|
40
53
|
const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
|
|
41
54
|
|
|
55
|
+
export function resolveAgentSearchRequest(rawFormat, agentRequested = false) {
|
|
56
|
+
const explicit = AGENT_FORMATS.has(rawFormat) ? rawFormat : undefined;
|
|
57
|
+
const requested = agentRequested === true;
|
|
58
|
+
return {
|
|
59
|
+
agentFormat: explicit || (requested ? 'agent' : undefined),
|
|
60
|
+
renderText: requested && rawFormat === 'text' && !explicit,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
function canonicalProjectRoot(root) {
|
|
43
65
|
const resolved = path.resolve(root || process.cwd());
|
|
44
66
|
try {
|
|
@@ -62,6 +84,14 @@ function parseInteger(value, name) {
|
|
|
62
84
|
return n;
|
|
63
85
|
}
|
|
64
86
|
|
|
87
|
+
function parseBoundedSearchInteger(value, name, max) {
|
|
88
|
+
if (value == null || value === '') return 0;
|
|
89
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name} must be a non-negative integer`);
|
|
90
|
+
const n = Number(value);
|
|
91
|
+
if (!Number.isSafeInteger(n) || n > max) throw new Error(`${name} must be <= ${max}`);
|
|
92
|
+
return n;
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
function reusableLateInteractionIndex(searcher) {
|
|
66
96
|
const idx = searcher?.lateInteractionIndex || null;
|
|
67
97
|
if (!idx) return null;
|
|
@@ -78,6 +108,85 @@ function readSemanticError(status, message, extra = {}) {
|
|
|
78
108
|
};
|
|
79
109
|
}
|
|
80
110
|
|
|
111
|
+
export function buildAgentSpanDaemonResponse(payload, {
|
|
112
|
+
isUnixSocket = false,
|
|
113
|
+
ledger = null,
|
|
114
|
+
} = {}) {
|
|
115
|
+
if (!isUnixSocket) return readSemanticError(403, '/agent-spans is only available via Unix socket');
|
|
116
|
+
if (!ledger || !payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
117
|
+
return readSemanticError(400, 'Invalid agent-span request');
|
|
118
|
+
}
|
|
119
|
+
const { operation, sessionId, force = false } = payload;
|
|
120
|
+
if (!validAgentSessionId(sessionId)) return readSemanticError(400, 'Invalid session id');
|
|
121
|
+
if (operation === 'reset' || operation === 'drop') {
|
|
122
|
+
ledger.reset(sessionId);
|
|
123
|
+
return { status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) };
|
|
124
|
+
}
|
|
125
|
+
if (operation !== 'observe' && operation !== 'read') {
|
|
126
|
+
return readSemanticError(400, 'Invalid agent-span operation');
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(payload.spans) || payload.spans.length > 20) {
|
|
129
|
+
return readSemanticError(400, 'Invalid agent-span receipts');
|
|
130
|
+
}
|
|
131
|
+
if ((payload.query != null
|
|
132
|
+
&& (typeof payload.query !== 'string' || payload.query.length > SEARCH_SERVER_MAX_QUERY_LENGTH))
|
|
133
|
+
|| (payload.regex != null
|
|
134
|
+
&& (typeof payload.regex !== 'string' || payload.regex.length > SEARCH_SERVER_MAX_QUERY_LENGTH))) {
|
|
135
|
+
return readSemanticError(400, 'Invalid agent-span query context');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const call = ledger.beginCall(sessionId);
|
|
139
|
+
if (call == null) return readSemanticError(400, 'Invalid session id');
|
|
140
|
+
ledger.rememberQueryAtCall(sessionId, call, payload.query, payload.regex);
|
|
141
|
+
const decisions = operation === 'read'
|
|
142
|
+
? ledger.decideAndObserveAtCall(sessionId, call, payload.spans, { force: force === true })
|
|
143
|
+
: (ledger.observeAtCall(sessionId, call, payload.spans), []);
|
|
144
|
+
return {
|
|
145
|
+
status: 200,
|
|
146
|
+
contentType: 'application/json',
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
ok: true,
|
|
149
|
+
call,
|
|
150
|
+
decisions,
|
|
151
|
+
queryEvidence: ledger.queryEvidence(sessionId),
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function readBoundedJsonBody(req, maxBytes = AGENT_SPAN_BODY_MAX_BYTES) {
|
|
157
|
+
const declared = Number(req.headers?.['content-length'] || 0);
|
|
158
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
159
|
+
const err = new Error('Request body too large');
|
|
160
|
+
err.status = 413;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const chunks = [];
|
|
164
|
+
let bytes = 0;
|
|
165
|
+
for await (const chunk of req) {
|
|
166
|
+
bytes += chunk.length;
|
|
167
|
+
if (bytes > maxBytes) {
|
|
168
|
+
const err = new Error('Request body too large');
|
|
169
|
+
err.status = 413;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
chunks.push(chunk);
|
|
173
|
+
}
|
|
174
|
+
try { return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
175
|
+
} catch {
|
|
176
|
+
const err = new Error('Invalid JSON body');
|
|
177
|
+
err.status = 400;
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function beginAgentSpanUrlCall(url, ledger, { enabled = true } = {}) {
|
|
183
|
+
if (!enabled || !ledger || !flagEnabled(url.searchParams.get('exactRereadOmission'))) return null;
|
|
184
|
+
const sessionId = url.searchParams.get('agentSessionId');
|
|
185
|
+
if (!validAgentSessionId(sessionId)) return null;
|
|
186
|
+
const call = ledger.beginCall(sessionId);
|
|
187
|
+
return call == null ? null : { sessionId, call };
|
|
188
|
+
}
|
|
189
|
+
|
|
81
190
|
export async function buildReadSemanticDaemonResponse(reqUrl, {
|
|
82
191
|
isUnixSocket = false,
|
|
83
192
|
serverReady = false,
|
|
@@ -85,6 +194,7 @@ export async function buildReadSemanticDaemonResponse(reqUrl, {
|
|
|
85
194
|
searcher = null,
|
|
86
195
|
readSemanticFn = null,
|
|
87
196
|
formatReadSemanticResultFn = null,
|
|
197
|
+
agentSpanLedger = null,
|
|
88
198
|
} = {}) {
|
|
89
199
|
if (!isUnixSocket) {
|
|
90
200
|
return readSemanticError(403, '/read-semantic is only available via Unix socket');
|
|
@@ -141,6 +251,7 @@ export async function buildReadSemanticDaemonResponse(reqUrl, {
|
|
|
141
251
|
return readSemanticError(400, err.message);
|
|
142
252
|
}
|
|
143
253
|
const verbose = url.searchParams.get('verbose') === 'true';
|
|
254
|
+
const agentSpanCall = beginAgentSpanUrlCall(url, agentSpanLedger, { enabled: format === 'agent' });
|
|
144
255
|
|
|
145
256
|
try {
|
|
146
257
|
let readSemantic = readSemanticFn;
|
|
@@ -162,6 +273,11 @@ export async function buildReadSemanticDaemonResponse(reqUrl, {
|
|
|
162
273
|
verbose,
|
|
163
274
|
_lateInteractionIndex: reusableLateInteractionIndex(searcher),
|
|
164
275
|
});
|
|
276
|
+
if (agentSpanCall) {
|
|
277
|
+
const spans = collectSemanticShownSpans(result, { projectRoot: serverRoot });
|
|
278
|
+
agentSpanLedger.rememberQueryAtCall(agentSpanCall.sessionId, agentSpanCall.call, query);
|
|
279
|
+
agentSpanLedger.observeAtCall(agentSpanCall.sessionId, agentSpanCall.call, spans);
|
|
280
|
+
}
|
|
165
281
|
const body = formatReadSemanticResult(result, format);
|
|
166
282
|
return {
|
|
167
283
|
status: result?.ok === false ? 404 : 200,
|
|
@@ -183,6 +299,7 @@ export async function buildTraceDaemonResponse(reqUrl, {
|
|
|
183
299
|
serverReady = false,
|
|
184
300
|
initError = null,
|
|
185
301
|
searcher = null,
|
|
302
|
+
agentSpanLedger = null,
|
|
186
303
|
} = {}) {
|
|
187
304
|
if (!isUnixSocket) {
|
|
188
305
|
return readSemanticError(403, '/trace is only available via Unix socket');
|
|
@@ -231,6 +348,7 @@ export async function buildTraceDaemonResponse(reqUrl, {
|
|
|
231
348
|
} catch (err) {
|
|
232
349
|
return readSemanticError(400, err.message);
|
|
233
350
|
}
|
|
351
|
+
const agentSpanCall = beginAgentSpanUrlCall(url, agentSpanLedger, { enabled: !json });
|
|
234
352
|
|
|
235
353
|
try {
|
|
236
354
|
const { traceSymbol, formatStructuralContext } = await import('./search-trace.js');
|
|
@@ -243,6 +361,14 @@ export async function buildTraceDaemonResponse(reqUrl, {
|
|
|
243
361
|
maxDepth: maxDepth ?? 3,
|
|
244
362
|
tokenBudget: tokenBudget ?? null,
|
|
245
363
|
});
|
|
364
|
+
if (agentSpanCall) {
|
|
365
|
+
agentSpanLedger.rememberQueryAtCall(
|
|
366
|
+
agentSpanCall.sessionId,
|
|
367
|
+
agentSpanCall.call,
|
|
368
|
+
`${symbol} ${queryHint}`.trim(),
|
|
369
|
+
);
|
|
370
|
+
agentSpanLedger.observeAtCall(agentSpanCall.sessionId, agentSpanCall.call, []);
|
|
371
|
+
}
|
|
246
372
|
// handleTraceCli writes `console.log(json ? JSON : formatStructuralContext)`,
|
|
247
373
|
// i.e. body + exactly one trailing newline in BOTH modes.
|
|
248
374
|
const body = json ? JSON.stringify(result, null, 2) : formatStructuralContext(result);
|
|
@@ -266,6 +392,7 @@ export async function buildReadDaemonResponse(reqUrl, {
|
|
|
266
392
|
serverReady = false,
|
|
267
393
|
initError = null,
|
|
268
394
|
searcher = null,
|
|
395
|
+
agentSpanLedger = null,
|
|
269
396
|
} = {}) {
|
|
270
397
|
if (!isUnixSocket) {
|
|
271
398
|
return readSemanticError(403, '/read is only available via Unix socket');
|
|
@@ -291,6 +418,10 @@ export async function buildReadDaemonResponse(reqUrl, {
|
|
|
291
418
|
const requestedRoot = url.searchParams.get('projectRoot') || '';
|
|
292
419
|
const fmtParam = url.searchParams.get('format') || 'agent';
|
|
293
420
|
const format = (fmtParam === 'json' || fmtParam === 'raw') ? fmtParam : 'agent';
|
|
421
|
+
const exactRereadOmission = format === 'agent'
|
|
422
|
+
&& flagEnabled(url.searchParams.get('exactRereadOmission'));
|
|
423
|
+
const agentSessionId = url.searchParams.get('agentSessionId');
|
|
424
|
+
const force = url.searchParams.get('force') === 'true';
|
|
294
425
|
|
|
295
426
|
if (paths.length === 0) return readSemanticError(400, 'Missing path parameter ?path=');
|
|
296
427
|
if (paths.length > 20) return readSemanticError(413, 'read accepts at most 20 files');
|
|
@@ -331,7 +462,21 @@ export async function buildReadDaemonResponse(reqUrl, {
|
|
|
331
462
|
try {
|
|
332
463
|
const { readFiles, formatReadResults } = await import('./search-read.js');
|
|
333
464
|
const out = await readFiles(files, { projectRoot: serverRoot, includeMetadata });
|
|
334
|
-
|
|
465
|
+
let queryEvidence = null;
|
|
466
|
+
if (exactRereadOmission && agentSpanLedger && validAgentSessionId(agentSessionId)) {
|
|
467
|
+
const spans = collectReadShownSpans(out, { projectRoot: serverRoot });
|
|
468
|
+
const agentSpanCall = beginAgentSpanUrlCall(url, agentSpanLedger);
|
|
469
|
+
if (agentSpanCall) {
|
|
470
|
+
const compactDecisions = agentSpanLedger.decideAndObserveAtCall(
|
|
471
|
+
agentSpanCall.sessionId, agentSpanCall.call, spans, { force },
|
|
472
|
+
);
|
|
473
|
+
const decisions = Array.from({ length: out.files.length }, () => ({ omit: false }));
|
|
474
|
+
spans.forEach((span, index) => { decisions[span.resultIndex] = compactDecisions[index]; });
|
|
475
|
+
applyReadOmissionDecisions(out, decisions);
|
|
476
|
+
queryEvidence = agentSpanLedger.queryEvidence(agentSpanCall.sessionId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const body = formatReadResults(out, format, { surface: 'cli', queryEvidence });
|
|
335
480
|
// handleReadCli appends '\n' for non-json output (the extra process.stdout
|
|
336
481
|
// .write('\n')); json gets no trailing newline. Mirror exactly.
|
|
337
482
|
const allFailed = out.files.length > 0 && out.files.every(f => !f.ok);
|
|
@@ -437,13 +582,54 @@ function buildTextSearchResponse(results, stats, totalTime, { summary = false, m
|
|
|
437
582
|
});
|
|
438
583
|
}
|
|
439
584
|
|
|
585
|
+
const regexDialectNote = renderRegexDialectHint(stats?.regexDialectHint);
|
|
586
|
+
if (regexDialectNote) out += `${regexDialectNote}\n`;
|
|
587
|
+
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Render a packaged agent response for the native captured-output CLI. */
|
|
592
|
+
export function renderAgentSearchResponse(response) {
|
|
593
|
+
const results = response?.results || [];
|
|
594
|
+
const routing = response?.stats?.routing || {};
|
|
595
|
+
const routedMode = routing.mode || response?.mode || 'auto';
|
|
596
|
+
let out = `# sweet-search: routed=${routedMode} budget=${response?.tokenBudget ?? '?'} used=${response?.tokensUsed ?? '?'} results=${results.length} subMode=${response?.subMode ?? 'agent'}\n`;
|
|
597
|
+
if (response?.confidence) {
|
|
598
|
+
out += `# confidence=${response.confidence}${response.confidenceReason ? ` (${response.confidenceReason})` : ''}`;
|
|
599
|
+
if (response.sufficiencyVerdict) out += ` sufficient=${response.sufficiencyVerdict}`;
|
|
600
|
+
out += '\n';
|
|
601
|
+
}
|
|
602
|
+
for (const result of results) {
|
|
603
|
+
const symbol = result.symbol ? ` [${result.symbolType || 'code'}: ${result.symbol}]` : '';
|
|
604
|
+
const kind = result.expansionKind ? ` kind=${result.expansionKind}` : '';
|
|
605
|
+
const stale = result.stale ? ' STALE' : '';
|
|
606
|
+
out += `\n## #${result.rank} ${result.file}:${result.startLine}-${result.endLine}${symbol} (${result.presentation}${kind}${stale}) score=${(result.score || 0).toFixed(3)}\n`;
|
|
607
|
+
if (result.headerContext) out += `### imports\n\`\`\`\n${result.headerContext}\n\`\`\`\n`;
|
|
608
|
+
if (result.code) out += `\`\`\`\n${result.code}\n\`\`\`\n`;
|
|
609
|
+
else if (result.summary) out += `${result.summary}\n`;
|
|
610
|
+
if (result.neighbors?.rendered) {
|
|
611
|
+
out += `### related (1-hop graph, ~${result.neighbors.tokens} tok)\n${result.neighbors.rendered}\n`;
|
|
612
|
+
}
|
|
613
|
+
if (result.sameFile?.rendered) out += `${result.sameFile.rendered}\n`;
|
|
614
|
+
if (result.continuation?.rendered) {
|
|
615
|
+
out += `${result.continuation.rendered}\n`;
|
|
616
|
+
if (result.continuation.kind === 'symbol' && result.continuation.code) {
|
|
617
|
+
out += `\`\`\`\n${result.continuation.code}\n\`\`\`\n`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (result.familyManifest?.rendered) out += `${result.familyManifest.rendered}\n`;
|
|
621
|
+
}
|
|
622
|
+
if (results.length === 0) out += '(no matches)\n';
|
|
623
|
+
const regexDialectNote = renderRegexDialectHint(response?.stats?.regexDialectHint);
|
|
624
|
+
if (regexDialectNote) out += `${regexDialectNote}\n`;
|
|
440
625
|
return out;
|
|
441
626
|
}
|
|
442
627
|
|
|
443
|
-
function buildJsonSearchResponse(results, stats, totalTime) {
|
|
628
|
+
function buildJsonSearchResponse(results, stats, totalTime, extra = {}) {
|
|
444
629
|
return JSON.stringify({
|
|
445
630
|
results,
|
|
446
631
|
stats: { ...stats, server_ms: totalTime },
|
|
632
|
+
...extra,
|
|
447
633
|
});
|
|
448
634
|
}
|
|
449
635
|
|
|
@@ -525,6 +711,7 @@ export async function startServer() {
|
|
|
525
711
|
// Track request count for periodic cache clearing in long-running sessions.
|
|
526
712
|
let requestCount = 0;
|
|
527
713
|
const CACHE_CLEAR_INTERVAL = 1000; // Clear caches every 1000 requests
|
|
714
|
+
let agentSpanLedger = null;
|
|
528
715
|
|
|
529
716
|
let tcpServer;
|
|
530
717
|
let unixServer;
|
|
@@ -632,7 +819,29 @@ export async function startServer() {
|
|
|
632
819
|
console.log(`[Server] Cache cleared after ${requestCount} requests`);
|
|
633
820
|
}
|
|
634
821
|
|
|
635
|
-
if (req.method === '
|
|
822
|
+
if (req.method === 'POST' && reqUrl === '/agent-spans') {
|
|
823
|
+
if (req.socket.remoteAddress) {
|
|
824
|
+
const response = readSemanticError(403, '/agent-spans is only available via Unix socket');
|
|
825
|
+
res.writeHead(response.status, { 'Content-Type': response.contentType });
|
|
826
|
+
res.end(response.body);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
let response;
|
|
830
|
+
try {
|
|
831
|
+
const payload = await readBoundedJsonBody(req);
|
|
832
|
+
response = buildAgentSpanDaemonResponse(payload, {
|
|
833
|
+
isUnixSocket: true,
|
|
834
|
+
ledger: agentSpanLedger ||= new AgentSpanLedger(),
|
|
835
|
+
});
|
|
836
|
+
if (response.status === 200 && (payload.operation === 'read' || payload.operation === 'observe')) {
|
|
837
|
+
lastActivityMs = Date.now();
|
|
838
|
+
}
|
|
839
|
+
} catch (err) {
|
|
840
|
+
response = readSemanticError(err.status || 400, err.message || 'Invalid request body');
|
|
841
|
+
}
|
|
842
|
+
res.writeHead(response.status, { 'Content-Type': response.contentType });
|
|
843
|
+
res.end(response.body);
|
|
844
|
+
} else if (req.method === 'GET' && reqUrl.startsWith('/search?')) {
|
|
636
845
|
// Real query traffic — reset the idle-TTL clock (NOT /health or /stop).
|
|
637
846
|
lastActivityMs = Date.now();
|
|
638
847
|
if (!serverReady) {
|
|
@@ -650,9 +859,36 @@ export async function startServer() {
|
|
|
650
859
|
return;
|
|
651
860
|
}
|
|
652
861
|
const url = new URL(reqUrl, `http://localhost:${SEARCH_SERVER_PORT}`);
|
|
862
|
+
const isUnixSocket = !req.socket.remoteAddress;
|
|
863
|
+
if (isUnixSocket
|
|
864
|
+
&& (AGENT_FORMATS.has(url.searchParams.get('format'))
|
|
865
|
+
|| url.searchParams.get('agent') === 'true')
|
|
866
|
+
&& flagEnabled(url.searchParams.get('exactRereadOmission'))
|
|
867
|
+
&& !agentSpanLedger) {
|
|
868
|
+
agentSpanLedger = new AgentSpanLedger();
|
|
869
|
+
}
|
|
653
870
|
const query = url.searchParams.get('q') || '';
|
|
654
871
|
const mode = url.searchParams.get('mode') || 'auto';
|
|
655
872
|
const topK = parseInt(url.searchParams.get('k') || '10', 10);
|
|
873
|
+
const requestedRoot = url.searchParams.get('projectRoot') || '';
|
|
874
|
+
if (requestedRoot.length > SEARCH_SERVER_MAX_READ_PATH_LENGTH) {
|
|
875
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
876
|
+
res.end(JSON.stringify({ error: `Project root too long (max ${SEARCH_SERVER_MAX_READ_PATH_LENGTH} chars)` }));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (requestedRoot) {
|
|
880
|
+
const serverRoot = canonicalProjectRoot(searcher.projectRoot || process.cwd());
|
|
881
|
+
const clientRoot = canonicalProjectRoot(requestedRoot);
|
|
882
|
+
if (serverRoot !== clientRoot) {
|
|
883
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
884
|
+
res.end(JSON.stringify({
|
|
885
|
+
error: 'Daemon project root mismatch',
|
|
886
|
+
serverProjectRoot: serverRoot,
|
|
887
|
+
requestedProjectRoot: clientRoot,
|
|
888
|
+
}));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
656
892
|
|
|
657
893
|
// Additional search options
|
|
658
894
|
const expand = url.searchParams.get('expand') !== 'false';
|
|
@@ -680,6 +916,21 @@ export async function startServer() {
|
|
|
680
916
|
}
|
|
681
917
|
const maxMatches = parseInt(url.searchParams.get('maxMatches') || '0', 10);
|
|
682
918
|
const contextLines = parseInt(url.searchParams.get('contextLines') || '0', 10);
|
|
919
|
+
const fileFilter = url.searchParams.get('fileFilter') || undefined;
|
|
920
|
+
if (fileFilter && fileFilter.length > SEARCH_SERVER_MAX_READ_PATH_LENGTH) {
|
|
921
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
922
|
+
res.end(JSON.stringify({ error: `File filter too long (max ${SEARCH_SERVER_MAX_READ_PATH_LENGTH} chars)` }));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
let perFileCap; let maxFiles;
|
|
926
|
+
try {
|
|
927
|
+
perFileCap = parseBoundedSearchInteger(url.searchParams.get('perFileCap'), 'perFileCap', 1000);
|
|
928
|
+
maxFiles = parseBoundedSearchInteger(url.searchParams.get('maxFiles'), 'maxFiles', 1000);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
931
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
683
934
|
const fixedString = url.searchParams.get('fixedString') === 'true';
|
|
684
935
|
const symbolType = url.searchParams.get('type') || '';
|
|
685
936
|
const useLiteralFilter = url.searchParams.get('literalFilter') !== 'false';
|
|
@@ -688,7 +939,12 @@ export async function startServer() {
|
|
|
688
939
|
|
|
689
940
|
// Agent mode: context packaging (ColGrep agent format)
|
|
690
941
|
const rawFormat = url.searchParams.get('format');
|
|
691
|
-
const
|
|
942
|
+
const agentRequested = url.searchParams.get('agent') === 'true';
|
|
943
|
+
const { agentFormat, renderText: renderAgentText } = resolveAgentSearchRequest(
|
|
944
|
+
rawFormat,
|
|
945
|
+
agentRequested,
|
|
946
|
+
);
|
|
947
|
+
const isAgentFormat = Boolean(agentFormat);
|
|
692
948
|
const tokenBudget = url.searchParams.has('budget')
|
|
693
949
|
? parseInt(url.searchParams.get('budget'), 10)
|
|
694
950
|
: undefined;
|
|
@@ -703,6 +959,10 @@ export async function startServer() {
|
|
|
703
959
|
res.end(JSON.stringify({ error: `Query too long (max ${SEARCH_SERVER_MAX_QUERY_LENGTH} chars)` }));
|
|
704
960
|
return;
|
|
705
961
|
}
|
|
962
|
+
const agentSpanCall = beginAgentSpanUrlCall(url, agentSpanLedger, {
|
|
963
|
+
enabled: isUnixSocket && Boolean(agentFormat)
|
|
964
|
+
&& url.searchParams.get('trackAgentSpans') !== 'false',
|
|
965
|
+
});
|
|
706
966
|
|
|
707
967
|
try {
|
|
708
968
|
const start = Date.now();
|
|
@@ -712,6 +972,9 @@ export async function startServer() {
|
|
|
712
972
|
regex,
|
|
713
973
|
maxMatches,
|
|
714
974
|
contextLines,
|
|
975
|
+
fileFilter,
|
|
976
|
+
perFileCap,
|
|
977
|
+
maxFiles,
|
|
715
978
|
fixedString,
|
|
716
979
|
type: symbolType,
|
|
717
980
|
globs,
|
|
@@ -721,6 +984,7 @@ export async function startServer() {
|
|
|
721
984
|
rerank,
|
|
722
985
|
fusion,
|
|
723
986
|
useLateInteraction,
|
|
987
|
+
_isAgentFormat: isAgentFormat,
|
|
724
988
|
...(agentFormat && { format: agentFormat, tokenBudget }),
|
|
725
989
|
});
|
|
726
990
|
|
|
@@ -729,11 +993,37 @@ export async function startServer() {
|
|
|
729
993
|
// produced these results (defends against multi-repo bench reusing
|
|
730
994
|
// a stale daemon — see eval/agent-read-workflows/run-bench.js).
|
|
731
995
|
if (searchResult.format === 'agent') {
|
|
996
|
+
if (agentSpanCall) {
|
|
997
|
+
agentSpanLedger.rememberQueryAtCall(
|
|
998
|
+
agentSpanCall.sessionId,
|
|
999
|
+
agentSpanCall.call,
|
|
1000
|
+
query,
|
|
1001
|
+
regex,
|
|
1002
|
+
);
|
|
1003
|
+
const spans = collectAgentShownSpans(searchResult.results, {
|
|
1004
|
+
projectRoot: searcher.projectRoot || process.cwd(),
|
|
1005
|
+
});
|
|
1006
|
+
agentSpanLedger.observeAtCall(agentSpanCall.sessionId, agentSpanCall.call, spans);
|
|
1007
|
+
}
|
|
732
1008
|
searchResult.serverProjectRoot = searcher.projectRoot || null;
|
|
733
1009
|
searchResult.serverPid = process.pid;
|
|
734
|
-
|
|
735
|
-
|
|
1010
|
+
if (renderAgentText) {
|
|
1011
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1012
|
+
res.end(renderAgentSearchResponse(searchResult));
|
|
1013
|
+
} else {
|
|
1014
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1015
|
+
res.end(JSON.stringify(searchResult));
|
|
1016
|
+
}
|
|
736
1017
|
} else {
|
|
1018
|
+
if (agentSpanCall) {
|
|
1019
|
+
agentSpanLedger.rememberQueryAtCall(
|
|
1020
|
+
agentSpanCall.sessionId,
|
|
1021
|
+
agentSpanCall.call,
|
|
1022
|
+
query,
|
|
1023
|
+
regex,
|
|
1024
|
+
);
|
|
1025
|
+
agentSpanLedger.observeAtCall(agentSpanCall.sessionId, agentSpanCall.call, []);
|
|
1026
|
+
}
|
|
737
1027
|
let { results, stats } = searchResult;
|
|
738
1028
|
|
|
739
1029
|
// Enrich with summaries if summary mode
|
|
@@ -749,7 +1039,10 @@ export async function startServer() {
|
|
|
749
1039
|
res.end(out);
|
|
750
1040
|
} else {
|
|
751
1041
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
752
|
-
res.end(buildJsonSearchResponse(results, stats, totalTime
|
|
1042
|
+
res.end(buildJsonSearchResponse(results, stats, totalTime, {
|
|
1043
|
+
...(searchResult.fileSummary ? { fileSummary: searchResult.fileSummary } : {}),
|
|
1044
|
+
...(searchResult.familyManifest ? { familyManifest: searchResult.familyManifest } : {}),
|
|
1045
|
+
}));
|
|
753
1046
|
}
|
|
754
1047
|
}
|
|
755
1048
|
} catch (err) {
|
|
@@ -759,6 +1052,11 @@ export async function startServer() {
|
|
|
759
1052
|
} else if (req.method === 'GET' && reqUrl.startsWith('/read-semantic?')) {
|
|
760
1053
|
// Real query traffic — reset the idle-TTL clock (NOT /health or /stop).
|
|
761
1054
|
lastActivityMs = Date.now();
|
|
1055
|
+
if (!req.socket.remoteAddress
|
|
1056
|
+
&& reqUrl.includes('exactRereadOmission=true')
|
|
1057
|
+
&& !agentSpanLedger) {
|
|
1058
|
+
agentSpanLedger = new AgentSpanLedger();
|
|
1059
|
+
}
|
|
762
1060
|
// read-semantic needs the indexes — wait out the cold-start init race
|
|
763
1061
|
// (bounded) so a freshly-spawned daemon doesn't 503 the first request.
|
|
764
1062
|
await waitForServerReady();
|
|
@@ -767,12 +1065,18 @@ export async function startServer() {
|
|
|
767
1065
|
serverReady,
|
|
768
1066
|
initError,
|
|
769
1067
|
searcher,
|
|
1068
|
+
agentSpanLedger,
|
|
770
1069
|
});
|
|
771
1070
|
res.writeHead(response.status, { 'Content-Type': response.contentType });
|
|
772
1071
|
res.end(response.body);
|
|
773
1072
|
} else if (req.method === 'GET' && reqUrl.startsWith('/trace?')) {
|
|
774
1073
|
// Real query traffic — reset the idle-TTL clock (NOT /health or /stop).
|
|
775
1074
|
lastActivityMs = Date.now();
|
|
1075
|
+
if (!req.socket.remoteAddress
|
|
1076
|
+
&& reqUrl.includes('exactRereadOmission=true')
|
|
1077
|
+
&& !agentSpanLedger) {
|
|
1078
|
+
agentSpanLedger = new AgentSpanLedger();
|
|
1079
|
+
}
|
|
776
1080
|
// trace needs the code-graph — wait out the cold-start init race (bounded)
|
|
777
1081
|
// so a freshly-spawned daemon doesn't 503 the first request.
|
|
778
1082
|
await waitForServerReady();
|
|
@@ -781,17 +1085,24 @@ export async function startServer() {
|
|
|
781
1085
|
serverReady,
|
|
782
1086
|
initError,
|
|
783
1087
|
searcher,
|
|
1088
|
+
agentSpanLedger,
|
|
784
1089
|
});
|
|
785
1090
|
res.writeHead(response.status, { 'Content-Type': response.contentType });
|
|
786
1091
|
res.end(response.body);
|
|
787
1092
|
} else if (req.method === 'GET' && reqUrl.startsWith('/read?')) {
|
|
788
1093
|
// Real query traffic — reset the idle-TTL clock (NOT /health or /stop).
|
|
789
1094
|
lastActivityMs = Date.now();
|
|
1095
|
+
if (!req.socket.remoteAddress
|
|
1096
|
+
&& reqUrl.includes('exactRereadOmission=true')
|
|
1097
|
+
&& !agentSpanLedger) {
|
|
1098
|
+
agentSpanLedger = new AgentSpanLedger();
|
|
1099
|
+
}
|
|
790
1100
|
const response = await buildReadDaemonResponse(reqUrl, {
|
|
791
1101
|
isUnixSocket: !req.socket.remoteAddress,
|
|
792
1102
|
serverReady,
|
|
793
1103
|
initError,
|
|
794
1104
|
searcher,
|
|
1105
|
+
agentSpanLedger,
|
|
795
1106
|
});
|
|
796
1107
|
res.writeHead(response.status, { 'Content-Type': response.contentType });
|
|
797
1108
|
res.end(response.body);
|
|
@@ -978,6 +1289,9 @@ export async function queryServer(query, options = {}) {
|
|
|
978
1289
|
topK = 10,
|
|
979
1290
|
maxMatches = 0,
|
|
980
1291
|
contextLines = 0,
|
|
1292
|
+
fileFilter,
|
|
1293
|
+
perFileCap = 0,
|
|
1294
|
+
maxFiles = 0,
|
|
981
1295
|
fixedString = false,
|
|
982
1296
|
type = '',
|
|
983
1297
|
globs = [],
|
|
@@ -991,6 +1305,9 @@ export async function queryServer(query, options = {}) {
|
|
|
991
1305
|
mid = false,
|
|
992
1306
|
format,
|
|
993
1307
|
tokenBudget,
|
|
1308
|
+
projectRoot,
|
|
1309
|
+
trackAgentSpans = true,
|
|
1310
|
+
_isAgentFormat = false,
|
|
994
1311
|
} = options;
|
|
995
1312
|
|
|
996
1313
|
return new Promise((resolve, reject) => {
|
|
@@ -1004,6 +1321,9 @@ export async function queryServer(query, options = {}) {
|
|
|
1004
1321
|
if (regex) params.set('regex', regex);
|
|
1005
1322
|
if (maxMatches > 0) params.set('maxMatches', maxMatches.toString());
|
|
1006
1323
|
if (contextLines > 0) params.set('contextLines', contextLines.toString());
|
|
1324
|
+
if (fileFilter) params.set('fileFilter', fileFilter);
|
|
1325
|
+
if (perFileCap > 0) params.set('perFileCap', perFileCap.toString());
|
|
1326
|
+
if (maxFiles > 0) params.set('maxFiles', maxFiles.toString());
|
|
1007
1327
|
if (fixedString) params.set('fixedString', 'true');
|
|
1008
1328
|
if (type) params.set('type', type);
|
|
1009
1329
|
if (!literalFilter) params.set('literalFilter', 'false');
|
|
@@ -1016,6 +1336,16 @@ export async function queryServer(query, options = {}) {
|
|
|
1016
1336
|
if (mid) params.set('mid', 'true');
|
|
1017
1337
|
if (format && format.startsWith('agent')) params.set('format', format);
|
|
1018
1338
|
if (tokenBudget) params.set('budget', tokenBudget.toString());
|
|
1339
|
+
if (projectRoot) params.set('projectRoot', projectRoot);
|
|
1340
|
+
if (_isAgentFormat) params.set('agent', 'true');
|
|
1341
|
+
if (!trackAgentSpans) params.set('trackAgentSpans', 'false');
|
|
1342
|
+
if (trackAgentSpans && format?.startsWith('agent') && exactRereadOmissionEnabled()) {
|
|
1343
|
+
const sessionId = resolveAgentSessionId();
|
|
1344
|
+
if (sessionId) {
|
|
1345
|
+
params.set('exactRereadOmission', 'true');
|
|
1346
|
+
params.set('agentSessionId', sessionId);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1019
1349
|
|
|
1020
1350
|
// Per-project Unix socket (C3) — the canonical local transport.
|
|
1021
1351
|
const req = http.request({
|
|
@@ -1038,6 +1368,44 @@ export async function queryServer(query, options = {}) {
|
|
|
1038
1368
|
});
|
|
1039
1369
|
}
|
|
1040
1370
|
|
|
1371
|
+
/**
|
|
1372
|
+
* Query the warm daemon's read-semantic route as JSON. The caller may retain
|
|
1373
|
+
* its existing renderer while reusing the daemon's resident model/index.
|
|
1374
|
+
*
|
|
1375
|
+
* @param {{ path: string, query: string, projectRoot: string, maxChars?: number }} request
|
|
1376
|
+
* @returns {Promise<object>}
|
|
1377
|
+
*/
|
|
1378
|
+
export async function queryReadSemanticServer({ path: file, query, projectRoot, maxChars } = {}) {
|
|
1379
|
+
if (!file || !query || !projectRoot) {
|
|
1380
|
+
throw new TypeError('path, query, and projectRoot are required');
|
|
1381
|
+
}
|
|
1382
|
+
const http = await import('http');
|
|
1383
|
+
const params = new URLSearchParams({
|
|
1384
|
+
path: file,
|
|
1385
|
+
q: query,
|
|
1386
|
+
projectRoot,
|
|
1387
|
+
format: 'json',
|
|
1388
|
+
});
|
|
1389
|
+
if (maxChars > 0) params.set('maxChars', String(maxChars));
|
|
1390
|
+
|
|
1391
|
+
return new Promise((resolve, reject) => {
|
|
1392
|
+
const req = http.request({
|
|
1393
|
+
socketPath: projectSocketPath(),
|
|
1394
|
+
path: `/read-semantic?${params.toString()}`,
|
|
1395
|
+
method: 'GET',
|
|
1396
|
+
}, (res) => {
|
|
1397
|
+
let data = '';
|
|
1398
|
+
res.on('data', chunk => { data += chunk; });
|
|
1399
|
+
res.on('end', () => {
|
|
1400
|
+
try { resolve(JSON.parse(data)); }
|
|
1401
|
+
catch { reject(new Error('Invalid server response')); }
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
req.on('error', reject);
|
|
1405
|
+
req.end();
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1041
1409
|
/**
|
|
1042
1410
|
* Fetch /health from the running daemon. Returns the parsed body, or null if
|
|
1043
1411
|
* the daemon is unreachable / replies non-200.
|