byterover-cli 3.6.1 → 3.7.1

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 (50) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/core/interfaces/cipher-services.d.ts +7 -0
  3. package/dist/agent/infra/agent/cipher-agent.js +4 -2
  4. package/dist/agent/infra/agent/service-initializer.js +28 -14
  5. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  6. package/dist/agent/infra/sandbox/curate-service.js +6 -4
  7. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +4 -2
  8. package/dist/agent/infra/tools/implementations/curate-tool.js +169 -25
  9. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +2 -0
  10. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +1 -1
  11. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +0 -8
  12. package/dist/agent/infra/tools/implementations/memory-symbol-tree.js +3 -15
  13. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +49 -4
  14. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +123 -53
  15. package/dist/agent/infra/tools/implementations/search-knowledge-tool.d.ts +2 -0
  16. package/dist/agent/infra/tools/tool-provider.js +1 -0
  17. package/dist/agent/infra/tools/tool-registry.d.ts +7 -0
  18. package/dist/agent/infra/tools/tool-registry.js +13 -6
  19. package/dist/oclif/commands/dream.d.ts +4 -0
  20. package/dist/oclif/commands/dream.js +31 -13
  21. package/dist/server/constants.d.ts +1 -1
  22. package/dist/server/constants.js +20 -1
  23. package/dist/server/core/domain/knowledge/markdown-writer.d.ts +12 -42
  24. package/dist/server/core/domain/knowledge/markdown-writer.js +55 -96
  25. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +18 -37
  26. package/dist/server/core/domain/knowledge/memory-scoring.js +36 -85
  27. package/dist/server/core/domain/knowledge/runtime-signals-schema.d.ts +59 -0
  28. package/dist/server/core/domain/knowledge/runtime-signals-schema.js +46 -0
  29. package/dist/server/core/domain/knowledge/sidecar-logging.d.ts +14 -0
  30. package/dist/server/core/domain/knowledge/sidecar-logging.js +18 -0
  31. package/dist/server/core/interfaces/storage/i-runtime-signal-store.d.ts +111 -0
  32. package/dist/server/core/interfaces/storage/i-runtime-signal-store.js +38 -0
  33. package/dist/server/infra/context-tree/file-context-tree-archive-service.d.ts +16 -6
  34. package/dist/server/infra/context-tree/file-context-tree-archive-service.js +91 -32
  35. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +14 -0
  36. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +20 -7
  37. package/dist/server/infra/context-tree/runtime-signal-store.d.ts +46 -0
  38. package/dist/server/infra/context-tree/runtime-signal-store.js +118 -0
  39. package/dist/server/infra/daemon/agent-process.js +25 -4
  40. package/dist/server/infra/dream/operations/consolidate.d.ts +17 -0
  41. package/dist/server/infra/dream/operations/consolidate.js +40 -19
  42. package/dist/server/infra/dream/operations/prune.d.ts +18 -0
  43. package/dist/server/infra/dream/operations/prune.js +31 -20
  44. package/dist/server/infra/dream/operations/synthesize.d.ts +13 -0
  45. package/dist/server/infra/dream/operations/synthesize.js +15 -3
  46. package/dist/server/infra/executor/dream-executor.d.ts +8 -0
  47. package/dist/server/infra/executor/dream-executor.js +3 -0
  48. package/dist/server/templates/skill/SKILL.md +79 -22
  49. package/oclif.manifest.json +429 -429
  50. package/package.json +1 -1
@@ -46,14 +46,6 @@ function determineKind(segments) {
46
46
  }
47
47
  }
48
48
  }
49
- function extractMetadataFromScoring(scoring) {
50
- return {
51
- importance: scoring.importance ?? 50,
52
- keywords: [],
53
- maturity: scoring.maturity ?? 'draft',
54
- tags: [],
55
- };
56
- }
57
49
  /**
58
50
  * Get or create a folder symbol (Domain/Topic/Subtopic) at the given path.
59
51
  * Creates intermediate nodes as needed.
@@ -133,12 +125,8 @@ export function buildSymbolTree(documentMap, summaryMap) {
133
125
  const segments = doc.path.split('/');
134
126
  const folderPath = segments.slice(0, -1).join('/');
135
127
  const folderNode = symbolMap.get(folderPath);
136
- if (folderNode) {
137
- folderNode.metadata = {
138
- ...folderNode.metadata,
139
- ...extractMetadataFromScoring(doc.scoring),
140
- };
141
- }
128
+ // Post-commit-5: ranking signals (importance, maturity) are read from the
129
+ // sidecar at query time; the symbol tree only carries structural metadata.
142
130
  // Also register the context.md path itself for direct lookups
143
131
  symbolMap.set(doc.path, folderNode ?? getOrCreateFolderNode(symbolMap, root, folderPath, segments.slice(0, -1)));
144
132
  }
@@ -150,7 +138,7 @@ export function buildSymbolTree(documentMap, summaryMap) {
150
138
  const contextNode = {
151
139
  children: [],
152
140
  kind: MemorySymbolKind.Context,
153
- metadata: extractMetadataFromScoring(doc.scoring),
141
+ metadata: { ...DEFAULT_METADATA },
154
142
  name: doc.title || segments.at(-1).replace(/\.md$/, ''),
155
143
  parent: parentNode,
156
144
  path: doc.path,
@@ -1,4 +1,6 @@
1
+ import type { IRuntimeSignalStore } from '../../../../server/core/interfaces/storage/i-runtime-signal-store.js';
1
2
  import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
3
+ import type { ILogger } from '../../../core/interfaces/i-logger.js';
2
4
  import type { ISearchKnowledgeService, SearchKnowledgeResult } from '../../sandbox/tools-sdk.js';
3
5
  /**
4
6
  * Configuration for SearchKnowledgeService.
@@ -8,6 +10,18 @@ export interface SearchKnowledgeServiceConfig {
8
10
  baseDirectory?: string;
9
11
  /** Cache TTL in milliseconds (defaults to 5000) */
10
12
  cacheTtlMs?: number;
13
+ /**
14
+ * Optional logger. When provided, sidecar read failures on the ranking
15
+ * path emit a `warn` so the fail-open degradation is observable rather
16
+ * than silent.
17
+ */
18
+ logger?: ILogger;
19
+ /**
20
+ * Sidecar store for runtime ranking signals. When provided, `flushAccessHits`
21
+ * mirrors its importance/accessCount/maturity bumps to the store alongside
22
+ * the existing markdown writes. Phase 3 of the runtime-signals migration.
23
+ */
24
+ runtimeSignalStore?: IRuntimeSignalStore;
11
25
  }
12
26
  /**
13
27
  * Extended search options supporting symbolic filters.
@@ -36,15 +50,24 @@ export declare class SearchKnowledgeService implements ISearchKnowledgeService {
36
50
  private readonly baseDirectory;
37
51
  private readonly cacheTtlMs;
38
52
  private readonly fileSystem;
53
+ private readonly logger?;
39
54
  private readonly pendingAccessHits;
55
+ private readonly runtimeSignalStore?;
40
56
  private readonly state;
41
57
  constructor(fileSystem: IFileSystem, config?: SearchKnowledgeServiceConfig);
42
58
  /**
43
- * Flush accumulated access hits to disk by updating frontmatter scoring.
44
- * Called during index rebuild to batch writes and avoid write amplification.
45
- * Best-effort: errors are swallowed per file.
59
+ * Flush accumulated access hits to the runtime-signal sidecar.
60
+ *
61
+ * Post-commit-5 this no longer writes to markdown — ranking signals
62
+ * live exclusively in the sidecar. The `contextTreePath` parameter is
63
+ * retained for signature compatibility with the cache-invalidation
64
+ * callback but is no longer used.
65
+ *
66
+ * Returns `true` when hits were processed so the caller knows to
67
+ * refresh file mtimes (historical contract); with markdown writes
68
+ * removed the refresh is harmless but kept for symmetry.
46
69
  */
47
- flushAccessHits(contextTreePath: string): Promise<boolean>;
70
+ flushAccessHits(_contextTreePath: string): Promise<boolean>;
48
71
  /**
49
72
  * Search the knowledge base for relevant topics.
50
73
  * Supports symbolic path queries, scoped search, kind/maturity filtering, and overview mode.
@@ -64,6 +87,28 @@ export declare class SearchKnowledgeService implements ISearchKnowledgeService {
64
87
  * For archive stubs, extracts points_to path into archiveFullPath.
65
88
  */
66
89
  private enrichResult;
90
+ /**
91
+ * Load the runtime-signal map for this search via `getMany`, limiting the
92
+ * request to paths that this search could possibly rank: every indexed
93
+ * document plus every summary sibling (parent-propagation can touch any
94
+ * summary). The returned map contains entries only for paths with stored
95
+ * signals; callers use `.has()` / `.get()` to distinguish missing from
96
+ * default. Returns an empty map on sidecar failure so ranking degrades to
97
+ * BM25 alone rather than erroring out mid-query.
98
+ */
99
+ private loadSignalsByPath;
100
+ /**
101
+ * Mirror a batch of access-hit bumps into the runtime-signal sidecar.
102
+ *
103
+ * Matches the markdown flush semantics above:
104
+ * - `recordAccessHits` bumps `importance` by `ACCESS_IMPORTANCE_BONUS * count`
105
+ * and `accessCount` by `count`.
106
+ * - `determineTier` recomputes `maturity` from the new importance.
107
+ * Both steps run inside `update`'s atomic read-modify-write callback so
108
+ * same-path contention within the process does not lose bumps.
109
+ * `updatedAt` is never mirrored — it is a content timestamp, not a signal.
110
+ */
111
+ private mirrorHitsToSignalStore;
67
112
  /**
68
113
  * Run the standard text-based MiniSearch pipeline, optionally scoped to a subtree.
69
114
  */
@@ -3,8 +3,9 @@ import { realpath } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { removeStopwords } from 'stopword';
5
5
  import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, OVERVIEW_EXTENSION, SHARED_SOURCE_LOCAL_SCORE_BOOST, SUMMARY_INDEX_FILE, } from '../../../../server/constants.js';
6
- import { parseFrontmatterScoring, updateScoringInContent, } from '../../../../server/core/domain/knowledge/markdown-writer.js';
7
- import { applyDecay, applyDefaultScoring, compoundScore, determineTier, recordAccessHits, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
6
+ import { applyDecay, compoundScore, determineTier, recordAccessHits, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
7
+ import { createDefaultRuntimeSignals, } from '../../../../server/core/domain/knowledge/runtime-signals-schema.js';
8
+ import { warnSidecarFailure } from '../../../../server/core/domain/knowledge/sidecar-logging.js';
8
9
  import { loadSources } from '../../../../server/core/domain/source/source-schema.js';
9
10
  import { isArchiveStub, isDerivedArtifact } from '../../../../server/infra/context-tree/derived-artifact.js';
10
11
  import { parseArchiveStubFrontmatter, parseSummaryFrontmatter, } from '../../../../server/infra/context-tree/summary-frontmatter.js';
@@ -55,7 +56,7 @@ function getSymbolPath(origin, relativePath) {
55
56
  * @param propagationFactor - Score multiplier per level up (default 0.55)
56
57
  * @returns New parent entries only — caller merges and re-sorts
57
58
  */
58
- function propagateScoresToParents(results, symbolTree, summaryMap, symbolPathDocMap, propagationFactor = 0.55) {
59
+ function propagateScoresToParents(results, symbolTree, summaryMap, symbolPathDocMap, signalsByPath, propagationFactor = 0.55) {
59
60
  const boosts = new Map();
60
61
  for (const r of results) {
61
62
  const symbol = symbolTree.symbolMap.get(r.path);
@@ -76,11 +77,15 @@ function propagateScoresToParents(results, symbolTree, summaryMap, symbolPathDoc
76
77
  const doc = getSummarySource(parentPath, summaryMap, symbolPathDocMap);
77
78
  if (!doc)
78
79
  continue;
79
- // Propagate the strongest child BM25 signal upward, then apply the parent
80
- // summary's own scoring exactly once. This avoids double-counting lifecycle
81
- // weights that are already baked into child compound scores.
82
- const finalScore = doc.scoring
83
- ? compoundScore(score, doc.scoring.importance ?? 50, doc.scoring.recency ?? 0.5, doc.scoring.maturity ?? 'draft')
80
+ // Propagate the strongest child BM25 signal upward. Apply the parent
81
+ // summary's own scoring boost only when the sidecar has a concrete entry
82
+ // for this path matches the pre-migration behaviour where summaries
83
+ // without scoring in their frontmatter were not boosted. Lookup uses the
84
+ // summary doc's file path (e.g. `auth/jwt/_index.md`) which matches the
85
+ // sidecar's relPath key scheme.
86
+ const sidecarSignals = signalsByPath.get(doc.path);
87
+ const finalScore = sidecarSignals
88
+ ? compoundScore(score, sidecarSignals)
84
89
  : score;
85
90
  boosted.push({
86
91
  backlinkCount: 0,
@@ -114,7 +119,6 @@ function getSummarySource(path, summaryMap, symbolPathDocMap) {
114
119
  return {
115
120
  excerpt: summaryDoc.excerpt ?? '',
116
121
  path: summaryDoc.path,
117
- scoring: summaryDoc.scoring,
118
122
  };
119
123
  }
120
124
  // Look up context.md via symbolPath-keyed map since documentMap keys are
@@ -124,7 +128,6 @@ function getSummarySource(path, summaryMap, symbolPathDocMap) {
124
128
  return {
125
129
  excerpt: extractExcerpt(contextDoc.content, contextDoc.title),
126
130
  path: contextDoc.path,
127
- scoring: contextDoc.scoring,
128
131
  };
129
132
  }
130
133
  return undefined;
@@ -318,7 +321,6 @@ async function indexOriginDocuments(fileSystem, origin, filesWithMtime) {
318
321
  const fullPath = join(origin.contextTreeRoot, filePath);
319
322
  const { content } = await fileSystem.readFile(fullPath);
320
323
  const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
321
- const scoring = parseFrontmatterScoring(content) ?? applyDefaultScoring();
322
324
  const qualifiedId = `${origin.originKey}::${filePath}`;
323
325
  const symbolPath = getSymbolPath(origin, filePath);
324
326
  // Check if a .overview.md sibling exists (written by abstract generation queue)
@@ -333,7 +335,6 @@ async function indexOriginDocuments(fileSystem, origin, filesWithMtime) {
333
335
  originKey: origin.originKey,
334
336
  ...(overviewPath !== undefined && { overviewPath }),
335
337
  path: filePath,
336
- scoring,
337
338
  symbolPath,
338
339
  title,
339
340
  };
@@ -352,16 +353,10 @@ async function indexOriginDocuments(fileSystem, origin, filesWithMtime) {
352
353
  const fm = parseSummaryFrontmatter(content);
353
354
  if (!fm)
354
355
  return null;
355
- // Persist frontmatter scoring so propagateScoresToParents can apply hotness/tier boosts
356
- const frontmatter = parseFrontmatterScoring(content);
357
- const scoring = frontmatter
358
- ? { importance: frontmatter.importance, maturity: frontmatter.maturity, recency: frontmatter.recency }
359
- : undefined;
360
356
  return {
361
357
  condensationOrder: fm.condensation_order,
362
358
  excerpt: stripMarkdownFrontmatter(content).slice(0, 400),
363
359
  path: getSymbolPath(origin, filePath),
364
- scoring,
365
360
  tokenCount: fm.token_count,
366
361
  };
367
362
  }
@@ -600,7 +595,9 @@ export class SearchKnowledgeService {
600
595
  baseDirectory;
601
596
  cacheTtlMs;
602
597
  fileSystem;
598
+ logger;
603
599
  pendingAccessHits = new Map();
600
+ runtimeSignalStore;
604
601
  state = {
605
602
  buildingPromise: undefined,
606
603
  cachedIndex: undefined,
@@ -609,34 +606,28 @@ export class SearchKnowledgeService {
609
606
  this.fileSystem = fileSystem;
610
607
  this.baseDirectory = config.baseDirectory ?? process.cwd();
611
608
  this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
609
+ this.logger = config.logger;
610
+ this.runtimeSignalStore = config.runtimeSignalStore;
612
611
  }
613
612
  /**
614
- * Flush accumulated access hits to disk by updating frontmatter scoring.
615
- * Called during index rebuild to batch writes and avoid write amplification.
616
- * Best-effort: errors are swallowed per file.
613
+ * Flush accumulated access hits to the runtime-signal sidecar.
614
+ *
615
+ * Post-commit-5 this no longer writes to markdown — ranking signals
616
+ * live exclusively in the sidecar. The `contextTreePath` parameter is
617
+ * retained for signature compatibility with the cache-invalidation
618
+ * callback but is no longer used.
619
+ *
620
+ * Returns `true` when hits were processed so the caller knows to
621
+ * refresh file mtimes (historical contract); with markdown writes
622
+ * removed the refresh is harmless but kept for symmetry.
617
623
  */
618
- async flushAccessHits(contextTreePath) {
624
+ async flushAccessHits(_contextTreePath) {
619
625
  if (this.pendingAccessHits.size === 0) {
620
626
  return false;
621
627
  }
622
628
  const hits = new Map(this.pendingAccessHits);
623
629
  this.pendingAccessHits.clear();
624
- const tasks = [...hits.entries()].map(async ([relPath, count]) => {
625
- try {
626
- const fullPath = join(contextTreePath, relPath);
627
- const { content } = await this.fileSystem.readFile(fullPath);
628
- const scoring = parseFrontmatterScoring(content) ?? applyDefaultScoring();
629
- const updated = recordAccessHits(scoring, count);
630
- const newTier = determineTier(updated.importance ?? 50, (updated.maturity ?? 'draft'));
631
- const finalScoring = { ...updated, maturity: newTier };
632
- const newContent = updateScoringInContent(content, finalScoring);
633
- await this.fileSystem.writeFile(fullPath, newContent);
634
- }
635
- catch {
636
- // Best-effort — swallow per-file errors
637
- }
638
- });
639
- await Promise.allSettled(tasks);
630
+ await this.mirrorHitsToSignalStore(hits);
640
631
  return true;
641
632
  }
642
633
  /**
@@ -674,9 +665,16 @@ export class SearchKnowledgeService {
674
665
  if (options?.overview) {
675
666
  return this.buildOverviewResult(symbolTree, referenceIndex, normalizedScope, options.overviewDepth);
676
667
  }
668
+ // Load the runtime-signal sidecar map for the specific paths this query
669
+ // touches: all document paths in the index plus their summary siblings.
670
+ // This is O(k) per search instead of O(all entries). Entries are present
671
+ // only for paths with stored signals — callers use `.has()` to distinguish
672
+ // missing from default, and `.get(path) ?? defaults` for ergonomic
673
+ // default-on-miss. On sidecar failure, degrade to BM25-only ranking.
674
+ const signalsByPath = await this.loadSignalsByPath(documentMap, summaryMap);
677
675
  // Symbolic path resolution: try path-based query first
678
676
  if (isPathLikeQuery(query, symbolTree)) {
679
- const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, options);
677
+ const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, signalsByPath, options);
680
678
  if (symbolicResult) {
681
679
  return symbolicResult;
682
680
  }
@@ -691,10 +689,10 @@ export class SearchKnowledgeService {
691
689
  const effectiveScope = (rawScope !== undefined && rawScope !== '' ? rawScope : undefined) ?? parsed.scopePath;
692
690
  const effectiveQuery = parsed.scopePath ? parsed.textQuery : query;
693
691
  // Run text-based MiniSearch (existing pipeline), optionally scoped to a subtree
694
- const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
692
+ const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, signalsByPath, options);
695
693
  // If scoped search returned nothing and we had a scope, fall back to global search
696
694
  if (textResult.results.length === 0 && effectiveScope && effectiveQuery) {
697
- return this.runTextSearch(query, documentMap, index, limit, undefined, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
695
+ return this.runTextSearch(query, documentMap, index, limit, undefined, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, signalsByPath, options);
698
696
  }
699
697
  return textResult;
700
698
  }
@@ -772,10 +770,72 @@ export class SearchKnowledgeService {
772
770
  symbolPath: isContextSummary ? summaryPath : symbol?.path,
773
771
  };
774
772
  }
773
+ /**
774
+ * Load the runtime-signal map for this search via `getMany`, limiting the
775
+ * request to paths that this search could possibly rank: every indexed
776
+ * document plus every summary sibling (parent-propagation can touch any
777
+ * summary). The returned map contains entries only for paths with stored
778
+ * signals; callers use `.has()` / `.get()` to distinguish missing from
779
+ * default. Returns an empty map on sidecar failure so ranking degrades to
780
+ * BM25 alone rather than erroring out mid-query.
781
+ */
782
+ async loadSignalsByPath(documentMap, summaryMap) {
783
+ if (!this.runtimeSignalStore)
784
+ return new Map();
785
+ const paths = new Set();
786
+ for (const doc of documentMap.values())
787
+ paths.add(doc.path);
788
+ for (const summary of summaryMap.values())
789
+ paths.add(summary.path);
790
+ try {
791
+ return await this.runtimeSignalStore.getMany([...paths]);
792
+ }
793
+ catch (error) {
794
+ this.logger?.warn(`SearchKnowledgeService: sidecar getMany failed, falling back to BM25-only ranking: ${error instanceof Error ? error.message : String(error)}`);
795
+ return new Map();
796
+ }
797
+ }
798
+ /**
799
+ * Mirror a batch of access-hit bumps into the runtime-signal sidecar.
800
+ *
801
+ * Matches the markdown flush semantics above:
802
+ * - `recordAccessHits` bumps `importance` by `ACCESS_IMPORTANCE_BONUS * count`
803
+ * and `accessCount` by `count`.
804
+ * - `determineTier` recomputes `maturity` from the new importance.
805
+ * Both steps run inside `update`'s atomic read-modify-write callback so
806
+ * same-path contention within the process does not lose bumps.
807
+ * `updatedAt` is never mirrored — it is a content timestamp, not a signal.
808
+ */
809
+ async mirrorHitsToSignalStore(hits) {
810
+ const store = this.runtimeSignalStore;
811
+ if (!store)
812
+ return;
813
+ const updates = new Map([...hits.entries()].map(([relPath, count]) => [
814
+ relPath,
815
+ (current) => {
816
+ const bumped = recordAccessHits(current, count);
817
+ return {
818
+ ...current,
819
+ accessCount: bumped.accessCount,
820
+ importance: bumped.importance,
821
+ maturity: determineTier(bumped.importance, current.maturity),
822
+ };
823
+ },
824
+ ]));
825
+ try {
826
+ await store.batchUpdate(updates);
827
+ }
828
+ catch (error) {
829
+ // Best-effort — sidecar failure must not break the flush. The sidecar
830
+ // will re-sync on the next bump for these paths; the warn makes the
831
+ // fail-open visible to operators.
832
+ warnSidecarFailure(this.logger, 'search-knowledge-flush', 'batchUpdate', `${updates.size} path(s)`, error);
833
+ }
834
+ }
775
835
  /**
776
836
  * Run the standard text-based MiniSearch pipeline, optionally scoped to a subtree.
777
837
  */
778
- runTextSearch(query, documentMap, index, limit, scopePath, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options) {
838
+ runTextSearch(query, documentMap, index, limit, scopePath, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, signalsByPath, options) {
779
839
  const filteredQuery = filterStopWords(query);
780
840
  const filteredWords = filteredQuery.split(/\s+/).filter((w) => w.length >= 2);
781
841
  // Build scope filter if a subtree is specified
@@ -811,14 +871,19 @@ export class SearchKnowledgeService {
811
871
  // Normalize BM25 scores to [0, 1) then blend with importance + recency via compound scoring.
812
872
  // Decay is computed lazily from file mtime — no disk writes during search.
813
873
  // Local results get a configurable score boost to prefer local knowledge over shared.
874
+ //
875
+ // Runtime signals (importance / recency / maturity) come from the sidecar
876
+ // `signalsByPath` map. Paths with no sidecar entry fall back to defaults —
877
+ // matches the pre-migration behaviour where missing-frontmatter files used
878
+ // applyDefaultScoring().
814
879
  const now = Date.now();
815
880
  const searchResults = rawResults.map((r) => {
816
881
  const doc = documentMap.get(r.id);
817
- const scoring = doc?.scoring ?? applyDefaultScoring();
882
+ const signals = (doc && signalsByPath.get(doc.path)) ?? createDefaultRuntimeSignals();
818
883
  const daysSince = doc ? Math.max(0, (now - doc.mtime) / 86_400_000) : 0;
819
- const decayed = applyDecay(scoring, daysSince);
884
+ const decayed = applyDecay(signals, daysSince);
820
885
  const bm25 = normalizeScore(r.score);
821
- let finalScore = compoundScore(bm25, decayed.importance ?? 50, decayed.recency ?? 1, decayed.maturity ?? 'draft');
886
+ let finalScore = compoundScore(bm25, decayed);
822
887
  // Local score boost: prefer local results over shared when scores are close
823
888
  if (doc?.origin === 'local') {
824
889
  finalScore = Math.min(finalScore + SHARED_SOURCE_LOCAL_SCORE_BOOST, 1);
@@ -883,10 +948,15 @@ export class SearchKnowledgeService {
883
948
  }
884
949
  if (options?.minMaturity && enriched.symbolKind) {
885
950
  const docMaturity = enriched.symbolKind === 'summary'
886
- ? (getSummarySource(enriched.path, summaryMap, symbolPathDocMap)?.scoring?.maturity ??
887
- symbolTree.symbolMap.get(enriched.path)?.metadata.maturity ??
888
- 'draft')
889
- : (symbolTree.symbolMap.get(document.symbolPath)?.metadata.maturity ?? 'draft');
951
+ ? (() => {
952
+ const summaryDoc = getSummarySource(enriched.path, summaryMap, symbolPathDocMap);
953
+ return ((summaryDoc && signalsByPath.get(summaryDoc.path)?.maturity) ??
954
+ symbolTree.symbolMap.get(enriched.path)?.metadata.maturity ??
955
+ 'draft');
956
+ })()
957
+ : (signalsByPath.get(document.path)?.maturity ??
958
+ symbolTree.symbolMap.get(document.symbolPath)?.metadata.maturity ??
959
+ 'draft');
890
960
  if ((MATURITY_TIER_RANK[docMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1)) {
891
961
  continue;
892
962
  }
@@ -900,7 +970,7 @@ export class SearchKnowledgeService {
900
970
  }
901
971
  }
902
972
  // Propagate scores upward to parent domain/topic nodes (hierarchical retrieval)
903
- const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap, symbolPathDocMap);
973
+ const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap, symbolPathDocMap, signalsByPath);
904
974
  for (const p of propagated) {
905
975
  // Apply local score boost to propagated summaries so they stay competitive
906
976
  // with boosted direct BM25 hits (the boost was already applied to direct hits above)
@@ -913,7 +983,7 @@ export class SearchKnowledgeService {
913
983
  continue;
914
984
  if (options?.minMaturity && p.symbolKind === 'summary') {
915
985
  const summaryDoc = getSummarySource(p.path, summaryMap, symbolPathDocMap);
916
- const summaryMaturity = summaryDoc?.scoring?.maturity ?? 'draft';
986
+ const summaryMaturity = (summaryDoc && signalsByPath.get(summaryDoc.path)?.maturity) ?? 'draft';
917
987
  if ((MATURITY_TIER_RANK[summaryMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1))
918
988
  continue;
919
989
  }
@@ -942,7 +1012,7 @@ export class SearchKnowledgeService {
942
1012
  /**
943
1013
  * Try to resolve the query as a symbolic path. Returns null if no path match found.
944
1014
  */
945
- trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, options) {
1015
+ trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, signalsByPath, options) {
946
1016
  const pathMatches = matchMemoryPath(symbolTree, query.split(/\s+/)[0].includes('/') ? query.split(/\s+/)[0] : query);
947
1017
  if (pathMatches.length === 0) {
948
1018
  return null;
@@ -971,7 +1041,7 @@ export class SearchKnowledgeService {
971
1041
  const textPart = query.slice(query.indexOf(pathPart) + pathPart.length).trim();
972
1042
  if (textPart) {
973
1043
  // Scoped search: search text within the matched subtree
974
- return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
1044
+ return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, signalsByPath, options);
975
1045
  }
976
1046
  // No text part — return all children of the matched node
977
1047
  const subtreeIds = getSubtreeDocumentIds(symbolTree, topMatch.path);
@@ -1,3 +1,4 @@
1
+ import type { IRuntimeSignalStore } from '../../../../server/core/interfaces/storage/i-runtime-signal-store.js';
1
2
  import type { Tool } from '../../../core/domain/tools/types.js';
2
3
  import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
3
4
  /**
@@ -6,6 +7,7 @@ import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
6
7
  export interface SearchKnowledgeToolConfig {
7
8
  baseDirectory?: string;
8
9
  cacheTtlMs?: number;
10
+ runtimeSignalStore?: IRuntimeSignalStore;
9
11
  }
10
12
  /**
11
13
  * Creates the search knowledge tool.
@@ -28,6 +28,7 @@ export class ToolProvider {
28
28
  maxContextTokens: 0,
29
29
  memoryManager: 0,
30
30
  processService: 0,
31
+ runtimeSignalStore: 0,
31
32
  sandboxService: 0,
32
33
  swarmCoordinator: 0,
33
34
  todoStorage: 0,
@@ -1,3 +1,4 @@
1
+ import type { IRuntimeSignalStore } from '../../../server/core/interfaces/storage/i-runtime-signal-store.js';
1
2
  import type { EnvironmentContext } from '../../core/domain/environment/types.js';
2
3
  import type { KnownTool } from '../../core/domain/tools/constants.js';
3
4
  import type { Tool } from '../../core/domain/tools/types.js';
@@ -42,6 +43,12 @@ export interface ToolServices {
42
43
  memoryManager?: MemoryManager;
43
44
  /** Process service for command execution */
44
45
  processService?: IProcessService;
46
+ /**
47
+ * Sidecar store for per-machine ranking signals. When provided, curate and
48
+ * search tools mirror their scoring writes here alongside the markdown
49
+ * writes. Phase 3 of the runtime-signals migration.
50
+ */
51
+ runtimeSignalStore?: IRuntimeSignalStore;
45
52
  /** Sandbox service for code execution */
46
53
  sandboxService?: ISandboxService;
47
54
  /** Swarm coordinator for cross-provider memory queries */
@@ -57,7 +57,7 @@ export const TOOL_REGISTRY = {
57
57
  },
58
58
  [ToolName.CODE_EXEC]: {
59
59
  descriptionFile: 'code_exec',
60
- factory({ abstractQueue, environmentContext, fileSystemService, sandboxService, swarmCoordinator }) {
60
+ factory({ abstractQueue, environmentContext, fileSystemService, runtimeSignalStore, sandboxService, swarmCoordinator }) {
61
61
  const sandbox = getRequiredService(sandboxService, 'sandboxService');
62
62
  // Inject file system service into sandbox for Tools SDK
63
63
  if (fileSystemService && sandbox.setFileSystem) {
@@ -65,12 +65,14 @@ export const TOOL_REGISTRY = {
65
65
  }
66
66
  // Inject search knowledge service into sandbox for Tools SDK
67
67
  if (fileSystemService && sandbox.setSearchKnowledgeService) {
68
- const searchKnowledgeService = createSearchKnowledgeService(fileSystemService);
68
+ const searchKnowledgeService = createSearchKnowledgeService(fileSystemService, {
69
+ runtimeSignalStore,
70
+ });
69
71
  sandbox.setSearchKnowledgeService(searchKnowledgeService);
70
72
  }
71
73
  // Inject curate service into sandbox for Tools SDK
72
74
  if (sandbox.setCurateService) {
73
- const curateService = createCurateService(environmentContext?.workingDirectory, abstractQueue);
75
+ const curateService = createCurateService(environmentContext?.workingDirectory, abstractQueue, runtimeSignalStore);
74
76
  sandbox.setCurateService(curateService);
75
77
  }
76
78
  // Inject environment context into sandbox for env.* access
@@ -88,14 +90,17 @@ export const TOOL_REGISTRY = {
88
90
  },
89
91
  [ToolName.CURATE]: {
90
92
  descriptionFile: 'curate',
91
- factory: ({ abstractQueue, environmentContext }) => createCurateTool(environmentContext?.workingDirectory, abstractQueue),
93
+ factory: ({ abstractQueue, environmentContext, logger, runtimeSignalStore }) => createCurateTool(environmentContext?.workingDirectory, abstractQueue, runtimeSignalStore, logger),
92
94
  markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
93
95
  outputGuidance: 'curate',
94
96
  requiredServices: [],
95
97
  },
96
98
  [ToolName.EXPAND_KNOWLEDGE]: {
97
99
  descriptionFile: 'expand_knowledge',
98
- factory: ({ environmentContext }) => createExpandKnowledgeTool({ baseDirectory: environmentContext?.workingDirectory }),
100
+ factory: ({ environmentContext, runtimeSignalStore }) => createExpandKnowledgeTool({
101
+ baseDirectory: environmentContext?.workingDirectory,
102
+ runtimeSignalStore,
103
+ }),
99
104
  markers: [ToolMarker.Discovery],
100
105
  requiredServices: [],
101
106
  },
@@ -144,7 +149,9 @@ export const TOOL_REGISTRY = {
144
149
  },
145
150
  [ToolName.SEARCH_KNOWLEDGE]: {
146
151
  descriptionFile: 'search_knowledge',
147
- factory: (services) => createSearchKnowledgeTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
152
+ factory: (services) => createSearchKnowledgeTool(getRequiredService(services.fileSystemService, 'fileSystemService'), {
153
+ runtimeSignalStore: services.runtimeSignalStore,
154
+ }),
148
155
  markers: [ToolMarker.ContextBuilding, ToolMarker.Discovery],
149
156
  requiredServices: ['fileSystemService'],
150
157
  },
@@ -1,5 +1,9 @@
1
1
  import { Command } from '@oclif/core';
2
+ import type { ILogger } from '../../agent/core/interfaces/i-logger.js';
3
+ import { undoLastDream } from '../../server/infra/dream/dream-undo.js';
2
4
  import { type DaemonClientOptions } from '../lib/daemon-client.js';
5
+ /** Build the dep bundle for `undoLastDream` on the CLI-direct path; exported for wiring tests. */
6
+ export declare function buildUndoDeps(projectRoot: string, logger?: ILogger): Promise<Parameters<typeof undoLastDream>[0]>;
3
7
  export default class Dream extends Command {
4
8
  static description: string;
5
9
  static examples: string[];
@@ -1,10 +1,14 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
+ import { NoOpLogger } from '../../agent/core/interfaces/i-logger.js';
5
+ import { ConsoleLogger } from '../../agent/infra/logger/console-logger.js';
6
+ import { FileKeyStorage } from '../../agent/infra/storage/file-key-storage.js';
4
7
  import { BRV_DIR, CONTEXT_TREE_DIR } from '../../server/constants.js';
5
8
  import { TransportStateEventNames } from '../../server/core/domain/transport/schemas.js';
6
9
  import { FileContextTreeArchiveService } from '../../server/infra/context-tree/file-context-tree-archive-service.js';
7
10
  import { FileContextTreeManifestService } from '../../server/infra/context-tree/file-context-tree-manifest-service.js';
11
+ import { RuntimeSignalStore } from '../../server/infra/context-tree/runtime-signal-store.js';
8
12
  import { DreamLogStore } from '../../server/infra/dream/dream-log-store.js';
9
13
  import { DreamStateService } from '../../server/infra/dream/dream-state-service.js';
10
14
  import { undoLastDream } from '../../server/infra/dream/dream-undo.js';
@@ -16,6 +20,28 @@ import { TaskEvents } from '../../shared/transport/events/index.js';
16
20
  import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../lib/daemon-client.js';
17
21
  import { writeJsonResponse } from '../lib/json-response.js';
18
22
  import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion } from '../lib/task-client.js';
23
+ /** Build the dep bundle for `undoLastDream` on the CLI-direct path; exported for wiring tests. */
24
+ export async function buildUndoDeps(projectRoot, logger = new ConsoleLogger()) {
25
+ const brvDir = join(projectRoot, BRV_DIR);
26
+ const contextTreeDir = join(brvDir, CONTEXT_TREE_DIR);
27
+ const projectDataDir = getProjectDataDir(projectRoot);
28
+ // Runtime-signal sidecar — keeps archive/restore from leaking orphan
29
+ // signal entries on the CLI-direct `brv dream --undo` path. Mirrors the
30
+ // daemon wiring in agent-process.ts.
31
+ const keyStorage = new FileKeyStorage({ storageDir: projectDataDir });
32
+ await keyStorage.initialize();
33
+ const runtimeSignalStore = new RuntimeSignalStore(keyStorage, logger);
34
+ return {
35
+ archiveService: new FileContextTreeArchiveService(runtimeSignalStore),
36
+ contextTreeDir,
37
+ curateLogStore: new FileCurateLogStore({ baseDir: projectDataDir }),
38
+ dreamLogStore: new DreamLogStore({ baseDir: brvDir }),
39
+ dreamStateService: new DreamStateService({ baseDir: brvDir }),
40
+ manifestService: new FileContextTreeManifestService({ baseDirectory: projectRoot, runtimeSignalStore }),
41
+ projectRoot,
42
+ reviewBackupStore: new FileReviewBackupStore(brvDir),
43
+ };
44
+ }
19
45
  export default class Dream extends Command {
20
46
  static description = 'Run background memory consolidation on the context tree';
21
47
  static examples = [
@@ -119,20 +145,12 @@ export default class Dream extends Command {
119
145
  }
120
146
  async runUndo(format) {
121
147
  const projectRoot = resolveProject()?.projectRoot ?? process.cwd();
122
- const brvDir = join(projectRoot, BRV_DIR);
123
- const contextTreeDir = join(brvDir, CONTEXT_TREE_DIR);
124
- const projectDataDir = getProjectDataDir(projectRoot);
148
+ // JSON mode: route sidecar warnings to a no-op so structured stdout
149
+ // is never paired with stderr noise that breaks downstream parsers.
150
+ const logger = format === 'json' ? new NoOpLogger() : new ConsoleLogger();
151
+ const deps = await buildUndoDeps(projectRoot, logger);
125
152
  try {
126
- const result = await undoLastDream({
127
- archiveService: new FileContextTreeArchiveService(),
128
- contextTreeDir,
129
- curateLogStore: new FileCurateLogStore({ baseDir: projectDataDir }),
130
- dreamLogStore: new DreamLogStore({ baseDir: brvDir }),
131
- dreamStateService: new DreamStateService({ baseDir: brvDir }),
132
- manifestService: new FileContextTreeManifestService({ baseDirectory: projectRoot }),
133
- projectRoot,
134
- reviewBackupStore: new FileReviewBackupStore(brvDir),
135
- });
153
+ const result = await undoLastDream(deps);
136
154
  if (format === 'json') {
137
155
  writeJsonResponse({ command: 'dream', data: { ...result, status: 'undone' }, success: true });
138
156
  }
@@ -71,6 +71,6 @@ export declare const OVERVIEW_EXTENSION = ".overview.md";
71
71
  export declare const MANIFEST_FILE = "_manifest.json";
72
72
  export declare const ARCHIVE_IMPORTANCE_THRESHOLD = 35;
73
73
  export declare const DEFAULT_GHOST_CUE_MAX_TOKENS = 220;
74
- /** Patterns the context-tree .gitignore must contain (derived artifacts only). */
74
+ /** Patterns the context-tree .gitignore must contain. */
75
75
  export declare const CONTEXT_TREE_GITIGNORE_PATTERNS: string[];
76
76
  export declare const CONTEXT_TREE_GITIGNORE_HEADER = "# Derived artifacts \u2014 do not track";