byterover-cli 3.7.0 → 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 (49) hide show
  1. package/dist/agent/core/interfaces/cipher-services.d.ts +7 -0
  2. package/dist/agent/infra/agent/cipher-agent.js +4 -2
  3. package/dist/agent/infra/agent/service-initializer.js +28 -14
  4. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  5. package/dist/agent/infra/sandbox/curate-service.js +6 -4
  6. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +4 -2
  7. package/dist/agent/infra/tools/implementations/curate-tool.js +169 -25
  8. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +2 -0
  9. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +1 -1
  10. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +0 -8
  11. package/dist/agent/infra/tools/implementations/memory-symbol-tree.js +3 -15
  12. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +49 -4
  13. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +123 -53
  14. package/dist/agent/infra/tools/implementations/search-knowledge-tool.d.ts +2 -0
  15. package/dist/agent/infra/tools/tool-provider.js +1 -0
  16. package/dist/agent/infra/tools/tool-registry.d.ts +7 -0
  17. package/dist/agent/infra/tools/tool-registry.js +13 -6
  18. package/dist/oclif/commands/dream.d.ts +4 -0
  19. package/dist/oclif/commands/dream.js +31 -13
  20. package/dist/server/constants.d.ts +1 -1
  21. package/dist/server/constants.js +20 -1
  22. package/dist/server/core/domain/knowledge/markdown-writer.d.ts +12 -42
  23. package/dist/server/core/domain/knowledge/markdown-writer.js +55 -96
  24. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +18 -37
  25. package/dist/server/core/domain/knowledge/memory-scoring.js +36 -85
  26. package/dist/server/core/domain/knowledge/runtime-signals-schema.d.ts +59 -0
  27. package/dist/server/core/domain/knowledge/runtime-signals-schema.js +46 -0
  28. package/dist/server/core/domain/knowledge/sidecar-logging.d.ts +14 -0
  29. package/dist/server/core/domain/knowledge/sidecar-logging.js +18 -0
  30. package/dist/server/core/interfaces/storage/i-runtime-signal-store.d.ts +111 -0
  31. package/dist/server/core/interfaces/storage/i-runtime-signal-store.js +38 -0
  32. package/dist/server/infra/context-tree/file-context-tree-archive-service.d.ts +16 -6
  33. package/dist/server/infra/context-tree/file-context-tree-archive-service.js +91 -32
  34. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +14 -0
  35. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +20 -7
  36. package/dist/server/infra/context-tree/runtime-signal-store.d.ts +46 -0
  37. package/dist/server/infra/context-tree/runtime-signal-store.js +118 -0
  38. package/dist/server/infra/daemon/agent-process.js +25 -4
  39. package/dist/server/infra/dream/operations/consolidate.d.ts +17 -0
  40. package/dist/server/infra/dream/operations/consolidate.js +40 -19
  41. package/dist/server/infra/dream/operations/prune.d.ts +18 -0
  42. package/dist/server/infra/dream/operations/prune.js +31 -20
  43. package/dist/server/infra/dream/operations/synthesize.d.ts +13 -0
  44. package/dist/server/infra/dream/operations/synthesize.js +15 -3
  45. package/dist/server/infra/executor/dream-executor.d.ts +8 -0
  46. package/dist/server/infra/executor/dream-executor.js +3 -0
  47. package/dist/server/templates/skill/SKILL.md +79 -22
  48. package/oclif.manifest.json +426 -426
  49. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import type { IRuntimeSignalStore } from '../../../server/core/interfaces/storage/i-runtime-signal-store.js';
1
2
  import type { AgentEventBus, SessionEventBus } from '../../infra/events/event-emitter.js';
2
3
  import type { FileSystemService } from '../../infra/file-system/file-system-service.js';
3
4
  import type { CompactionService } from '../../infra/llm/context/compaction/compaction-service.js';
@@ -51,6 +52,12 @@ export interface CipherAgentServices {
51
52
  messageStorageService: MessageStorageService;
52
53
  policyEngine: IPolicyEngine;
53
54
  processService: ProcessService;
55
+ /**
56
+ * Sidecar store for per-machine ranking signals kept out of the shared
57
+ * context-tree markdown (importance, recency, maturity, accessCount,
58
+ * updateCount). Reachable here for future wiring; no consumer uses it yet.
59
+ */
60
+ runtimeSignalStore: IRuntimeSignalStore;
54
61
  sandboxService: ISandboxService;
55
62
  systemPromptManager: SystemPromptManager;
56
63
  toolManager: ToolManager;
@@ -978,8 +978,10 @@ export class CipherAgent extends BaseAgent {
978
978
  services.abstractQueue.setGenerator(retryableFreshGenerator);
979
979
  });
980
980
  }
981
- // Rebuild sandbox CurateService with the queue — reuses existing hot-swap path
982
- const newCurateService = createCurateService(services.workingDirectory, services.abstractQueue);
981
+ // Rebuild sandbox CurateService with the queue — reuses existing hot-swap path.
982
+ // runtimeSignalStore is threaded so agent-driven curate ADD/UPDATE seed +
983
+ // bump the sidecar (matches the tool-registry wiring at construction time).
984
+ const newCurateService = createCurateService(services.workingDirectory, services.abstractQueue, services.runtimeSignalStore);
983
985
  services.sandboxService.setCurateService?.(newCurateService);
984
986
  // Atomically rebuild CURATE + INGEST_RESOURCE tools so both enqueue abstracts
985
987
  services.toolProvider.replaceTools([ToolName.CURATE, ToolName.INGEST_RESOURCE], { abstractQueue: services.abstractQueue, contentGenerator: retryableCurateGenerator });
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { RuntimeSignalStore } from '../../../server/infra/context-tree/runtime-signal-store.js';
15
16
  import { createBlobStorage } from '../blob/blob-storage-factory.js';
16
17
  import { EnvironmentContextBuilder } from '../environment/environment-context-builder.js';
17
18
  import { SessionEventBus } from '../events/event-emitter.js';
@@ -139,7 +140,22 @@ export async function createCipherAgentServices(config, agentEventBus) {
139
140
  // Priority 16 — right after context tree structure, before memories
140
141
  const mapSelectionContributor = new MapSelectionContributor('mapSelection', 16);
141
142
  systemPromptManager.registerContributor(mapSelectionContributor);
142
- // 6b. Swarm coordinatortry to load config and build providers.
143
+ // 6b. Storage layerinitialised before the swarm block so the swarm
144
+ // SearchKnowledgeService receives `runtimeSignalStore` at construction
145
+ // time. Post-commit-5 the markdown fallback is gone, so a swarm search
146
+ // without the sidecar would silently drop every access-hit bump.
147
+ const keyStorage = new FileKeyStorage({
148
+ storageDir: storageBasePath,
149
+ });
150
+ await keyStorage.initialize();
151
+ const messageStorage = new MessageStorageService(keyStorage);
152
+ const messageStorageService = messageStorage;
153
+ const historyStorage = new GranularHistoryStorage(messageStorage);
154
+ // Sidecar store for per-machine ranking signals (importance, recency,
155
+ // maturity, accessCount, updateCount). Kept out of the context-tree
156
+ // markdown so query-time bumps don't dirty version-controlled files.
157
+ const runtimeSignalStore = new RuntimeSignalStore(keyStorage, logger);
158
+ // 6c. Swarm coordinator — try to load config and build providers.
143
159
  // Missing config → fail-open (no swarm). Invalid config → warn but continue.
144
160
  let swarmCoordinator;
145
161
  try {
@@ -158,7 +174,11 @@ export async function createCipherAgentServices(config, agentEventBus) {
158
174
  logger.warn(`Swarm provider issue: ${error.provider}: ${error.message}`);
159
175
  }
160
176
  const swarmProviders = buildProvidersFromConfig(swarmConfig, {
161
- searchService: createSearchKnowledgeService(fileSystemService),
177
+ searchService: createSearchKnowledgeService(fileSystemService, {
178
+ baseDirectory: workingDirectory,
179
+ logger,
180
+ runtimeSignalStore,
181
+ }),
162
182
  });
163
183
  if (swarmProviders.length > 0) {
164
184
  swarmCoordinator = new SwarmCoordinator(swarmProviders, swarmConfig);
@@ -182,7 +202,7 @@ export async function createCipherAgentServices(config, agentEventBus) {
182
202
  }
183
203
  // 7. Abstract generation queue (generator injected later via rebindCurateTools)
184
204
  const abstractQueue = new AbstractGenerationQueue(workingDirectory);
185
- // 8. Tool provider (depends on FileSystemService, ProcessService, MemoryManager, SystemPromptManager)
205
+ // 9. Tool provider (depends on FileSystemService, ProcessService, MemoryManager, SystemPromptManager)
186
206
  const verbose = config.llm.verbose ?? false;
187
207
  const descriptionLoader = new ToolDescriptionLoader();
188
208
  const toolProvider = new ToolProvider({
@@ -192,28 +212,21 @@ export async function createCipherAgentServices(config, agentEventBus) {
192
212
  getToolProvider: () => toolProvider,
193
213
  memoryManager,
194
214
  processService,
215
+ runtimeSignalStore,
195
216
  sandboxService,
196
217
  swarmCoordinator,
197
218
  }, systemPromptManager, descriptionLoader);
198
219
  await toolProvider.initialize();
199
- // 9. Policy engine with default rules for autonomous execution
220
+ // 10. Policy engine with default rules for autonomous execution
200
221
  const policyEngine = new PolicyEngine({ defaultDecision: 'ALLOW' });
201
222
  policyEngine.addRules(DEFAULT_POLICY_RULES);
202
- // 10. Tool scheduler (orchestrates policy check → execution)
223
+ // 11. Tool scheduler (orchestrates policy check → execution)
203
224
  const toolScheduler = new CoreToolScheduler(toolProvider, policyEngine, undefined, {
204
225
  verbose,
205
226
  });
206
- // 11. Tool manager (with scheduler for policy-based execution)
227
+ // 12. Tool manager (with scheduler for policy-based execution)
207
228
  const toolManager = new ToolManager(toolProvider, toolScheduler);
208
229
  await toolManager.initialize();
209
- // 11. History storage - granular file-based storage
210
- const keyStorage = new FileKeyStorage({
211
- storageDir: storageBasePath,
212
- });
213
- await keyStorage.initialize();
214
- const messageStorage = new MessageStorageService(keyStorage);
215
- const messageStorageService = messageStorage;
216
- const historyStorage = new GranularHistoryStorage(messageStorage);
217
230
  // CompactionService for context overflow management
218
231
  const tokenizer = new GeminiTokenizer(config.model ?? 'gemini-3-flash-preview');
219
232
  const compactionService = new CompactionService(messageStorage, tokenizer, {
@@ -239,6 +252,7 @@ export async function createCipherAgentServices(config, agentEventBus) {
239
252
  messageStorageService,
240
253
  policyEngine,
241
254
  processService,
255
+ runtimeSignalStore,
242
256
  sandboxService,
243
257
  systemPromptManager,
244
258
  toolManager,
@@ -2,6 +2,7 @@
2
2
  * Curate service implementation for sandbox integration.
3
3
  * Wraps the curate-tool logic for use in the sandbox's tools.* SDK.
4
4
  */
5
+ import type { IRuntimeSignalStore } from '../../../server/core/interfaces/storage/i-runtime-signal-store.js';
5
6
  import type { CurateOperation, CurateOptions, CurateResult, DetectDomainsInput, DetectDomainsResult, ICurateService } from '../../core/interfaces/i-curate-service.js';
6
7
  import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
7
8
  /**
@@ -10,8 +11,9 @@ import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
10
11
  */
11
12
  export declare class CurateService implements ICurateService {
12
13
  private readonly abstractQueue?;
14
+ private readonly runtimeSignalStore?;
13
15
  private readonly workingDirectory;
14
- constructor(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue | undefined);
16
+ constructor(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue | undefined, runtimeSignalStore?: IRuntimeSignalStore | undefined);
15
17
  /**
16
18
  * Execute curate operations on knowledge topics.
17
19
  *
@@ -35,4 +37,4 @@ export declare class CurateService implements ICurateService {
35
37
  * @param workingDirectory - Working directory for resolving relative paths
36
38
  * @returns CurateService instance
37
39
  */
38
- export declare function createCurateService(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue): ICurateService;
40
+ export declare function createCurateService(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue, runtimeSignalStore?: IRuntimeSignalStore): ICurateService;
@@ -74,9 +74,11 @@ function validateOperations(operations) {
74
74
  */
75
75
  export class CurateService {
76
76
  abstractQueue;
77
+ runtimeSignalStore;
77
78
  workingDirectory;
78
- constructor(workingDirectory, abstractQueue) {
79
+ constructor(workingDirectory, abstractQueue, runtimeSignalStore) {
79
80
  this.abstractQueue = abstractQueue;
81
+ this.runtimeSignalStore = runtimeSignalStore;
80
82
  this.workingDirectory = workingDirectory ?? process.cwd();
81
83
  }
82
84
  /**
@@ -120,7 +122,7 @@ export class CurateService {
120
122
  };
121
123
  }
122
124
  // Call the underlying executeCurate function from curate-tool
123
- const result = await executeCurate({ basePath, operations }, undefined, this.abstractQueue);
125
+ const result = await executeCurate({ basePath, operations }, undefined, this.abstractQueue, this.runtimeSignalStore);
124
126
  return result;
125
127
  }
126
128
  /**
@@ -159,6 +161,6 @@ export class CurateService {
159
161
  * @param workingDirectory - Working directory for resolving relative paths
160
162
  * @returns CurateService instance
161
163
  */
162
- export function createCurateService(workingDirectory, abstractQueue) {
163
- return new CurateService(workingDirectory, abstractQueue);
164
+ export function createCurateService(workingDirectory, abstractQueue, runtimeSignalStore) {
165
+ return new CurateService(workingDirectory, abstractQueue, runtimeSignalStore);
164
166
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import type { IRuntimeSignalStore } from '../../../../server/core/interfaces/storage/i-runtime-signal-store.js';
2
3
  import type { Tool, ToolExecutionContext } from '../../../core/domain/tools/types.js';
4
+ import type { ILogger } from '../../../core/interfaces/i-logger.js';
3
5
  import type { AbstractGenerationQueue } from '../../map/abstract-queue.js';
4
6
  /**
5
7
  * Operation types for curating knowledge topics.
@@ -574,6 +576,6 @@ export interface CurateOutput {
574
576
  * Execute curate operations on knowledge topics.
575
577
  * Exported for use by CurateService in sandbox.
576
578
  */
577
- export declare function executeCurate(input: unknown, _context?: ToolExecutionContext, abstractQueue?: AbstractGenerationQueue): Promise<CurateOutput>;
578
- export declare function createCurateTool(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue): Tool;
579
+ export declare function executeCurate(input: unknown, _context?: ToolExecutionContext, abstractQueue?: AbstractGenerationQueue, runtimeSignalStore?: IRuntimeSignalStore, logger?: ILogger): Promise<CurateOutput>;
580
+ export declare function createCurateTool(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue, runtimeSignalStore?: IRuntimeSignalStore, logger?: ILogger): Tool;
579
581
  export {};
@@ -2,12 +2,90 @@ import { basename, dirname, join, relative, resolve } from 'node:path';
2
2
  import { z } from 'zod';
3
3
  import { REVIEW_BACKUPS_DIR } from '../../../../server/constants.js';
4
4
  import { DirectoryManager } from '../../../../server/core/domain/knowledge/directory-manager.js';
5
- import { MarkdownWriter, parseFrontmatterScoring } from '../../../../server/core/domain/knowledge/markdown-writer.js';
6
- import { applyDefaultScoring, determineTier, recordCurateUpdate, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
5
+ import { MarkdownWriter, parseCreatedAt } from '../../../../server/core/domain/knowledge/markdown-writer.js';
6
+ import { determineTier, mergeScoring, recordCurateUpdate, } 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';
7
9
  import { toSnakeCase } from '../../../../server/utils/file-helpers.js';
8
10
  import { deriveImpactFromLoss, detectStructuralLoss } from '../../../core/domain/knowledge/conflict-detector.js';
9
11
  import { resolveStructuralLoss } from '../../../core/domain/knowledge/conflict-resolver.js';
10
12
  import { ToolName } from '../../../core/domain/tools/constants.js';
13
+ /**
14
+ * Derive the sidecar relPath (forward-slash, relative to the context tree
15
+ * root) from an absolute context-file path and the operation basePath.
16
+ */
17
+ function relPathFromContextPath(contextPath, basePath) {
18
+ return relative(basePath, contextPath).split('\\').join('/');
19
+ }
20
+ /**
21
+ * Preserve the original `createdAt` from the existing markdown frontmatter on
22
+ * UPDATE. `createdAt` is immutable content metadata, not a runtime signal, so
23
+ * it stays in the markdown source-of-truth. Falls back to a fresh timestamp
24
+ * when the existing file has no `createdAt` (old files or those that never
25
+ * had it).
26
+ */
27
+ function existingCreatedAt(existingContent) {
28
+ if (!existingContent)
29
+ return new Date().toISOString();
30
+ return parseCreatedAt(existingContent) ?? new Date().toISOString();
31
+ }
32
+ const CURATE_SITE = 'curate-tool';
33
+ /**
34
+ * Seed the sidecar with default signals for a newly-added file.
35
+ * Best-effort: sidecar write failures never break the markdown operation.
36
+ */
37
+ async function seedSidecarDefaults(store, relPath, logger) {
38
+ if (!store)
39
+ return;
40
+ try {
41
+ await store.set(relPath, createDefaultRuntimeSignals());
42
+ }
43
+ catch (error) {
44
+ warnSidecarFailure(logger, CURATE_SITE, 'seed', relPath, error);
45
+ }
46
+ }
47
+ /**
48
+ * Mirror a curate UPDATE into the sidecar.
49
+ *
50
+ * Applies `recordCurateUpdate`-equivalent bumps (importance +5, recency=1,
51
+ * updateCount+1) and recomputes `maturity` via `determineTier` inside the
52
+ * atomic updater so same-path contention does not lose writes.
53
+ * `updatedAt` is intentionally NOT mirrored — it is a content timestamp
54
+ * that stays in markdown frontmatter.
55
+ */
56
+ async function mirrorCurateUpdate(store, relPath, logger) {
57
+ if (!store)
58
+ return;
59
+ try {
60
+ await store.update(relPath, (current) => {
61
+ const bumped = recordCurateUpdate(current);
62
+ return {
63
+ ...current,
64
+ importance: bumped.importance,
65
+ maturity: determineTier(bumped.importance, current.maturity),
66
+ recency: bumped.recency,
67
+ updateCount: bumped.updateCount,
68
+ };
69
+ });
70
+ }
71
+ catch (error) {
72
+ warnSidecarFailure(logger, CURATE_SITE, 'update', relPath, error);
73
+ }
74
+ }
75
+ /**
76
+ * Remove a path's sidecar entry after its markdown file was deleted or moved
77
+ * (DELETE, MERGE source, archive). Best-effort.
78
+ */
79
+ async function dropSidecar(store, relPath, logger) {
80
+ if (!store)
81
+ return;
82
+ try {
83
+ await store.delete(relPath);
84
+ }
85
+ catch (error) {
86
+ warnSidecarFailure(logger, CURATE_SITE, 'drop', relPath, error);
87
+ }
88
+ }
11
89
  /**
12
90
  * Operation types for curating knowledge topics.
13
91
  * Inspired by ACE Curator patterns.
@@ -453,7 +531,7 @@ function buildFullPath(basePath, knowledgePath) {
453
531
  /**
454
532
  * Execute ADD operation - create new domain/topic/subtopic with {title}.md
455
533
  */
456
- async function executeAdd(basePath, operation, onAfterWrite) {
534
+ async function executeAdd(basePath, operation, onAfterWrite, runtimeSignalStore, logger) {
457
535
  const { confidence, content, domainContext, impact, path, reason, subtopicContext, summary, title, topicContext } = operation;
458
536
  const reviewMeta = deriveReviewMetadata('ADD', confidence, impact);
459
537
  if (!title) {
@@ -513,15 +591,18 @@ async function executeAdd(basePath, operation, onAfterWrite) {
513
591
  rawConcept: filteredContent.rawConcept,
514
592
  reason,
515
593
  relations: filteredContent.relations,
516
- scoring: applyDefaultScoring(),
517
594
  snippets: filteredContent.snippets ?? [],
518
595
  summary,
519
596
  tags: filteredContent.tags,
597
+ timestamps: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
520
598
  });
521
599
  const filename = `${toSnakeCase(title)}.md`;
522
600
  const contextPath = join(finalPath, filename);
523
601
  await DirectoryManager.writeFileAtomic(contextPath, contextContent);
524
602
  onAfterWrite?.(contextPath, contextContent);
603
+ // Dual-write: seed the sidecar with default signals for the new file.
604
+ // Mirrors the default scoring applied to markdown frontmatter above.
605
+ await seedSidecarDefaults(runtimeSignalStore, relPathFromContextPath(contextPath, basePath), logger);
525
606
  await ensureContextMd(basePath, parsed, topicContext, subtopicContext, onAfterWrite);
526
607
  return {
527
608
  ...reviewMeta,
@@ -555,7 +636,7 @@ function maxImpact(a, b) {
555
636
  /**
556
637
  * Execute UPDATE operation - modify existing {title}.md
557
638
  */
558
- async function executeUpdate(basePath, operation, onAfterWrite) {
639
+ async function executeUpdate(basePath, operation, onAfterWrite, runtimeSignalStore, logger) {
559
640
  const { confidence, content, domainContext, impact, path, reason, subtopicContext, summary, title, topicContext } = operation;
560
641
  // Used for early-exit validation failures (before structural loss can be assessed)
561
642
  const baseReviewMeta = deriveReviewMetadata('UPDATE', confidence, impact);
@@ -606,12 +687,15 @@ async function executeUpdate(basePath, operation, onAfterWrite) {
606
687
  };
607
688
  }
608
689
  await createDomainContextIfMissing(basePath, parsed.domain, domainContext, onAfterWrite);
609
- // Read existing file to preserve scoring metadata and detect structural loss
690
+ // Read existing file to detect structural loss
610
691
  const existingContent = await DirectoryManager.readFile(contextPath);
611
- const existingScoring = existingContent ? parseFrontmatterScoring(existingContent) : undefined;
612
- const updatedScoring = existingScoring ? recordCurateUpdate(existingScoring) : applyDefaultScoring();
613
- const newTier = determineTier(updatedScoring.importance ?? 50, (updatedScoring.maturity ?? 'draft'));
614
- const finalScoring = { ...updatedScoring, maturity: newTier };
692
+ // Markdown only carries content timestamps post-commit-5. The sidecar
693
+ // handles all scoring (importance / recency / maturity / counts) via
694
+ // `mirrorCurateUpdate` below, inside an atomic read-modify-write.
695
+ const timestamps = {
696
+ createdAt: existingCreatedAt(existingContent),
697
+ updatedAt: new Date().toISOString(),
698
+ };
615
699
  // Filter out non-existent files from rawConcept.files
616
700
  const filteredContent = await filterValidFiles(content);
617
701
  // Extract previous summary from existing file's frontmatter (for review UI)
@@ -640,12 +724,16 @@ async function executeUpdate(basePath, operation, onAfterWrite) {
640
724
  const contextContent = MarkdownWriter.generateContext({
641
725
  ...resolvedContextData,
642
726
  reason,
643
- scoring: finalScoring,
644
727
  summary,
728
+ timestamps,
645
729
  });
646
730
  await backupBeforeWrite(contextPath, basePath);
647
731
  await DirectoryManager.writeFileAtomic(contextPath, contextContent);
648
732
  onAfterWrite?.(contextPath, contextContent);
733
+ // Dual-write: mirror the curate-update bumps (importance +5, recency=1,
734
+ // updateCount+1, maturity retiered) into the sidecar. `updatedAt` stays
735
+ // in markdown only — it is a content timestamp, not a runtime signal.
736
+ await mirrorCurateUpdate(runtimeSignalStore, relPathFromContextPath(contextPath, basePath), logger);
649
737
  await ensureContextMd(basePath, parsed, topicContext, subtopicContext, onAfterWrite);
650
738
  return {
651
739
  ...reviewMeta,
@@ -674,7 +762,7 @@ async function executeUpdate(basePath, operation, onAfterWrite) {
674
762
  * Execute UPSERT operation - automatically creates or updates based on file existence
675
763
  * This is the recommended operation type as it eliminates the need for pre-checks.
676
764
  */
677
- async function executeUpsert(basePath, operation, onAfterWrite) {
765
+ async function executeUpsert(basePath, operation, onAfterWrite, runtimeSignalStore, logger) {
678
766
  const { path, reason, title } = operation;
679
767
  const reviewMeta = deriveReviewMetadata('UPSERT', operation.confidence, operation.impact);
680
768
  if (!title) {
@@ -716,7 +804,7 @@ async function executeUpsert(basePath, operation, onAfterWrite) {
716
804
  const exists = await DirectoryManager.fileExists(contextPath);
717
805
  if (exists) {
718
806
  // File exists - delegate to UPDATE logic
719
- const result = await executeUpdate(basePath, { ...operation, type: 'UPDATE' }, onAfterWrite);
807
+ const result = await executeUpdate(basePath, { ...operation, type: 'UPDATE' }, onAfterWrite, runtimeSignalStore, logger);
720
808
  // Return with UPSERT type but indicate it was an update
721
809
  return {
722
810
  ...result,
@@ -725,7 +813,7 @@ async function executeUpsert(basePath, operation, onAfterWrite) {
725
813
  };
726
814
  }
727
815
  // File doesn't exist - delegate to ADD logic
728
- const result = await executeAdd(basePath, { ...operation, type: 'ADD' }, onAfterWrite);
816
+ const result = await executeAdd(basePath, { ...operation, type: 'ADD' }, onAfterWrite, runtimeSignalStore, logger);
729
817
  // Return with UPSERT type but indicate it was an add
730
818
  return {
731
819
  ...result,
@@ -747,7 +835,7 @@ async function executeUpsert(basePath, operation, onAfterWrite) {
747
835
  /**
748
836
  * Execute MERGE operation - combine source file into target file, delete source file
749
837
  */
750
- async function executeMerge(basePath, operation, onAfterWrite) {
838
+ async function executeMerge(basePath, operation, onAfterWrite, runtimeSignalStore, logger) {
751
839
  const { confidence, domainContext, impact, mergeTarget, mergeTargetTitle, path, reason, subtopicContext, summary, title, topicContext, } = operation;
752
840
  const reviewMeta = deriveReviewMetadata('MERGE', confidence, impact);
753
841
  if (!title) {
@@ -831,11 +919,58 @@ async function executeMerge(basePath, operation, onAfterWrite) {
831
919
  // Backup both files before merge modifies target and deletes source
832
920
  await backupBeforeWrite(targetContextPath, basePath);
833
921
  await backupBeforeWrite(sourceContextPath, basePath);
922
+ // Capture source sidecar signals BEFORE any destructive operation so a
923
+ // mid-flow crash cannot leave the target unmerged with an orphaned
924
+ // source entry. The sidecar merge happens after the markdown writes
925
+ // succeed, using the captured snapshot.
926
+ const sourceRelPath = relPathFromContextPath(sourceContextPath, basePath);
927
+ const targetRelPath = relPathFromContextPath(targetContextPath, basePath);
928
+ const sourceSignalsSnapshot = runtimeSignalStore
929
+ ? await runtimeSignalStore.get(sourceRelPath)
930
+ : null;
834
931
  const mergedContent = MarkdownWriter.mergeContexts(sourceContent, targetContent, reason, summary);
835
932
  await DirectoryManager.writeFileAtomic(targetContextPath, mergedContent);
836
933
  onAfterWrite?.(targetContextPath, mergedContent);
837
934
  await DirectoryManager.deleteFile(sourceContextPath);
838
935
  await deleteDerivedSiblings(sourceContextPath);
936
+ // Dual-write: merge sidecar signals using `mergeScoring` (the canonical
937
+ // merge policy). Runs inside `update`'s atomic callback so a concurrent
938
+ // access-hit flush on the target cannot lose bumps.
939
+ //
940
+ // The target-update and source-delete are wrapped in separate try/catch
941
+ // blocks so an operator can tell which half failed. If update succeeds
942
+ // but delete throws the source sidecar entry becomes an orphan (source
943
+ // markdown is already gone, nothing will ever overwrite it). Tracked by
944
+ // pruneOrphans in the backlog.
945
+ if (runtimeSignalStore && sourceSignalsSnapshot) {
946
+ let targetUpdated = false;
947
+ try {
948
+ await runtimeSignalStore.update(targetRelPath, (current) => {
949
+ const merged = mergeScoring(sourceSignalsSnapshot, current);
950
+ return {
951
+ accessCount: merged.accessCount,
952
+ importance: merged.importance,
953
+ maturity: determineTier(merged.importance, merged.maturity),
954
+ recency: merged.recency,
955
+ updateCount: merged.updateCount,
956
+ };
957
+ });
958
+ targetUpdated = true;
959
+ }
960
+ catch (error) {
961
+ // Best-effort — markdown merge already succeeded.
962
+ warnSidecarFailure(logger, CURATE_SITE, 'merge-update', `${sourceRelPath} -> ${targetRelPath}`, error);
963
+ }
964
+ if (targetUpdated) {
965
+ try {
966
+ await runtimeSignalStore.delete(sourceRelPath);
967
+ }
968
+ catch (error) {
969
+ // Source sidecar is now a permanent orphan until pruneOrphans runs.
970
+ warnSidecarFailure(logger, CURATE_SITE, 'merge-delete', sourceRelPath, error);
971
+ }
972
+ }
973
+ }
839
974
  await ensureContextMd(basePath, sourceParsed, topicContext, subtopicContext, onAfterWrite);
840
975
  await ensureContextMd(basePath, targetParsed, topicContext, subtopicContext, onAfterWrite);
841
976
  return {
@@ -866,7 +1001,7 @@ async function executeMerge(basePath, operation, onAfterWrite) {
866
1001
  * Execute DELETE operation - remove specific file or entire folder
867
1002
  * If title is provided, deletes specific file; if omitted, deletes entire folder
868
1003
  */
869
- async function executeDelete(basePath, operation) {
1004
+ async function executeDelete(basePath, operation, runtimeSignalStore, logger) {
870
1005
  const { path, reason, title } = operation;
871
1006
  const reviewMeta = deriveReviewMetadata('DELETE', operation.confidence, operation.impact);
872
1007
  try {
@@ -900,6 +1035,9 @@ async function executeDelete(basePath, operation) {
900
1035
  await backupBeforeWrite(filePath, basePath);
901
1036
  await DirectoryManager.deleteFile(filePath);
902
1037
  await deleteDerivedSiblings(filePath);
1038
+ // Dual-write: drop the deleted file's sidecar entry so it does not
1039
+ // become an orphan.
1040
+ await dropSidecar(runtimeSignalStore, relPathFromContextPath(filePath, basePath), logger);
903
1041
  return {
904
1042
  ...reviewMeta,
905
1043
  filePath,
@@ -947,6 +1085,12 @@ async function executeDelete(basePath, operation) {
947
1085
  }
948
1086
  await Promise.all(mdFiles.map((f) => backupBeforeWrite(f, basePath)));
949
1087
  await DirectoryManager.deleteTopicRecursive(fullPath);
1088
+ // Dual-write: drop sidecar entries for every markdown file that was
1089
+ // deleted. Without this, folder deletes leak orphan signal entries.
1090
+ // Best-effort — the markdown delete has already succeeded.
1091
+ if (runtimeSignalStore) {
1092
+ await Promise.all(mdFiles.map((f) => dropSidecar(runtimeSignalStore, relPathFromContextPath(f, basePath), logger)));
1093
+ }
950
1094
  return {
951
1095
  ...reviewMeta,
952
1096
  additionalFilePaths: mdFiles,
@@ -974,7 +1118,7 @@ async function executeDelete(basePath, operation) {
974
1118
  * Execute curate operations on knowledge topics.
975
1119
  * Exported for use by CurateService in sandbox.
976
1120
  */
977
- export async function executeCurate(input, _context, abstractQueue) {
1121
+ export async function executeCurate(input, _context, abstractQueue, runtimeSignalStore, logger) {
978
1122
  const parseResult = CurateInputSchema.safeParse(input);
979
1123
  if (!parseResult.success) {
980
1124
  return {
@@ -1016,31 +1160,31 @@ export async function executeCurate(input, _context, abstractQueue) {
1016
1160
  let result;
1017
1161
  switch (operation.type) {
1018
1162
  case 'ADD': {
1019
- result = await executeAdd(basePath, operation, onAfterWrite);
1163
+ result = await executeAdd(basePath, operation, onAfterWrite, runtimeSignalStore, logger);
1020
1164
  if (result.status === 'success')
1021
1165
  summary.added++;
1022
1166
  break;
1023
1167
  }
1024
1168
  case 'DELETE': {
1025
- result = await executeDelete(basePath, operation);
1169
+ result = await executeDelete(basePath, operation, runtimeSignalStore, logger);
1026
1170
  if (result.status === 'success')
1027
1171
  summary.deleted++;
1028
1172
  break;
1029
1173
  }
1030
1174
  case 'MERGE': {
1031
- result = await executeMerge(basePath, operation, onAfterWrite);
1175
+ result = await executeMerge(basePath, operation, onAfterWrite, runtimeSignalStore, logger);
1032
1176
  if (result.status === 'success')
1033
1177
  summary.merged++;
1034
1178
  break;
1035
1179
  }
1036
1180
  case 'UPDATE': {
1037
- result = await executeUpdate(basePath, operation, onAfterWrite);
1181
+ result = await executeUpdate(basePath, operation, onAfterWrite, runtimeSignalStore, logger);
1038
1182
  if (result.status === 'success')
1039
1183
  summary.updated++;
1040
1184
  break;
1041
1185
  }
1042
1186
  case 'UPSERT': {
1043
- result = await executeUpsert(basePath, operation, onAfterWrite);
1187
+ result = await executeUpsert(basePath, operation, onAfterWrite, runtimeSignalStore, logger);
1044
1188
  // UPSERT counts as either added or updated based on what happened
1045
1189
  if (result.status === 'success') {
1046
1190
  if (result.message?.includes('created new')) {
@@ -1075,7 +1219,7 @@ export async function executeCurate(input, _context, abstractQueue) {
1075
1219
  /* eslint-enable no-await-in-loop */
1076
1220
  return { applied, summary };
1077
1221
  }
1078
- export function createCurateTool(workingDirectory, abstractQueue) {
1222
+ export function createCurateTool(workingDirectory, abstractQueue, runtimeSignalStore, logger) {
1079
1223
  return {
1080
1224
  description: `Curate knowledge topics with atomic operations. This tool manages the knowledge structure using four operation types and supports a two-part context model: Raw Concept + Narrative.
1081
1225
 
@@ -1275,10 +1419,10 @@ export function createCurateTool(workingDirectory, abstractQueue) {
1275
1419
  const parseResult = CurateInputSchema.safeParse(input);
1276
1420
  if (parseResult.success) {
1277
1421
  parseResult.data.basePath = resolve(workingDirectory, parseResult.data.basePath);
1278
- return executeCurate(parseResult.data, context, abstractQueue);
1422
+ return executeCurate(parseResult.data, context, abstractQueue, runtimeSignalStore, logger);
1279
1423
  }
1280
1424
  }
1281
- return executeCurate(input, context, abstractQueue);
1425
+ return executeCurate(input, context, abstractQueue, runtimeSignalStore, logger);
1282
1426
  },
1283
1427
  id: ToolName.CURATE,
1284
1428
  inputSchema: CurateInputSchema,
@@ -1,9 +1,11 @@
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
  /**
3
4
  * Configuration for expand knowledge tool.
4
5
  */
5
6
  export interface ExpandKnowledgeToolConfig {
6
7
  baseDirectory?: string;
8
+ runtimeSignalStore?: IRuntimeSignalStore;
7
9
  }
8
10
  /**
9
11
  * Creates the expand knowledge tool.
@@ -36,7 +36,7 @@ const ExpandKnowledgeInputSchema = z
36
36
  * @returns Configured expand knowledge tool
37
37
  */
38
38
  export function createExpandKnowledgeTool(config = {}) {
39
- const archiveService = new FileContextTreeArchiveService();
39
+ const archiveService = new FileContextTreeArchiveService(config.runtimeSignalStore);
40
40
  return {
41
41
  description: 'Retrieve full content from archived knowledge entries or L1 overview files. ' +
42
42
  'Use stubPath when search results include an archive_stub that you need to drill into. ' +
@@ -1,4 +1,3 @@
1
- import type { FrontmatterScoring } from '../../../../server/core/domain/knowledge/markdown-writer.js';
2
1
  /**
3
2
  * Symbol kinds in the memory hierarchy, ordered by depth.
4
3
  * Mirrors Serena's SymbolKind pattern for code symbols.
@@ -48,12 +47,6 @@ export interface SummaryDocLike {
48
47
  excerpt?: string;
49
48
  /** Path to the _index.md file, e.g. "domain/topic/_index.md" */
50
49
  path: string;
51
- /** Frontmatter scoring parsed from _index.md — used to apply hotness/importance to propagated hits */
52
- scoring?: {
53
- importance?: number;
54
- maturity?: string;
55
- recency?: number;
56
- };
57
50
  tokenCount: number;
58
51
  }
59
52
  /**
@@ -97,7 +90,6 @@ interface DocumentLike {
97
90
  content: string;
98
91
  id: string;
99
92
  path: string;
100
- scoring: FrontmatterScoring;
101
93
  title: string;
102
94
  }
103
95
  /**
@@ -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,