byterover-cli 3.0.0 → 3.1.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 (73) hide show
  1. package/dist/agent/core/domain/tools/constants.d.ts +1 -0
  2. package/dist/agent/core/domain/tools/constants.js +1 -0
  3. package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
  4. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  5. package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
  6. package/dist/agent/infra/agent/agent-error-codes.js +0 -1
  7. package/dist/agent/infra/agent/agent-error.d.ts +0 -1
  8. package/dist/agent/infra/agent/agent-error.js +0 -1
  9. package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
  10. package/dist/agent/infra/agent/agent-state-manager.js +1 -3
  11. package/dist/agent/infra/agent/base-agent.d.ts +1 -1
  12. package/dist/agent/infra/agent/base-agent.js +1 -1
  13. package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
  14. package/dist/agent/infra/agent/cipher-agent.js +188 -3
  15. package/dist/agent/infra/agent/index.d.ts +1 -1
  16. package/dist/agent/infra/agent/index.js +1 -1
  17. package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
  18. package/dist/agent/infra/agent/service-initializer.js +14 -8
  19. package/dist/agent/infra/agent/types.d.ts +0 -1
  20. package/dist/agent/infra/file-system/file-system-service.js +6 -5
  21. package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
  22. package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
  23. package/dist/agent/infra/llm/providers/openai.js +12 -0
  24. package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
  25. package/dist/agent/infra/llm/stream-to-text.js +14 -0
  26. package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
  27. package/dist/agent/infra/map/abstract-generator.js +67 -0
  28. package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
  29. package/dist/agent/infra/map/abstract-queue.js +218 -0
  30. package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
  31. package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
  32. package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
  33. package/dist/agent/infra/memory/memory-manager.js +6 -5
  34. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  35. package/dist/agent/infra/sandbox/curate-service.js +6 -7
  36. package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
  37. package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
  38. package/dist/agent/infra/sandbox/tools-sdk.d.ts +3 -1
  39. package/dist/agent/infra/session/session-compressor.d.ts +43 -0
  40. package/dist/agent/infra/session/session-compressor.js +296 -0
  41. package/dist/agent/infra/session/session-manager.d.ts +7 -0
  42. package/dist/agent/infra/session/session-manager.js +9 -0
  43. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
  44. package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
  45. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
  46. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
  47. package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
  48. package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
  49. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
  50. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
  51. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +207 -34
  52. package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
  53. package/dist/agent/infra/tools/tool-provider.js +1 -0
  54. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  55. package/dist/agent/infra/tools/tool-registry.js +15 -4
  56. package/dist/server/constants.d.ts +2 -0
  57. package/dist/server/constants.js +2 -0
  58. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
  59. package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
  60. package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
  61. package/dist/server/core/domain/transport/schemas.d.ts +10 -10
  62. package/dist/server/infra/context-tree/derived-artifact.js +5 -1
  63. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
  64. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
  65. package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
  66. package/dist/server/infra/executor/curate-executor.js +2 -1
  67. package/dist/server/infra/executor/folder-pack-executor.js +72 -2
  68. package/dist/server/infra/executor/query-executor.js +11 -3
  69. package/dist/server/infra/transport/handlers/status-handler.js +10 -0
  70. package/dist/server/utils/curate-result-parser.d.ts +4 -4
  71. package/dist/shared/transport/types/dto.d.ts +7 -0
  72. package/oclif.manifest.json +1 -1
  73. package/package.json +10 -4
@@ -1,7 +1,8 @@
1
1
  import MiniSearch from 'minisearch';
2
+ import { realpath } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { removeStopwords } from 'stopword';
4
- import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, SUMMARY_INDEX_FILE } from '../../../../server/constants.js';
5
+ import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, OVERVIEW_EXTENSION, SUMMARY_INDEX_FILE } from '../../../../server/constants.js';
5
6
  import { parseFrontmatterScoring, updateScoringInContent, } from '../../../../server/core/domain/knowledge/markdown-writer.js';
6
7
  import { applyDecay, applyDefaultScoring, compoundScore, determineTier, recordAccessHits, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
7
8
  import { isArchiveStub, isDerivedArtifact } from '../../../../server/infra/context-tree/derived-artifact.js';
@@ -13,9 +14,9 @@ const DEFAULT_CACHE_TTL_MS = 5000;
13
14
  /** Bump when MINISEARCH_OPTIONS fields/boost change to invalidate cached indexes */
14
15
  const INDEX_SCHEMA_VERSION = 4;
15
16
  /** Only include results whose normalized score is at least this fraction of the top result's score */
16
- const SCORE_GAP_RATIO = 0.75;
17
+ const SCORE_GAP_RATIO = 0.7;
17
18
  /** Minimum normalized score for the top result. Below this, the query is considered out-of-domain */
18
- const MINIMUM_RELEVANCE_SCORE = 0.6;
19
+ const MINIMUM_RELEVANCE_SCORE = 0.45;
19
20
  /** Normalized score threshold above which results are trusted despite unmatched query terms */
20
21
  const UNMATCHED_TERM_SCORE_THRESHOLD = 0.85;
21
22
  /** Minimum query term length to consider "significant" for OOD term-based detection */
@@ -33,6 +34,59 @@ const CHUNK_OVERLAP_CHARS = 120;
33
34
  function normalizeScore(rawScore) {
34
35
  return rawScore / (1 + rawScore);
35
36
  }
37
+ /**
38
+ * Propagate BM25 scores upward to parent domain/topic nodes.
39
+ *
40
+ * For each matched result, walks the parent chain and computes a decayed boost
41
+ * (score * propagationFactor per level). New summary entries are added for
42
+ * parent nodes that have a _index.md in summaryMap but are not already in results.
43
+ *
44
+ * @param results - Already-enriched search results (gap-ratio filtered)
45
+ * @param symbolTree - Symbol tree for parent-chain traversal
46
+ * @param summaryMap - Map of _index.md file paths → SummaryDocLike (for excerpt/metadata)
47
+ * @param propagationFactor - Score multiplier per level up (default 0.55)
48
+ * @returns New parent entries only — caller merges and re-sorts
49
+ */
50
+ function propagateScoresToParents(results, symbolTree, summaryMap, documentMap, propagationFactor = 0.55) {
51
+ const boosts = new Map();
52
+ for (const r of results) {
53
+ const symbol = symbolTree.symbolMap.get(r.path);
54
+ let parent = symbol?.parent;
55
+ let factor = propagationFactor;
56
+ while (parent) {
57
+ const cur = boosts.get(parent.path) ?? 0;
58
+ boosts.set(parent.path, Math.max(cur, r.bm25Score * factor));
59
+ parent = parent.parent;
60
+ factor *= propagationFactor;
61
+ }
62
+ }
63
+ const existingPaths = new Set(results.map((r) => r.path));
64
+ const boosted = [];
65
+ for (const [parentPath, score] of boosts.entries()) {
66
+ if (existingPaths.has(parentPath))
67
+ continue;
68
+ const doc = getSummarySource(parentPath, summaryMap, documentMap);
69
+ if (!doc)
70
+ continue;
71
+ // Propagate the strongest child BM25 signal upward, then apply the parent
72
+ // summary's own scoring exactly once. This avoids double-counting lifecycle
73
+ // weights that are already baked into child compound scores.
74
+ const finalScore = doc.scoring
75
+ ? compoundScore(score, doc.scoring.importance ?? 50, doc.scoring.recency ?? 0.5, doc.scoring.maturity ?? 'draft')
76
+ : score;
77
+ boosted.push({
78
+ backlinkCount: 0,
79
+ excerpt: doc.excerpt,
80
+ path: parentPath,
81
+ score: finalScore,
82
+ symbolKind: 'summary',
83
+ title: parentPath,
84
+ });
85
+ }
86
+ return boosted;
87
+ }
88
+ /** Numeric rank for maturity tiers — used for minMaturity filtering in both BM25 and propagated results. */
89
+ const MATURITY_TIER_RANK = { core: 3, draft: 1, validated: 2 };
36
90
  const MINISEARCH_OPTIONS = {
37
91
  fields: ['title', 'content', 'path'],
38
92
  idField: 'id',
@@ -43,6 +97,28 @@ const MINISEARCH_OPTIONS = {
43
97
  },
44
98
  storeFields: ['title', 'path'],
45
99
  };
100
+ function getSummaryAccessPath(path, summaryMap, documentMap) {
101
+ return getSummarySource(path, summaryMap, documentMap)?.path ?? `${path}/${SUMMARY_INDEX_FILE}`;
102
+ }
103
+ function getSummarySource(path, summaryMap, documentMap) {
104
+ const summaryDoc = summaryMap.get(`${path}/${SUMMARY_INDEX_FILE}`);
105
+ if (summaryDoc) {
106
+ return {
107
+ excerpt: summaryDoc.excerpt ?? '',
108
+ path: summaryDoc.path,
109
+ scoring: summaryDoc.scoring,
110
+ };
111
+ }
112
+ const contextDoc = documentMap.get(`${path}/context.md`);
113
+ if (contextDoc) {
114
+ return {
115
+ excerpt: extractExcerpt(contextDoc.content, contextDoc.title),
116
+ path: contextDoc.path,
117
+ scoring: contextDoc.scoring,
118
+ };
119
+ }
120
+ return undefined;
121
+ }
46
122
  function filterStopWords(query) {
47
123
  const words = query.toLowerCase().split(/\s+/);
48
124
  const filtered = removeStopwords(words);
@@ -162,6 +238,9 @@ function extractExcerpt(content, query, maxLength = 800) {
162
238
  }
163
239
  return excerpt || cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? '...' : '');
164
240
  }
241
+ function stripMarkdownFrontmatter(content) {
242
+ return content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '').trim();
243
+ }
165
244
  async function findMarkdownFilesWithMtime(fileSystem, contextTreePath) {
166
245
  try {
167
246
  const globResult = await fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
@@ -213,19 +292,27 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
213
292
  symbolTree: { root: [], symbolMap: new Map() },
214
293
  };
215
294
  }
216
- // Partition files: _index.md → summaryFiles, derived artifacts skip, rest → indexable
295
+ // Partition files: _index.md → summaryFiles, .overview.mdoverviewFiles (for cache
296
+ // invalidation + sibling detection), other derived artifacts → skip, rest → indexable
217
297
  const summaryFiles = [];
298
+ const overviewFiles = [];
218
299
  const indexableFiles = [];
300
+ // Track all known paths for sibling detection (e.g. .overview.md presence check)
301
+ const knownPaths = new Set(filesWithMtime.map((f) => f.path));
219
302
  for (const file of filesWithMtime) {
220
303
  const fileName = file.path.split('/').at(-1) ?? '';
221
304
  if (fileName === SUMMARY_INDEX_FILE) {
222
305
  summaryFiles.push(file);
223
306
  }
307
+ else if (file.path.endsWith(OVERVIEW_EXTENSION)) {
308
+ // Track mtimes so cache invalidates when a new .overview.md appears; not BM25-indexed
309
+ overviewFiles.push(file);
310
+ }
224
311
  else if (!isDerivedArtifact(file.path)) {
225
312
  // Includes regular .md files AND .stub.md files (stubs are searchable)
226
313
  indexableFiles.push(file);
227
314
  }
228
- // .full.md and _manifest.json are skipped (isDerivedArtifact returns true)
315
+ // .full.md, .abstract.md, and _manifest.json are skipped (isDerivedArtifact returns true)
229
316
  }
230
317
  // Read indexable documents for BM25 index
231
318
  const documentPromises = indexableFiles.map(async ({ mtime, path: filePath }) => {
@@ -234,10 +321,14 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
234
321
  const { content } = await fileSystem.readFile(fullPath);
235
322
  const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
236
323
  const scoring = parseFrontmatterScoring(content) ?? applyDefaultScoring();
324
+ // Check if a .overview.md sibling exists (written by abstract generation queue)
325
+ const overviewRelPath = filePath.replace(/\.md$/, OVERVIEW_EXTENSION);
326
+ const overviewPath = knownPaths.has(overviewRelPath) ? overviewRelPath : undefined;
237
327
  return {
238
328
  content,
239
329
  id: filePath,
240
330
  mtime,
331
+ ...(overviewPath !== undefined && { overviewPath }),
241
332
  path: filePath,
242
333
  scoring,
243
334
  title,
@@ -255,9 +346,16 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
255
346
  const fm = parseSummaryFrontmatter(content);
256
347
  if (!fm)
257
348
  return null;
349
+ // Persist frontmatter scoring so propagateScoresToParents can apply hotness/tier boosts
350
+ const frontmatter = parseFrontmatterScoring(content);
351
+ const scoring = frontmatter
352
+ ? { importance: frontmatter.importance, maturity: frontmatter.maturity, recency: frontmatter.recency }
353
+ : undefined;
258
354
  return {
259
355
  condensationOrder: fm.condensation_order,
356
+ excerpt: stripMarkdownFrontmatter(content).slice(0, 400),
260
357
  path: filePath,
358
+ scoring,
261
359
  tokenCount: fm.token_count,
262
360
  };
263
361
  }
@@ -280,6 +378,10 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
280
378
  for (const sf of summaryFiles) {
281
379
  fileMtimes.set(sf.path, sf.mtime);
282
380
  }
381
+ // Track .overview.md mtimes so the cache invalidates when a new overview is written
382
+ for (const ov of overviewFiles) {
383
+ fileMtimes.set(ov.path, ov.mtime);
384
+ }
283
385
  const summaryMap = new Map();
284
386
  for (const summary of summaryResults) {
285
387
  if (summary) {
@@ -345,12 +447,27 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
345
447
  };
346
448
  }
347
449
  }
348
- const allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
450
+ let allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
349
451
  // Exclude non-indexable derived artifacts (.full.md) so that currentFiles
350
452
  // matches what buildFreshIndex tracks in fileMtimes. Without this filter,
351
453
  // isCacheValid() sees a size mismatch once archives exist, causing cache thrash.
352
454
  // _index.md is kept (tracked for summary staleness), .stub.md is kept (BM25 indexed).
353
- const currentFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) || f.path.split('/').at(-1) === SUMMARY_INDEX_FILE);
455
+ // Keep _index.md (summary tracking) and .overview.md (sibling detection for overviewPath).
456
+ // .full.md, .abstract.md, and _manifest.json remain excluded.
457
+ let currentFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
458
+ f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
459
+ f.path.endsWith(OVERVIEW_EXTENSION));
460
+ // Flush pending access hits before reusing a stale-enough cache entry.
461
+ // The flush updates frontmatter on disk, so refresh mtimes before the cache-valid check.
462
+ if (onBeforeBuild) {
463
+ const wroteScoringUpdates = await onBeforeBuild(contextTreePath);
464
+ if (wroteScoringUpdates) {
465
+ allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
466
+ currentFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
467
+ f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
468
+ f.path.endsWith(OVERVIEW_EXTENSION));
469
+ }
470
+ }
354
471
  // Re-check cache validity after getting file list (another call may have finished)
355
472
  if (state.cachedIndex &&
356
473
  state.cachedIndex.contextTreePath === contextTreePath &&
@@ -364,10 +481,6 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
364
481
  state.cachedIndex = updatedCache;
365
482
  return updatedCache;
366
483
  }
367
- // Flush pending access hits before building so updated scoring is picked up
368
- if (onBeforeBuild) {
369
- await onBeforeBuild(contextTreePath);
370
- }
371
484
  // Build fresh index
372
485
  const freshIndex = await buildFreshIndex(fileSystem, contextTreePath, currentFiles);
373
486
  state.cachedIndex = freshIndex;
@@ -420,7 +533,7 @@ export class SearchKnowledgeService {
420
533
  */
421
534
  async flushAccessHits(contextTreePath) {
422
535
  if (this.pendingAccessHits.size === 0) {
423
- return;
536
+ return false;
424
537
  }
425
538
  const hits = new Map(this.pendingAccessHits);
426
539
  this.pendingAccessHits.clear();
@@ -440,6 +553,7 @@ export class SearchKnowledgeService {
440
553
  }
441
554
  });
442
555
  await Promise.allSettled(tasks);
556
+ return true;
443
557
  }
444
558
  /**
445
559
  * Search the knowledge base for relevant topics.
@@ -451,14 +565,15 @@ export class SearchKnowledgeService {
451
565
  */
452
566
  async search(query, options) {
453
567
  const limit = options?.limit ?? 10;
454
- const contextTreePath = join(this.baseDirectory, BRV_DIR, CONTEXT_TREE_DIR);
568
+ const resolvedBaseDirectory = await realpath(this.baseDirectory).catch(() => this.baseDirectory);
569
+ const contextTreePath = join(resolvedBaseDirectory, BRV_DIR, CONTEXT_TREE_DIR);
455
570
  // Acquire index with parallel-safe locking; flush pending access hits before any rebuild
456
571
  const indexResult = await acquireIndex(this.state, this.fileSystem, contextTreePath, this.cacheTtlMs, (ctxPath) => this.flushAccessHits(ctxPath));
457
572
  // Handle error case (context tree not initialized)
458
573
  if ('error' in indexResult) {
459
574
  return indexResult.result;
460
575
  }
461
- const { documentMap, index, referenceIndex, symbolTree } = indexResult;
576
+ const { documentMap, index, referenceIndex, summaryMap, symbolTree } = indexResult;
462
577
  if (documentMap.size === 0) {
463
578
  return {
464
579
  message: 'Context tree is empty. Use /curate to add knowledge.',
@@ -472,7 +587,7 @@ export class SearchKnowledgeService {
472
587
  }
473
588
  // Symbolic path resolution: try path-based query first
474
589
  if (isPathLikeQuery(query, symbolTree)) {
475
- const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, options);
590
+ const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options);
476
591
  if (symbolicResult) {
477
592
  return symbolicResult;
478
593
  }
@@ -482,10 +597,10 @@ export class SearchKnowledgeService {
482
597
  const effectiveScope = options?.scope ?? parsed.scopePath;
483
598
  const effectiveQuery = parsed.scopePath ? parsed.textQuery : query;
484
599
  // Run text-based MiniSearch (existing pipeline), optionally scoped to a subtree
485
- const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, symbolTree, referenceIndex, options);
600
+ const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, symbolTree, referenceIndex, summaryMap, options);
486
601
  // If scoped search returned nothing and we had a scope, fall back to global search
487
602
  if (textResult.results.length === 0 && effectiveScope && effectiveQuery) {
488
- return this.runTextSearch(query, documentMap, index, limit, undefined, symbolTree, referenceIndex, options);
603
+ return this.runTextSearch(query, documentMap, index, limit, undefined, symbolTree, referenceIndex, summaryMap, options);
489
604
  }
490
605
  return textResult;
491
606
  }
@@ -536,19 +651,27 @@ export class SearchKnowledgeService {
536
651
  }
537
652
  }
538
653
  }
654
+ const doc = documentMap.get(result.path);
655
+ const overviewPath = doc?.overviewPath;
656
+ const isContextSummary = doc?.path.endsWith('/context.md') || doc?.path === 'context.md';
657
+ const summaryPath = isContextSummary
658
+ ? doc?.path.slice(0, -'/context.md'.length) || doc?.path || result.path
659
+ : result.path;
539
660
  return {
540
661
  ...result,
541
662
  ...(archiveFullPath && { archiveFullPath }),
663
+ ...(overviewPath && { overviewPath }),
542
664
  backlinkCount: backlinks?.length ?? 0,
665
+ ...(isContextSummary && { path: summaryPath }),
543
666
  relatedPaths: backlinks?.slice(0, 3),
544
- symbolKind,
545
- symbolPath: symbol?.path,
667
+ symbolKind: isContextSummary ? 'summary' : symbolKind,
668
+ symbolPath: isContextSummary ? summaryPath : symbol?.path,
546
669
  };
547
670
  }
548
671
  /**
549
672
  * Run the standard text-based MiniSearch pipeline, optionally scoped to a subtree.
550
673
  */
551
- runTextSearch(query, documentMap, index, limit, scopePath, symbolTree, referenceIndex, options) {
674
+ runTextSearch(query, documentMap, index, limit, scopePath, symbolTree, referenceIndex, summaryMap, options) {
552
675
  const filteredQuery = filterStopWords(query);
553
676
  const filteredWords = filteredQuery.split(/\s+/).filter((w) => w.length >= 2);
554
677
  // Build scope filter if a subtree is specified
@@ -585,11 +708,14 @@ export class SearchKnowledgeService {
585
708
  const bm25 = normalizeScore(r.score);
586
709
  return {
587
710
  ...r,
711
+ bm25Score: bm25,
588
712
  score: compoundScore(bm25, decayed.importance ?? 50, decayed.recency ?? 1, decayed.maturity ?? 'draft'),
589
713
  };
590
714
  });
591
715
  searchResults.sort((a, b) => b.score - a.score);
592
716
  const results = [];
717
+ const propagationInputs = [];
718
+ let scoreFloor;
593
719
  if (searchResults.length > 0) {
594
720
  // OOD detection: if the best result scores below the minimum floor,
595
721
  // the query has no meaningful match in the knowledge base.
@@ -615,7 +741,7 @@ export class SearchKnowledgeService {
615
741
  };
616
742
  }
617
743
  const topScore = searchResults[0].score;
618
- const scoreFloor = topScore * SCORE_GAP_RATIO;
744
+ scoreFloor = topScore * SCORE_GAP_RATIO;
619
745
  const resultLimit = Math.min(limit, searchResults.length);
620
746
  for (let i = 0; i < resultLimit; i++) {
621
747
  const result = searchResults[i];
@@ -638,22 +764,54 @@ export class SearchKnowledgeService {
638
764
  continue;
639
765
  }
640
766
  if (options?.minMaturity && enriched.symbolKind) {
641
- const tierRank = { core: 3, draft: 1, validated: 2 };
642
- const symbol = symbolTree.symbolMap.get(document.path);
643
- const docMaturity = symbol?.metadata.maturity ?? 'draft';
644
- if ((tierRank[docMaturity] ?? 1) < (tierRank[options.minMaturity] ?? 1)) {
767
+ const docMaturity = enriched.symbolKind === 'summary'
768
+ ? getSummarySource(enriched.path, summaryMap, documentMap)?.scoring?.maturity
769
+ ?? symbolTree.symbolMap.get(enriched.path)?.metadata.maturity
770
+ ?? 'draft'
771
+ : symbolTree.symbolMap.get(document.path)?.metadata.maturity ?? 'draft';
772
+ if ((MATURITY_TIER_RANK[docMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1)) {
645
773
  continue;
646
774
  }
647
775
  }
648
776
  results.push(enriched);
777
+ propagationInputs.push({
778
+ bm25Score: result.bm25Score,
779
+ path: document.path,
780
+ });
649
781
  }
650
782
  }
651
783
  }
652
- // Accumulate access hits for returned results (flushed during next index rebuild)
653
- // Disabled for benchmark: prevents feedback loop from distorting scores across queries
654
- // if (results.length > 0) {
655
- // this.accumulateAccessHits(results.map((r) => r.path))
656
- // }
784
+ // Propagate scores upward to parent domain/topic nodes (hierarchical retrieval)
785
+ const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap, documentMap);
786
+ for (const p of propagated) {
787
+ if (scoreFloor !== undefined && p.score < scoreFloor)
788
+ continue;
789
+ if (options?.includeKinds && p.symbolKind && !options.includeKinds.includes(p.symbolKind))
790
+ continue;
791
+ if (options?.excludeKinds && p.symbolKind && options.excludeKinds.includes(p.symbolKind))
792
+ continue;
793
+ if (options?.minMaturity && p.symbolKind === 'summary') {
794
+ const summaryDoc = getSummarySource(p.path, summaryMap, documentMap);
795
+ const summaryMaturity = summaryDoc?.scoring?.maturity ?? 'draft';
796
+ if ((MATURITY_TIER_RANK[summaryMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1))
797
+ continue;
798
+ }
799
+ results.push(p);
800
+ }
801
+ if (propagated.length > 0) {
802
+ results.sort((a, b) => b.score - a.score);
803
+ // Trim back to the caller-requested limit after propagated entries are merged in.
804
+ if (results.length > limit)
805
+ results.splice(limit);
806
+ }
807
+ // Accumulate access hits for returned results (flushed during next index rebuild).
808
+ // Synthetic 'summary' results carry folder-style paths (e.g. 'auth') that are not
809
+ // real files; map them to their _index.md so flushAccessHits can read and update them.
810
+ if (results.length > 0) {
811
+ this.accumulateAccessHits(results.map((r) => (r.symbolKind === 'summary'
812
+ ? getSummaryAccessPath(r.path, summaryMap, documentMap)
813
+ : r.path)));
814
+ }
657
815
  return {
658
816
  message: results.length > 0
659
817
  ? `Found ${searchResults.length} result(s). Use read_file to view full content.`
@@ -665,7 +823,7 @@ export class SearchKnowledgeService {
665
823
  /**
666
824
  * Try to resolve the query as a symbolic path. Returns null if no path match found.
667
825
  */
668
- trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, options) {
826
+ trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options) {
669
827
  const pathMatches = matchMemoryPath(symbolTree, query.split(/\s+/)[0].includes('/') ? query.split(/\s+/)[0] : query);
670
828
  if (pathMatches.length === 0) {
671
829
  return null;
@@ -691,11 +849,25 @@ export class SearchKnowledgeService {
691
849
  const textPart = query.slice(query.indexOf(pathPart) + pathPart.length).trim();
692
850
  if (textPart) {
693
851
  // Scoped search: search text within the matched subtree
694
- return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, symbolTree, referenceIndex, options);
852
+ return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, symbolTree, referenceIndex, summaryMap, options);
695
853
  }
696
854
  // No text part — return all children of the matched node
697
855
  const subtreeIds = getSubtreeDocumentIds(symbolTree, topMatch.path);
698
856
  const results = [];
857
+ const accessHitPaths = [];
858
+ const summaryDoc = getSummarySource(topMatch.path, summaryMap, documentMap);
859
+ if (summaryDoc) {
860
+ results.push({
861
+ backlinkCount: 0,
862
+ excerpt: summaryDoc.excerpt,
863
+ path: topMatch.path,
864
+ score: 1,
865
+ symbolKind: 'summary',
866
+ symbolPath: topMatch.path,
867
+ title: topMatch.name,
868
+ });
869
+ accessHitPaths.push(summaryDoc.path);
870
+ }
699
871
  for (const docId of subtreeIds) {
700
872
  if (results.length >= limit)
701
873
  break;
@@ -703,9 +875,10 @@ export class SearchKnowledgeService {
703
875
  if (!doc)
704
876
  continue;
705
877
  results.push(this.enrichResult({ excerpt: extractExcerpt(doc.content, query), path: doc.path, score: 0.9, title: doc.title }, symbolTree, referenceIndex, documentMap));
878
+ accessHitPaths.push(doc.path);
706
879
  }
707
- if (results.length > 0) {
708
- this.accumulateAccessHits(results.map((r) => r.path));
880
+ if (accessHitPaths.length > 0) {
881
+ this.accumulateAccessHits(accessHitPaths);
709
882
  }
710
883
  return {
711
884
  message: `Found ${results.length} entries under ${topMatch.path}. Use read_file to view full content.`,
@@ -4,11 +4,11 @@ import { SearchKnowledgeService } from './search-knowledge-service.js';
4
4
  const SearchKnowledgeInputSchema = z
5
5
  .object({
6
6
  excludeKinds: z
7
- .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
7
+ .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
8
8
  .optional()
9
9
  .describe('Symbol kinds to exclude from results'),
10
10
  includeKinds: z
11
- .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
11
+ .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
12
12
  .optional()
13
13
  .describe('Symbol kinds to include in results (filters out others)'),
14
14
  limit: z
@@ -18,6 +18,7 @@ export class ToolProvider {
18
18
  * TypeScript ensures this object matches ToolServices keys at compile time.
19
19
  */
20
20
  static VALID_SERVICE_KEYS = new Set(Object.keys({
21
+ abstractQueue: 0,
21
22
  agentInstance: 0,
22
23
  contentGenerator: 0,
23
24
  environmentContext: 0,
@@ -9,6 +9,7 @@ import type { IProcessService } from '../../core/interfaces/i-process-service.js
9
9
  import type { ISandboxService } from '../../core/interfaces/i-sandbox-service.js';
10
10
  import type { ITodoStorage } from '../../core/interfaces/i-todo-storage.js';
11
11
  import type { ITokenizer } from '../../core/interfaces/i-tokenizer.js';
12
+ import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
12
13
  import type { MemoryManager } from '../memory/memory-manager.js';
13
14
  import type { ToolProviderGetter } from './tool-provider-getter.js';
14
15
  import { ToolMarker } from './tool-markers.js';
@@ -17,6 +18,8 @@ import { ToolMarker } from './tool-markers.js';
17
18
  * Tools declare which services they need via requiredServices.
18
19
  */
19
20
  export interface ToolServices {
21
+ /** Abstract generation queue for background L0/L1 abstract file generation */
22
+ abstractQueue?: AbstractGenerationQueue;
20
23
  /** Agent instance for creating sub-sessions (used by agentic_map) */
21
24
  agentInstance?: ICipherAgent;
22
25
  /** Content generator for stateless LLM calls (used by llm_map) */
@@ -6,6 +6,7 @@ import { createCurateTool } from './implementations/curate-tool.js';
6
6
  import { createExpandKnowledgeTool } from './implementations/expand-knowledge-tool.js';
7
7
  import { createGlobFilesTool } from './implementations/glob-files-tool.js';
8
8
  import { createGrepContentTool } from './implementations/grep-content-tool.js';
9
+ import { createIngestResourceTool } from './implementations/ingest-resource-tool.js';
9
10
  import { createListDirectoryTool } from './implementations/list-directory-tool.js';
10
11
  import { createLlmMapTool } from './implementations/llm-map-tool.js';
11
12
  import { createReadFileTool } from './implementations/read-file-tool.js';
@@ -54,7 +55,7 @@ export const TOOL_REGISTRY = {
54
55
  },
55
56
  [ToolName.CODE_EXEC]: {
56
57
  descriptionFile: 'code_exec',
57
- factory({ environmentContext, fileSystemService, sandboxService }) {
58
+ factory({ abstractQueue, environmentContext, fileSystemService, sandboxService }) {
58
59
  const sandbox = getRequiredService(sandboxService, 'sandboxService');
59
60
  // Inject file system service into sandbox for Tools SDK
60
61
  if (fileSystemService && sandbox.setFileSystem) {
@@ -67,7 +68,7 @@ export const TOOL_REGISTRY = {
67
68
  }
68
69
  // Inject curate service into sandbox for Tools SDK
69
70
  if (sandbox.setCurateService) {
70
- const curateService = createCurateService(environmentContext?.workingDirectory);
71
+ const curateService = createCurateService(environmentContext?.workingDirectory, abstractQueue);
71
72
  sandbox.setCurateService(curateService);
72
73
  }
73
74
  // Inject environment context into sandbox for env.* access
@@ -81,10 +82,10 @@ export const TOOL_REGISTRY = {
81
82
  },
82
83
  [ToolName.CURATE]: {
83
84
  descriptionFile: 'curate',
84
- factory: ({ environmentContext }) => createCurateTool(environmentContext?.workingDirectory),
85
+ factory: ({ abstractQueue, environmentContext }) => createCurateTool(environmentContext?.workingDirectory, abstractQueue),
85
86
  markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
86
87
  outputGuidance: 'curate',
87
- requiredServices: [], // Uses DirectoryManager and MarkdownWriter for file operations
88
+ requiredServices: [],
88
89
  },
89
90
  [ToolName.EXPAND_KNOWLEDGE]: {
90
91
  descriptionFile: 'expand_knowledge',
@@ -104,6 +105,16 @@ export const TOOL_REGISTRY = {
104
105
  markers: [ToolMarker.Core, ToolMarker.Discovery],
105
106
  requiredServices: ['fileSystemService'],
106
107
  },
108
+ [ToolName.INGEST_RESOURCE]: {
109
+ factory: ({ abstractQueue, contentGenerator, environmentContext, fileSystemService }) => createIngestResourceTool({
110
+ abstractQueue,
111
+ baseDirectory: environmentContext?.workingDirectory,
112
+ contentGenerator,
113
+ fileSystem: fileSystemService,
114
+ }),
115
+ markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
116
+ requiredServices: ['contentGenerator', 'fileSystemService'],
117
+ },
107
118
  [ToolName.LIST_DIRECTORY]: {
108
119
  descriptionFile: 'list_directory',
109
120
  factory: (services) => createListDirectoryTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
@@ -55,6 +55,8 @@ export declare const SUMMARY_INDEX_FILE = "_index.md";
55
55
  export declare const ARCHIVE_DIR = "_archived";
56
56
  export declare const STUB_EXTENSION = ".stub.md";
57
57
  export declare const FULL_ARCHIVE_EXTENSION = ".full.md";
58
+ export declare const ABSTRACT_EXTENSION = ".abstract.md";
59
+ export declare const OVERVIEW_EXTENSION = ".overview.md";
58
60
  export declare const MANIFEST_FILE = "_manifest.json";
59
61
  export declare const ARCHIVE_IMPORTANCE_THRESHOLD = 35;
60
62
  export declare const DEFAULT_GHOST_CUE_MAX_TOKENS = 220;
@@ -75,6 +75,8 @@ export const SUMMARY_INDEX_FILE = '_index.md';
75
75
  export const ARCHIVE_DIR = '_archived';
76
76
  export const STUB_EXTENSION = '.stub.md';
77
77
  export const FULL_ARCHIVE_EXTENSION = '.full.md';
78
+ export const ABSTRACT_EXTENSION = '.abstract.md';
79
+ export const OVERVIEW_EXTENSION = '.overview.md';
78
80
  export const MANIFEST_FILE = '_manifest.json';
79
81
  export const ARCHIVE_IMPORTANCE_THRESHOLD = 35;
80
82
  export const DEFAULT_GHOST_CUE_MAX_TOKENS = 220;
@@ -19,11 +19,11 @@ export declare const ACCESS_IMPORTANCE_BONUS = 3;
19
19
  /** Importance bonus per curate update */
20
20
  export declare const UPDATE_IMPORTANCE_BONUS = 5;
21
21
  /** BM25 relevance weight in compound score */
22
- export declare const W_RELEVANCE = 1;
22
+ export declare const W_RELEVANCE = 0.6;
23
23
  /** Importance weight in compound score */
24
- export declare const W_IMPORTANCE = 0;
24
+ export declare const W_IMPORTANCE = 0.2;
25
25
  /** Recency weight in compound score */
26
- export declare const W_RECENCY = 0;
26
+ export declare const W_RECENCY = 0.2;
27
27
  /** Importance threshold to promote draft -> validated */
28
28
  export declare const PROMOTE_TO_VALIDATED = 65;
29
29
  /** Importance threshold to promote validated -> core */
@@ -21,11 +21,11 @@ export const ACCESS_IMPORTANCE_BONUS = 3;
21
21
  /** Importance bonus per curate update */
22
22
  export const UPDATE_IMPORTANCE_BONUS = 5;
23
23
  /** BM25 relevance weight in compound score */
24
- export const W_RELEVANCE = 1;
24
+ export const W_RELEVANCE = 0.6;
25
25
  /** Importance weight in compound score */
26
- export const W_IMPORTANCE = 0;
26
+ export const W_IMPORTANCE = 0.2;
27
27
  /** Recency weight in compound score */
28
- export const W_RECENCY = 0;
28
+ export const W_RECENCY = 0.2;
29
29
  /** Importance threshold to promote draft -> validated */
30
30
  export const PROMOTE_TO_VALIDATED = 65;
31
31
  /** Importance threshold to promote validated -> core */
@@ -36,8 +36,8 @@ export const DEMOTE_FROM_CORE = 60;
36
36
  export const DEMOTE_FROM_VALIDATED = 35;
37
37
  /** Search score multiplier per maturity tier */
38
38
  export const TIER_BOOST = {
39
- core: 1,
40
- draft: 1,
39
+ core: 1.15,
40
+ draft: 0.85,
41
41
  validated: 1,
42
42
  };
43
43
  // ---------------------------------------------------------------------------
@@ -41,6 +41,10 @@ export interface ArchiveStubFrontmatter {
41
41
  type: 'archive_stub';
42
42
  }
43
43
  export interface ManifestEntry {
44
+ /** Relative path to .abstract.md sibling, if it exists */
45
+ abstractPath?: string;
46
+ /** Token count of .abstract.md (used for lane budgeting) */
47
+ abstractTokens?: number;
44
48
  /** Importance score from frontmatter (0-100, default 50) */
45
49
  importance?: number;
46
50
  /** Condensation order (only for summaries) */