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.
@@ -10,6 +10,16 @@ import { CodebaseRepository } from '../infrastructure/codebase-repository.js';
10
10
  import { DB_PATHS, PROJECT_ROOT } from '../infrastructure/config/index.js';
11
11
  import { withPinnedRead } from './search-reader-pin.js';
12
12
  import { emitToolIdentityAuto } from './cli-decoration.js';
13
+ import { resolveProjectRoot } from './server-identity.js';
14
+ import {
15
+ applyReadOmissionDecisions,
16
+ collectReadShownSpans,
17
+ exactRereadOmissionEnabled,
18
+ renderReadOmission,
19
+ resolveAgentSessionId,
20
+ } from './agent-span-ledger.js';
21
+ import { sendAgentSpanOperation } from './agent-span-client.js';
22
+ import { selectUnreadSymbols } from './unread-symbol-ranking.js';
13
23
 
14
24
  const CACHE_MAX_ENTRIES = 64;
15
25
  const CACHE_LARGE_FILE_BYTES = 4 * 1024 * 1024; // 4MB — switch to range-read mode
@@ -250,6 +260,7 @@ const SNIFF_MAX_LINES = 4000;
250
260
  const UNREAD_SYMBOLS_MAX = 5; // hard cap on named symbols in the trailer
251
261
  const UNREAD_SYMBOLS_MIN_LINES = 20; // smaller remainders get the short form
252
262
  const C_FAMILY_EXTS = new Set(['.c', '.h', '.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx', '.java', '.cs', '.m', '.mm']);
263
+ const _unreadSymbolCandidates = new WeakMap();
253
264
 
254
265
  // Keyword-introduced definitions (Python/Ruby/JS/TS/Go/Rust/Kotlin/PHP/...).
255
266
  const KEYWORD_DEF_RE = /^\s*(?:export\s+|default\s+|pub(?:\([^)]*\))?\s+|static\s+|async\s+|abstract\s+|final\s+|public\s+|private\s+|protected\s+|inline\s+|constexpr\s+|unsafe\s+|override\s+|open\s+|sealed\s+)*(?:def|fn|func|function\*?|class|struct|enum|trait|interface|impl|object|module|proc)\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*(?:(?:::|\.)[A-Za-z_][\w]*)*)/;
@@ -374,6 +385,7 @@ async function _readFileUnpinned(req) {
374
385
  symbols: symbols.slice(0, UNREAD_SYMBOLS_MAX),
375
386
  moreCount: Math.max(0, symbols.length - UNREAD_SYMBOLS_MAX),
376
387
  };
388
+ _unreadSymbolCandidates.set(unreadBelow, symbols);
377
389
  }
378
390
 
379
391
  // If a line range was requested, narrow attached chunks to the overlap.
@@ -454,24 +466,35 @@ export async function readFiles(files, opts = {}) {
454
466
  * covered the whole file / reached EOF.
455
467
  *
456
468
  * @param {Object} result - readFile() result
457
- * @param {{ command?: 'read'|'ss-read' }} [opts] - continue-command surface
469
+ * @param {{ command?: 'read'|'ss-read', queryEvidence?: {anchors?: string[], subtokens?: string[]} }} [opts]
470
+ * continue-command surface plus agent-session query evidence
458
471
  * @returns {string} one line without trailing newline, or ''
459
472
  */
460
- export function renderUnreadBelow(result, { command = 'read' } = {}) {
473
+ export function renderUnreadBelow(result, { command = 'read', queryEvidence = null } = {}) {
461
474
  const u = result?.unreadBelow;
462
475
  if (!u) return '';
463
- const names = (u.symbols || []).map(s => s.symbol).join(', ');
464
- const more = u.moreCount > 0 ? ` +${u.moreCount} more` : '';
476
+ let symbols = u.symbols || [];
477
+ let moreCount = u.moreCount || 0;
478
+ if (queryEvidence) {
479
+ const candidates = _unreadSymbolCandidates.get(u) || u.symbols || [];
480
+ const selected = selectUnreadSymbols(candidates, queryEvidence, UNREAD_SYMBOLS_MAX);
481
+ symbols = selected.symbols;
482
+ moreCount = selected.moreCount;
483
+ }
484
+ const names = symbols.map(s => s.symbol).join(', ');
485
+ const more = moreCount > 0 ? ` +${moreCount} more` : '';
465
486
  const cont = command === 'ss-read'
466
487
  ? `ss-read ${result.file} ${u.startLine} ${u.endLine}`
467
488
  : `read ${result.file} ${u.startLine}-${u.endLine}`;
468
489
  return `# unread below (${u.startLine}-${u.endLine})${names ? ': ' + names + more : ''} — continue: ${cont}`;
469
490
  }
470
491
 
471
- function _formatAgent(result) {
492
+ function _formatAgent(result, opts = {}) {
472
493
  if (!result.ok) {
473
494
  return `### ${result.file}\n[error] ${result.error}\n`;
474
495
  }
496
+ const omitted = renderReadOmission(result, opts);
497
+ if (omitted) return `### ${result.file}\n${omitted}\n`;
475
498
  const fence = result.language ? '```' + result.language : '```';
476
499
  const range = result.range
477
500
  ? ` (lines ${result.range.startLine}-${result.range.endLine} of ${result.totalLines})`
@@ -483,18 +506,18 @@ function _formatAgent(result) {
483
506
  .filter(Boolean);
484
507
  if (names.length) symbolHint = `\nsymbols: ${names.join(', ')}`;
485
508
  }
486
- const remainder = renderUnreadBelow(result);
509
+ const remainder = renderUnreadBelow(result, opts);
487
510
  return `### ${result.file}${range}${symbolHint}\n${fence}\n${result.text}\n\`\`\`${remainder ? '\n' + remainder : ''}\n`;
488
511
  }
489
512
 
490
- export function formatReadResults(results, format = 'agent') {
513
+ export function formatReadResults(results, format = 'agent', opts = {}) {
491
514
  if (format === 'json') {
492
515
  return JSON.stringify({ files: results.files, totalMs: results.totalMs }, null, 2);
493
516
  }
494
517
  if (format === 'raw') {
495
518
  return results.files.map(r => r.ok ? r.text : `[error: ${r.file}] ${r.error}`).join('\n\n');
496
519
  }
497
- return results.files.map(_formatAgent).join('\n');
520
+ return results.files.map((result) => _formatAgent(result, opts)).join('\n');
498
521
  }
499
522
 
500
523
  // ---------------------------------------------------------------------------
@@ -519,6 +542,7 @@ function _parseArgs(args) {
519
542
  let includeMetadata = true;
520
543
  let plain = false;
521
544
  let noBanner = false;
545
+ let force = false;
522
546
  for (let i = 0; i < args.length; i++) {
523
547
  const a = args[i];
524
548
  if (a === '--json') format = 'json';
@@ -526,6 +550,7 @@ function _parseArgs(args) {
526
550
  else if (a === '--agent') format = 'agent';
527
551
  else if (a === '--no-metadata') includeMetadata = false;
528
552
  else if (a === '--no-banner') noBanner = true;
553
+ else if (a === '--force') force = true;
529
554
  else if (a === '--format' || a.startsWith('--format=')) {
530
555
  const v = a === '--format' ? args[++i] : a.slice('--format='.length);
531
556
  if (v === 'json' || v === 'raw' || v === 'agent') format = v;
@@ -543,7 +568,7 @@ function _parseArgs(args) {
543
568
  positional.push(a);
544
569
  }
545
570
  }
546
- return { positional, format, startLine, endLine, includeMetadata, plain, noBanner };
571
+ return { positional, format, startLine, endLine, includeMetadata, plain, noBanner, force };
547
572
  }
548
573
 
549
574
  function _printHelp() {
@@ -562,6 +587,7 @@ function _printHelp() {
562
587
  ' --format <fmt> json | raw | agent | plain (plain = no identity line)',
563
588
  ' --no-banner Suppress the identity line',
564
589
  ' --no-metadata Skip index metadata attachment',
590
+ ' --force Retry the exact read named by an omission',
565
591
  '',
566
592
  ].join('\n'));
567
593
  }
@@ -585,11 +611,32 @@ export async function handleReadCli(args) {
585
611
  endLine: wantsRange ? parsed.endLine : undefined,
586
612
  }));
587
613
  const out = await readFiles(files, { includeMetadata: parsed.includeMetadata });
614
+ let queryEvidence = null;
615
+ if (parsed.format === 'agent' && exactRereadOmissionEnabled()) {
616
+ const agentSessionId = resolveAgentSessionId();
617
+ const spans = collectReadShownSpans(out, { projectRoot: resolveProjectRoot() });
618
+ const response = await sendAgentSpanOperation({
619
+ operation: 'read',
620
+ sessionId: agentSessionId,
621
+ spans,
622
+ force: parsed.force,
623
+ });
624
+ if (response?.ok && Array.isArray(response.decisions)) {
625
+ const decisions = Array.from({ length: out.files.length }, () => ({ omit: false }));
626
+ spans.forEach((span, index) => { decisions[span.resultIndex] = response.decisions[index]; });
627
+ applyReadOmissionDecisions(out, decisions);
628
+ }
629
+ queryEvidence = response?.queryEvidence || null;
630
+ }
588
631
  if (parsed.format !== 'json') {
589
632
  const detail = files.length === 1 ? files[0].path : `${files.length} files`;
590
633
  emitToolIdentityAuto('read', detail, { plain: parsed.plain, noBanner: parsed.noBanner });
591
634
  }
592
- process.stdout.write(formatReadResults(out, parsed.format));
635
+ process.stdout.write(formatReadResults(out, parsed.format, {
636
+ surface: 'cli',
637
+ force: parsed.force,
638
+ queryEvidence,
639
+ }));
593
640
  if (parsed.format !== 'json') process.stdout.write('\n');
594
641
  // Non-zero exit if every file failed (so shell pipelines see the error).
595
642
  const allFailed = out.files.length > 0 && out.files.every(f => !f.ok);