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.
Files changed (33) hide show
  1. package/README.md +7 -3
  2. package/core/graph/structural-context-format.js +5 -0
  3. package/core/graph/structural-context.js +13 -3
  4. package/core/infrastructure/code-graph-repository.js +58 -0
  5. package/core/infrastructure/language-patterns/registry-core.js +14 -0
  6. package/core/infrastructure/model-fetcher.js +7 -0
  7. package/core/infrastructure/structural-context-repository.js +73 -7
  8. package/core/infrastructure/structural-context-utils.js +11 -0
  9. package/core/infrastructure/structural-qualified-resolution.js +29 -0
  10. package/core/infrastructure/tree-sitter-provider.js +77 -3
  11. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
  12. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
  13. package/core/search/agent-pack-completion.js +493 -0
  14. package/core/search/agent-span-client.js +87 -0
  15. package/core/search/agent-span-ledger.js +447 -0
  16. package/core/search/context-expander.js +29 -0
  17. package/core/search/grep-output-shaping.js +28 -0
  18. package/core/search/regex-dialect.js +328 -0
  19. package/core/search/search-format.js +96 -0
  20. package/core/search/search-pattern.js +62 -10
  21. package/core/search/search-read.js +57 -10
  22. package/core/search/search-server.js +375 -7
  23. package/core/search/session-daemon-prewarm.mjs +75 -0
  24. package/core/search/sweet-search.js +7 -1
  25. package/core/search/unread-symbol-ranking.js +90 -0
  26. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +249 -45
  27. package/mcp/read-tool.js +34 -3
  28. package/mcp/server.js +55 -9
  29. package/mcp/tool-handlers.js +60 -7
  30. package/package.json +9 -9
  31. package/scripts/init.js +30 -5
  32. package/scripts/inject-agent-instructions.js +4 -2
  33. 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 { results, stats };
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
+ }