sweet-search 2.6.16 → 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/graph/structural-context-format.js +5 -0
- package/core/graph/structural-context.js +13 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/language-patterns/registry-core.js +14 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/infrastructure/structural-context-repository.js +73 -7
- package/core/infrastructure/structural-context-utils.js +11 -0
- package/core/infrastructure/structural-qualified-resolution.js +29 -0
- package/core/infrastructure/tree-sitter-provider.js +77 -3
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
- 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 +249 -45
- 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/inject-agent-instructions.js +4 -2
- package/scripts/uninstall.js +21 -19
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readFileSync,
|
|
28
28
|
readdirSync,
|
|
29
29
|
openSync,
|
|
30
|
+
appendFileSync,
|
|
30
31
|
writeSync,
|
|
31
32
|
closeSync,
|
|
32
33
|
unlinkSync,
|
|
@@ -36,6 +37,8 @@ import { dirname, join } from 'node:path';
|
|
|
36
37
|
import { fileURLToPath } from 'node:url';
|
|
37
38
|
import { launchMaintainer } from '../indexing/maintainer-launcher.mjs';
|
|
38
39
|
import { projectSocketPath, projectPidFile } from './server-identity.js';
|
|
40
|
+
import { sendAgentSpanOperation } from './agent-span-client.js';
|
|
41
|
+
import { validAgentSessionId } from './agent-span-ledger.js';
|
|
39
42
|
|
|
40
43
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
44
|
|
|
@@ -48,10 +51,33 @@ const LOCK_PATH = process.env.SWEET_SEARCH_PREWARM_LOCK || '/tmp/sweet-search-pr
|
|
|
48
51
|
const SOCKET_PROBE_TIMEOUT_MS = Number(process.env.SWEET_SEARCH_PREWARM_PROBE_MS ?? 300);
|
|
49
52
|
|
|
50
53
|
const verbose = !!process.env.SWEET_SEARCH_PREWARM_VERBOSE;
|
|
54
|
+
const hookMode = process.argv.includes('--agent-session-drop')
|
|
55
|
+
? 'drop'
|
|
56
|
+
: (process.argv.includes('--agent-session-hook') ? 'session-start' : null);
|
|
51
57
|
const log = (msg) => {
|
|
52
58
|
if (verbose) process.stderr.write(`[sweet-search prewarm] ${msg}\n`);
|
|
53
59
|
};
|
|
54
60
|
|
|
61
|
+
function readHookPayload() {
|
|
62
|
+
if (!hookMode) return null;
|
|
63
|
+
try {
|
|
64
|
+
const raw = readFileSync(0, 'utf8');
|
|
65
|
+
if (!raw || Buffer.byteLength(raw) > 64 * 1024) return null;
|
|
66
|
+
const payload = JSON.parse(raw);
|
|
67
|
+
return payload && typeof payload === 'object' ? payload : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function persistClaudeSessionId(sessionId) {
|
|
74
|
+
const envFile = process.env.CLAUDE_ENV_FILE;
|
|
75
|
+
if (!envFile || !validAgentSessionId(sessionId)) return;
|
|
76
|
+
const shellQuoted = `'${sessionId.replace(/'/g, `'"'"'`)}'`;
|
|
77
|
+
try { appendFileSync(envFile, `export SWEET_SEARCH_SESSION_ID=${shellQuoted}\n`, 'utf8'); }
|
|
78
|
+
catch (err) { log(`session env non-fatal: ${err?.message || err}`); }
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
function pageCacheSweepEnabled() {
|
|
56
82
|
const v = String(process.env.SWEET_SEARCH_PREWARM_PAGE_CACHE || '').trim().toLowerCase();
|
|
57
83
|
return v !== '0' && v !== 'false' && v !== 'off' && v !== 'no';
|
|
@@ -184,6 +210,16 @@ async function daemonHealthy() {
|
|
|
184
210
|
return socketResponsive(SOCKET_PATH, SOCKET_PROBE_TIMEOUT_MS);
|
|
185
211
|
}
|
|
186
212
|
|
|
213
|
+
async function waitForResponsiveSocket(timeoutMs = SOCKET_PROBE_TIMEOUT_MS) {
|
|
214
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
215
|
+
do {
|
|
216
|
+
if (await socketResponsive(SOCKET_PATH, Math.min(100, timeoutMs))) return true;
|
|
217
|
+
if (Date.now() >= deadline) break;
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
219
|
+
} while (Date.now() < deadline);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
187
223
|
/**
|
|
188
224
|
* Acquire an exclusive lock so only one concurrent SessionStart spawns a
|
|
189
225
|
* daemon. Returns the file descriptor on success, null on failure. Handles
|
|
@@ -268,6 +304,34 @@ if (process.env.SWEET_SEARCH_PAGE_CACHE_SWEEP === '1') {
|
|
|
268
304
|
process.exit(0);
|
|
269
305
|
}
|
|
270
306
|
|
|
307
|
+
const hookPayload = readHookPayload();
|
|
308
|
+
const hookSessionId = validAgentSessionId(hookPayload?.session_id)
|
|
309
|
+
? hookPayload.session_id
|
|
310
|
+
: null;
|
|
311
|
+
|
|
312
|
+
if (hookMode === 'drop') {
|
|
313
|
+
if (hookSessionId) {
|
|
314
|
+
await sendAgentSpanOperation({ operation: 'drop', sessionId: hookSessionId }, { timeoutMs: 750 });
|
|
315
|
+
}
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (hookMode === 'session-start' && hookSessionId) {
|
|
320
|
+
persistClaudeSessionId(hookSessionId);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resetRequested = hookMode === 'session-start'
|
|
324
|
+
&& hookSessionId
|
|
325
|
+
&& (hookPayload?.source === 'clear' || hookPayload?.source === 'compact');
|
|
326
|
+
let resetComplete = false;
|
|
327
|
+
if (resetRequested) {
|
|
328
|
+
const response = await sendAgentSpanOperation(
|
|
329
|
+
{ operation: 'reset', sessionId: hookSessionId },
|
|
330
|
+
{ timeoutMs: 750 },
|
|
331
|
+
);
|
|
332
|
+
resetComplete = response?.ok === true;
|
|
333
|
+
}
|
|
334
|
+
|
|
271
335
|
// The search server and the index maintainer are independent: a stuck/already
|
|
272
336
|
// running server must not stop the maintainer from starting, and vice versa.
|
|
273
337
|
// Each is isolated in its own try so one failing never blocks the other.
|
|
@@ -283,6 +347,17 @@ try {
|
|
|
283
347
|
log(`server prewarm non-fatal: ${err?.message || err}`);
|
|
284
348
|
}
|
|
285
349
|
|
|
350
|
+
if (resetRequested && !resetComplete) {
|
|
351
|
+
// A compact/clear can race a daemon cold start. The first attempt happens
|
|
352
|
+
// before unrelated prewarm work; this is the single bounded retry after the
|
|
353
|
+
// server has had a chance to bind its socket.
|
|
354
|
+
await waitForResponsiveSocket(SOCKET_PROBE_TIMEOUT_MS);
|
|
355
|
+
await sendAgentSpanOperation(
|
|
356
|
+
{ operation: 'reset', sessionId: hookSessionId },
|
|
357
|
+
{ timeoutMs: 750 },
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
286
361
|
try {
|
|
287
362
|
// Delegate to the shared launcher (core/indexing/maintainer-launcher.mjs).
|
|
288
363
|
// The hook is a best-effort convenience layer; the durable guarantee lives in
|
|
@@ -706,7 +706,12 @@ export class SweetSearch {
|
|
|
706
706
|
results = grepResult.results;
|
|
707
707
|
Object.assign(stats, grepResult.stats);
|
|
708
708
|
stats.total_ms = grepResult.stats.total_ms ?? (Date.now() - start);
|
|
709
|
-
return {
|
|
709
|
+
return {
|
|
710
|
+
results,
|
|
711
|
+
stats,
|
|
712
|
+
...(grepResult.fileSummary ? { fileSummary: grepResult.fileSummary } : {}),
|
|
713
|
+
...(grepResult.familyManifest ? { familyManifest: grepResult.familyManifest } : {}),
|
|
714
|
+
};
|
|
710
715
|
}
|
|
711
716
|
case 'pattern': {
|
|
712
717
|
const patternResult = await this.patternSearch(query, routing, options);
|
|
@@ -881,6 +886,7 @@ export class SweetSearch {
|
|
|
881
886
|
locationMap: null,
|
|
882
887
|
projectRoot: this.projectRoot,
|
|
883
888
|
ablations: options.ablations,
|
|
889
|
+
_isAgentFormat: true,
|
|
884
890
|
});
|
|
885
891
|
__ptEnd('packageForAgent', __t_pkg);
|
|
886
892
|
// Preserve the underlying retrieval stats so callers can inspect both layers
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-only relevance ordering for the ss-read unread-symbol trailer.
|
|
3
|
+
*
|
|
4
|
+
* The caller supplies symbols already loaded from the selected file's index.
|
|
5
|
+
* This module only reallocates the existing five display slots; it performs no
|
|
6
|
+
* I/O and never changes the total symbol count represented by "+N more".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { containsToken, informativeSubtokens } from './query-sufficiency.js';
|
|
10
|
+
|
|
11
|
+
function normalizeEvidence(queryEvidence) {
|
|
12
|
+
const anchors = Array.isArray(queryEvidence?.anchors)
|
|
13
|
+
? queryEvidence.anchors.filter((value) => typeof value === 'string' && value.length >= 3)
|
|
14
|
+
: [];
|
|
15
|
+
const subtokens = new Set(
|
|
16
|
+
Array.isArray(queryEvidence?.subtokens)
|
|
17
|
+
? queryEvidence.subtokens.filter((value) => typeof value === 'string' && value.length >= 3)
|
|
18
|
+
: [],
|
|
19
|
+
);
|
|
20
|
+
return { anchors, subtokens };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function relevance(symbol, evidence) {
|
|
24
|
+
const name = String(symbol?.symbol || '');
|
|
25
|
+
const lower = name.toLowerCase();
|
|
26
|
+
let exactAnchor = 0;
|
|
27
|
+
let containedAnchor = 0;
|
|
28
|
+
|
|
29
|
+
for (const anchor of evidence.anchors) {
|
|
30
|
+
const anchorLower = anchor.toLowerCase();
|
|
31
|
+
if (lower === anchorLower) {
|
|
32
|
+
exactAnchor = Math.max(exactAnchor, anchor.length);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const caseSensitive = /[A-Z]/.test(anchor);
|
|
36
|
+
if (containsToken(name, anchor, { caseSensitive }) || lower.includes(anchorLower)) {
|
|
37
|
+
containedAnchor = Math.max(containedAnchor, anchor.length);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const symbolTerms = informativeSubtokens(name);
|
|
42
|
+
let subtokenMatches = 0;
|
|
43
|
+
for (const term of evidence.subtokens) {
|
|
44
|
+
if (symbolTerms.has(term)) subtokenMatches++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
matched: exactAnchor > 0 || containedAnchor > 0 || subtokenMatches > 0,
|
|
49
|
+
exactAnchor,
|
|
50
|
+
containedAnchor,
|
|
51
|
+
subtokenMatches,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Select the fixed-size unread-symbol trailer list by query relevance.
|
|
57
|
+
* Stable position order is preserved for ties and when no symbol matches.
|
|
58
|
+
*/
|
|
59
|
+
export function selectUnreadSymbols(symbols, queryEvidence, maxSymbols = 5) {
|
|
60
|
+
const candidates = Array.isArray(symbols) ? symbols : [];
|
|
61
|
+
const limit = Math.max(0, maxSymbols | 0);
|
|
62
|
+
if (candidates.length <= limit) {
|
|
63
|
+
return { symbols: candidates.slice(), moreCount: 0 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const evidence = normalizeEvidence(queryEvidence);
|
|
67
|
+
if (evidence.anchors.length === 0 && evidence.subtokens.size === 0) {
|
|
68
|
+
return {
|
|
69
|
+
symbols: candidates.slice(0, limit),
|
|
70
|
+
moreCount: candidates.length - limit,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ranked = candidates.map((symbol, index) => ({
|
|
75
|
+
symbol,
|
|
76
|
+
index,
|
|
77
|
+
...relevance(symbol, evidence),
|
|
78
|
+
}));
|
|
79
|
+
ranked.sort((a, b) =>
|
|
80
|
+
Number(b.matched) - Number(a.matched)
|
|
81
|
+
|| b.exactAnchor - a.exactAnchor
|
|
82
|
+
|| b.containedAnchor - a.containedAnchor
|
|
83
|
+
|| b.subtokenMatches - a.subtokenMatches
|
|
84
|
+
|| a.index - b.index);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
symbols: ranked.slice(0, limit).map((entry) => entry.symbol),
|
|
88
|
+
moreCount: candidates.length - limit,
|
|
89
|
+
};
|
|
90
|
+
}
|