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
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Runtime signals — per-machine ranking fields that live in a sidecar store
3
+ * rather than in shared context-tree markdown frontmatter.
4
+ *
5
+ * These fields change on every query (access hit flush) and would otherwise
6
+ * dirty version control state and cause merge conflicts across teammates.
7
+ *
8
+ * Related: `features/runtime-signals/plan.md`
9
+ */
10
+ import { z } from 'zod';
11
+ export declare const DEFAULT_IMPORTANCE = 50;
12
+ export declare const DEFAULT_RECENCY = 1;
13
+ export declare const DEFAULT_MATURITY: "draft";
14
+ export declare const DEFAULT_ACCESS_COUNT = 0;
15
+ export declare const DEFAULT_UPDATE_COUNT = 0;
16
+ export declare const MaturityTierSchema: z.ZodEnum<["core", "draft", "validated"]>;
17
+ export declare const RuntimeSignalsSchema: z.ZodObject<{
18
+ accessCount: z.ZodDefault<z.ZodNumber>;
19
+ importance: z.ZodDefault<z.ZodNumber>;
20
+ maturity: z.ZodDefault<z.ZodEnum<["core", "draft", "validated"]>>;
21
+ recency: z.ZodDefault<z.ZodNumber>;
22
+ updateCount: z.ZodDefault<z.ZodNumber>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ accessCount: number;
25
+ importance: number;
26
+ maturity: "draft" | "core" | "validated";
27
+ recency: number;
28
+ updateCount: number;
29
+ }, {
30
+ accessCount?: number | undefined;
31
+ importance?: number | undefined;
32
+ maturity?: "draft" | "core" | "validated" | undefined;
33
+ recency?: number | undefined;
34
+ updateCount?: number | undefined;
35
+ }>;
36
+ export type MaturityTier = z.infer<typeof MaturityTierSchema>;
37
+ export type RuntimeSignals = z.infer<typeof RuntimeSignalsSchema>;
38
+ /**
39
+ * Frontmatter fields that remain in context-tree markdown after runtime
40
+ * signals are moved to the sidecar.
41
+ *
42
+ * `createdAt` is immutable. `updatedAt` reflects real content modifications
43
+ * (set by curate ADD/UPDATE and by MERGE); ranking updates never touch it.
44
+ */
45
+ export interface SemanticFrontmatter {
46
+ createdAt?: string;
47
+ keywords: string[];
48
+ related: string[];
49
+ summary?: string;
50
+ tags: string[];
51
+ title?: string;
52
+ updatedAt?: string;
53
+ }
54
+ /**
55
+ * Return a fresh RuntimeSignals with default values.
56
+ * Used by the sidecar store when a path has no entry yet, and by curate ADD
57
+ * when seeding a new knowledge file.
58
+ */
59
+ export declare function createDefaultRuntimeSignals(): RuntimeSignals;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Runtime signals — per-machine ranking fields that live in a sidecar store
3
+ * rather than in shared context-tree markdown frontmatter.
4
+ *
5
+ * These fields change on every query (access hit flush) and would otherwise
6
+ * dirty version control state and cause merge conflicts across teammates.
7
+ *
8
+ * Related: `features/runtime-signals/plan.md`
9
+ */
10
+ import { z } from 'zod';
11
+ // ---------------------------------------------------------------------------
12
+ // Defaults (used when a path has no sidecar entry yet)
13
+ // ---------------------------------------------------------------------------
14
+ export const DEFAULT_IMPORTANCE = 50;
15
+ export const DEFAULT_RECENCY = 1;
16
+ export const DEFAULT_MATURITY = 'draft';
17
+ export const DEFAULT_ACCESS_COUNT = 0;
18
+ export const DEFAULT_UPDATE_COUNT = 0;
19
+ // ---------------------------------------------------------------------------
20
+ // Schema
21
+ // ---------------------------------------------------------------------------
22
+ export const MaturityTierSchema = z.enum(['core', 'draft', 'validated']);
23
+ export const RuntimeSignalsSchema = z.object({
24
+ accessCount: z.number().int().nonnegative().default(DEFAULT_ACCESS_COUNT),
25
+ importance: z.number().min(0).max(100).default(DEFAULT_IMPORTANCE),
26
+ maturity: MaturityTierSchema.default(DEFAULT_MATURITY),
27
+ recency: z.number().min(0).max(1).default(DEFAULT_RECENCY),
28
+ updateCount: z.number().int().nonnegative().default(DEFAULT_UPDATE_COUNT),
29
+ });
30
+ // ---------------------------------------------------------------------------
31
+ // Factories
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Return a fresh RuntimeSignals with default values.
35
+ * Used by the sidecar store when a path has no entry yet, and by curate ADD
36
+ * when seeding a new knowledge file.
37
+ */
38
+ export function createDefaultRuntimeSignals() {
39
+ return {
40
+ accessCount: DEFAULT_ACCESS_COUNT,
41
+ importance: DEFAULT_IMPORTANCE,
42
+ maturity: DEFAULT_MATURITY,
43
+ recency: DEFAULT_RECENCY,
44
+ updateCount: DEFAULT_UPDATE_COUNT,
45
+ };
46
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared helper for observing swallowed sidecar failures.
3
+ *
4
+ * Runtime-signals dual-write is best-effort: failures never break the
5
+ * caller's primary operation (markdown write, ranking read, etc.). After
6
+ * commit 5 the sidecar is the canonical source for ranking signals, so
7
+ * silent swallows hide real outages from operators. Every site that
8
+ * swallows a sidecar error should call this helper from inside the catch.
9
+ *
10
+ * The log message shape is stable across call sites so operators can
11
+ * grep for `sidecar <verb> failed` to surface every occurrence.
12
+ */
13
+ import type { ILogger } from '../../../../agent/core/interfaces/i-logger.js';
14
+ export declare function warnSidecarFailure(logger: ILogger | undefined, site: string, verb: string, target: string, error: unknown): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared helper for observing swallowed sidecar failures.
3
+ *
4
+ * Runtime-signals dual-write is best-effort: failures never break the
5
+ * caller's primary operation (markdown write, ranking read, etc.). After
6
+ * commit 5 the sidecar is the canonical source for ranking signals, so
7
+ * silent swallows hide real outages from operators. Every site that
8
+ * swallows a sidecar error should call this helper from inside the catch.
9
+ *
10
+ * The log message shape is stable across call sites so operators can
11
+ * grep for `sidecar <verb> failed` to surface every occurrence.
12
+ */
13
+ export function warnSidecarFailure(logger, site, verb, target, error) {
14
+ if (!logger)
15
+ return;
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ logger.warn(`${site}: sidecar ${verb} failed for ${target}: ${message}`);
18
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Sidecar store for per-machine ranking signals.
3
+ *
4
+ * Keeps `importance`, `recency`, `maturity`, `accessCount`, `updateCount`
5
+ * out of context-tree markdown frontmatter so that query-time bumps don't
6
+ * dirty version-controlled files or create merge conflicts across teammates.
7
+ *
8
+ * Backed by `IKeyStorage` with composite keys of the form
9
+ * `["signals", ...pathSegments]`. The relative path is split on `/` so each
10
+ * segment satisfies the key-storage validation rules.
11
+ *
12
+ * All paths are relative to the context tree root (e.g. `auth/jwt-refresh.md`)
13
+ * using forward slashes, matching how paths flow through the rest of the
14
+ * knowledge pipeline.
15
+ *
16
+ * ## Concurrency guarantees
17
+ *
18
+ * Atomicity applies **within a single process**. Two `update` calls on the
19
+ * same path in the same process serialize via the per-key RWLock inside
20
+ * `FileKeyStorage` — no lost updates.
21
+ *
22
+ * Across processes (daemon + CLI), the per-process locks do not coordinate,
23
+ * so there is a narrow lost-update window when both processes race a read-
24
+ * modify-write on the same entry. For ranking signals this is acceptable:
25
+ * losing one access-hit bump has no correctness impact, only a tiny
26
+ * ranking drift that the next session self-heals. Do **not** rely on
27
+ * this interface for data where consistency is required (e.g. identifiers,
28
+ * counters that must never skip).
29
+ *
30
+ * ## Invariants NOT enforced here
31
+ *
32
+ * The store accepts any `RuntimeSignals` record that satisfies the schema —
33
+ * it does not enforce semantic invariants such as the importance ↔ maturity
34
+ * hysteresis defined by `determineTier`. Callers bumping `importance` must
35
+ * recompute `maturity` themselves (typically via `determineTier`) as part
36
+ * of the same updater callback.
37
+ */
38
+ import type { RuntimeSignals } from '../../domain/knowledge/runtime-signals-schema.js';
39
+ /**
40
+ * Pure function that derives the next signals from the current signals.
41
+ * Called inside an atomic read-modify-write critical section.
42
+ */
43
+ export type RuntimeSignalsUpdater = (current: RuntimeSignals) => RuntimeSignals;
44
+ export interface IRuntimeSignalStore {
45
+ /**
46
+ * Apply an updater to many entries in parallel.
47
+ *
48
+ * Each entry is updated atomically via {@link update}; different paths run
49
+ * concurrently. Used by the access-hit flush path which accumulates bumps
50
+ * across many files between index rebuilds.
51
+ */
52
+ batchUpdate(updates: Map<string, RuntimeSignalsUpdater>): Promise<void>;
53
+ /**
54
+ * Remove an entry. No-op if the entry does not exist.
55
+ *
56
+ * Called when a file is archived or deleted so the sidecar does not retain
57
+ * orphan records.
58
+ */
59
+ delete(relPath: string): Promise<void>;
60
+ /**
61
+ * Read the signals for a path, returning defaults when no entry exists
62
+ * or when the stored record fails schema validation.
63
+ *
64
+ * Never throws and never returns null — callers can treat every path as
65
+ * having a signal record.
66
+ */
67
+ get(relPath: string): Promise<RuntimeSignals>;
68
+ /**
69
+ * Bulk-read signals for a known set of paths.
70
+ *
71
+ * Preferred over {@link list} for ranking passes that operate on the
72
+ * top-N search results: O(k) where k is the number of requested paths,
73
+ * instead of O(all stored entries).
74
+ *
75
+ * The returned map contains entries **only for paths that have a stored
76
+ * record**. Missing or corrupt records are omitted so callers can
77
+ * distinguish "no entry yet" from "entry with default values" via
78
+ * `.has(path)`. Use `map.get(path) ?? createDefaultRuntimeSignals()` when
79
+ * the caller wants defaults on miss.
80
+ */
81
+ getMany(relPaths: readonly string[]): Promise<Map<string, RuntimeSignals>>;
82
+ /**
83
+ * Snapshot every stored entry as a Map keyed by relative path.
84
+ *
85
+ * Intended for administrative passes (diagnostics, orphan pruning) rather
86
+ * than per-query ranking — use {@link getMany} for that.
87
+ */
88
+ list(): Promise<Map<string, RuntimeSignals>>;
89
+ /**
90
+ * Replace the signals for a path with the provided record.
91
+ *
92
+ * Used for seeding (curate ADD with defaults) and for operations that
93
+ * compute a full new record without needing the current value (merge,
94
+ * restore).
95
+ */
96
+ set(relPath: string, signals: RuntimeSignals): Promise<void>;
97
+ /**
98
+ * Atomically read, transform, and write the signals for a path.
99
+ *
100
+ * The updater receives the current signals (defaults if none are stored)
101
+ * and must return the complete replacement record. Runs inside the
102
+ * per-key lock provided by {@link IKeyStorage.update}, so concurrent
103
+ * callers on the same path within one process serialize cleanly — no
104
+ * lost updates. See the interface-level note about cross-process
105
+ * behaviour.
106
+ *
107
+ * Use this for any bump semantics that depend on the current value, e.g.
108
+ * `accessCount += hits` or `importance = min(100, current + bonus)`.
109
+ */
110
+ update(relPath: string, updater: RuntimeSignalsUpdater): Promise<RuntimeSignals>;
111
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Sidecar store for per-machine ranking signals.
3
+ *
4
+ * Keeps `importance`, `recency`, `maturity`, `accessCount`, `updateCount`
5
+ * out of context-tree markdown frontmatter so that query-time bumps don't
6
+ * dirty version-controlled files or create merge conflicts across teammates.
7
+ *
8
+ * Backed by `IKeyStorage` with composite keys of the form
9
+ * `["signals", ...pathSegments]`. The relative path is split on `/` so each
10
+ * segment satisfies the key-storage validation rules.
11
+ *
12
+ * All paths are relative to the context tree root (e.g. `auth/jwt-refresh.md`)
13
+ * using forward slashes, matching how paths flow through the rest of the
14
+ * knowledge pipeline.
15
+ *
16
+ * ## Concurrency guarantees
17
+ *
18
+ * Atomicity applies **within a single process**. Two `update` calls on the
19
+ * same path in the same process serialize via the per-key RWLock inside
20
+ * `FileKeyStorage` — no lost updates.
21
+ *
22
+ * Across processes (daemon + CLI), the per-process locks do not coordinate,
23
+ * so there is a narrow lost-update window when both processes race a read-
24
+ * modify-write on the same entry. For ranking signals this is acceptable:
25
+ * losing one access-hit bump has no correctness impact, only a tiny
26
+ * ranking drift that the next session self-heals. Do **not** rely on
27
+ * this interface for data where consistency is required (e.g. identifiers,
28
+ * counters that must never skip).
29
+ *
30
+ * ## Invariants NOT enforced here
31
+ *
32
+ * The store accepts any `RuntimeSignals` record that satisfies the schema —
33
+ * it does not enforce semantic invariants such as the importance ↔ maturity
34
+ * hysteresis defined by `determineTier`. Callers bumping `importance` must
35
+ * recompute `maturity` themselves (typically via `determineTier`) as part
36
+ * of the same updater callback.
37
+ */
38
+ export {};
@@ -11,25 +11,35 @@
11
11
  * Fail-open: any error during ghost cue generation falls back to deterministic truncation.
12
12
  */
13
13
  import type { ICipherAgent } from '../../../agent/core/interfaces/i-cipher-agent.js';
14
+ import type { ILogger } from '../../../agent/core/interfaces/i-logger.js';
14
15
  import type { ArchiveResult, DrillDownResult } from '../../core/domain/knowledge/summary-types.js';
15
16
  import type { IContextTreeArchiveService } from '../../core/interfaces/context-tree/i-context-tree-archive-service.js';
17
+ import type { IRuntimeSignalStore } from '../../core/interfaces/storage/i-runtime-signal-store.js';
16
18
  export declare class FileContextTreeArchiveService implements IContextTreeArchiveService {
19
+ private readonly runtimeSignalStore?;
20
+ private readonly logger?;
21
+ constructor(runtimeSignalStore?: IRuntimeSignalStore | undefined, logger?: ILogger | undefined);
17
22
  archiveEntry(relativePath: string, agent: ICipherAgent, directory?: string): Promise<ArchiveResult>;
18
23
  drillDown(stubPath: string, directory?: string): Promise<DrillDownResult>;
19
24
  findArchiveCandidates(directory?: string): Promise<string[]>;
20
25
  restoreEntry(stubPath: string, directory?: string): Promise<string>;
21
- /**
22
- * Extract importance score from frontmatter. Returns 50 if not found.
23
- */
24
- private extractImportance;
25
26
  /**
26
27
  * Generate a ghost cue using LLM with deterministic fallback.
27
28
  */
28
29
  private generateGhostCue;
29
30
  /**
30
- * Parse FrontmatterScoring fields from content frontmatter.
31
+ * Extract the `updatedAt` timestamp from markdown frontmatter. This is
32
+ * the one scoring-adjacent field that stays in markdown (it tracks real
33
+ * content modification, not a runtime signal).
34
+ */
35
+ private parseUpdatedAt;
36
+ /**
37
+ * Read the importance value to embed in an archive stub's eviction metadata.
38
+ * Pulls from the runtime-signal sidecar (source of truth post-commit-4),
39
+ * falling back to the default when no entry exists or the store is
40
+ * unavailable.
31
41
  */
32
- private parseScoring;
42
+ private readImportanceForArchiveMetadata;
33
43
  /**
34
44
  * Recursively scan context tree for archive candidates.
35
45
  */
@@ -15,11 +15,19 @@ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
15
15
  import { dirname, extname, join } from 'node:path';
16
16
  import { ARCHIVE_DIR, ARCHIVE_IMPORTANCE_THRESHOLD, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, DEFAULT_GHOST_CUE_MAX_TOKENS, FULL_ARCHIVE_EXTENSION, STUB_EXTENSION, } from '../../constants.js';
17
17
  import { applyDecay } from '../../core/domain/knowledge/memory-scoring.js';
18
+ import { createDefaultRuntimeSignals } from '../../core/domain/knowledge/runtime-signals-schema.js';
19
+ import { warnSidecarFailure } from '../../core/domain/knowledge/sidecar-logging.js';
18
20
  import { estimateTokens } from '../executor/pre-compaction/compaction-escalation.js';
19
21
  import { isArchiveStub, isDerivedArtifact } from './derived-artifact.js';
20
22
  import { toUnixPath } from './path-utils.js';
21
23
  import { generateArchiveStubContent, parseArchiveStubFrontmatter } from './summary-frontmatter.js';
22
24
  export class FileContextTreeArchiveService {
25
+ runtimeSignalStore;
26
+ logger;
27
+ constructor(runtimeSignalStore, logger) {
28
+ this.runtimeSignalStore = runtimeSignalStore;
29
+ this.logger = logger;
30
+ }
23
31
  async archiveEntry(relativePath, agent, directory) {
24
32
  const baseDir = directory ?? process.cwd();
25
33
  const contextTreeDir = join(baseDir, BRV_DIR, CONTEXT_TREE_DIR);
@@ -40,8 +48,11 @@ export class FileContextTreeArchiveService {
40
48
  // Generate ghost cue via LLM (fail-open to deterministic truncation)
41
49
  const ghostCue = await this.generateGhostCue(agent, content);
42
50
  const ghostCueTokenCount = estimateTokens(ghostCue);
43
- // Parse frontmatter to get importance for eviction metadata
44
- const importance = this.extractImportance(content);
51
+ // Capture current importance from the sidecar for the archive stub's
52
+ // eviction metadata. Falls back to the default when no sidecar entry
53
+ // exists (pre-migration files, or a sidecar that hasn't been written to
54
+ // for this path). Fail-open on sidecar errors.
55
+ const importance = await this.readImportanceForArchiveMetadata(toUnixPath(relativePath));
45
56
  // Write .stub.md with archive stub frontmatter
46
57
  const stubContent = generateArchiveStubContent({
47
58
  evicted_at: new Date().toISOString(),
@@ -54,6 +65,17 @@ export class FileContextTreeArchiveService {
54
65
  await writeFile(stubFullPath, stubContent, 'utf8');
55
66
  // Delete original file
56
67
  await unlink(originalFullPath);
68
+ // Dual-write: drop the archived file's runtime-signal entry so the
69
+ // sidecar does not retain an orphan. Fail-open — markdown is canonical.
70
+ if (this.runtimeSignalStore) {
71
+ try {
72
+ await this.runtimeSignalStore.delete(toUnixPath(relativePath));
73
+ }
74
+ catch (error) {
75
+ // Best-effort — archive already succeeded.
76
+ warnSidecarFailure(this.logger, 'archive-service', 'delete', relativePath, error);
77
+ }
78
+ }
57
79
  return {
58
80
  fullPath: toUnixPath(fullRelPath),
59
81
  ghostCueTokenCount,
@@ -83,8 +105,22 @@ export class FileContextTreeArchiveService {
83
105
  async findArchiveCandidates(directory) {
84
106
  const baseDir = directory ?? process.cwd();
85
107
  const contextTreeDir = join(baseDir, BRV_DIR, CONTEXT_TREE_DIR);
108
+ // Preload the runtime-signal map once per scan. `list()` returns only
109
+ // paths with stored entries; paths without one fall back to defaults at
110
+ // the comparison site. On sidecar failure, scan with an empty map —
111
+ // archive candidacy then depends on defaults only (importance 50), which
112
+ // keeps all draft entries above the threshold and archives nothing. That
113
+ // is the safest fallback when scoring data is unavailable.
114
+ let signalsByPath;
115
+ try {
116
+ signalsByPath = this.runtimeSignalStore ? await this.runtimeSignalStore.list() : new Map();
117
+ }
118
+ catch (error) {
119
+ warnSidecarFailure(this.logger, 'archive-service', 'list', 'candidate scan', error);
120
+ signalsByPath = new Map();
121
+ }
86
122
  const candidates = [];
87
- await this.scanForCandidates(contextTreeDir, contextTreeDir, candidates);
123
+ await this.scanForCandidates(contextTreeDir, contextTreeDir, candidates, signalsByPath);
88
124
  return candidates;
89
125
  }
90
126
  async restoreEntry(stubPath, directory) {
@@ -107,15 +143,20 @@ export class FileContextTreeArchiveService {
107
143
  // Delete stub and full archive files
108
144
  await unlink(stubFullPath);
109
145
  await unlink(fullPath);
146
+ // Dual-write: seed the restored file with default signals. Signal
147
+ // history from before archiving was already dropped on archive — restore
148
+ // is a user-initiated action, so resetting to defaults is acceptable.
149
+ if (this.runtimeSignalStore) {
150
+ try {
151
+ await this.runtimeSignalStore.set(toUnixPath(fm.original_path), createDefaultRuntimeSignals());
152
+ }
153
+ catch (error) {
154
+ // Best-effort — markdown restore already succeeded.
155
+ warnSidecarFailure(this.logger, 'archive-service', 'seed', fm.original_path, error);
156
+ }
157
+ }
110
158
  return fm.original_path;
111
159
  }
112
- /**
113
- * Extract importance score from frontmatter. Returns 50 if not found.
114
- */
115
- extractImportance(content) {
116
- const match = /^importance:\s*(\d+(?:\.\d+)?)/m.exec(content);
117
- return match ? Number.parseFloat(match[1]) : 50;
118
- }
119
160
  /**
120
161
  * Generate a ghost cue using LLM with deterministic fallback.
121
162
  */
@@ -154,25 +195,36 @@ ${content.slice(0, 8000)}
154
195
  return `${content.replaceAll(/\s+/g, ' ').trim().slice(0, 320)}...`;
155
196
  }
156
197
  /**
157
- * Parse FrontmatterScoring fields from content frontmatter.
198
+ * Extract the `updatedAt` timestamp from markdown frontmatter. This is
199
+ * the one scoring-adjacent field that stays in markdown (it tracks real
200
+ * content modification, not a runtime signal).
158
201
  */
159
- parseScoring(content) {
160
- const scoring = {};
161
- const importanceMatch = /^importance:\s*(\d+(?:\.\d+)?)/m.exec(content);
162
- if (importanceMatch)
163
- scoring.importance = Number.parseFloat(importanceMatch[1]);
164
- const maturityMatch = /^maturity:\s*['"]?(core|draft|validated)['"]?/m.exec(content);
165
- if (maturityMatch)
166
- scoring.maturity = maturityMatch[1];
167
- const updatedMatch = /^updatedAt:\s*['"]?(.+?)['"]?\s*$/m.exec(content);
168
- if (updatedMatch)
169
- scoring.updatedAt = updatedMatch[1];
170
- return scoring;
202
+ parseUpdatedAt(content) {
203
+ const match = /^updatedAt:\s*['"]?(.+?)['"]?\s*$/m.exec(content);
204
+ return match ? match[1] : undefined;
205
+ }
206
+ /**
207
+ * Read the importance value to embed in an archive stub's eviction metadata.
208
+ * Pulls from the runtime-signal sidecar (source of truth post-commit-4),
209
+ * falling back to the default when no entry exists or the store is
210
+ * unavailable.
211
+ */
212
+ async readImportanceForArchiveMetadata(relativePath) {
213
+ if (!this.runtimeSignalStore)
214
+ return createDefaultRuntimeSignals().importance;
215
+ try {
216
+ const signals = await this.runtimeSignalStore.get(relativePath);
217
+ return signals.importance;
218
+ }
219
+ catch (error) {
220
+ warnSidecarFailure(this.logger, 'archive-service', 'get', `${relativePath} (archive metadata read)`, error);
221
+ return createDefaultRuntimeSignals().importance;
222
+ }
171
223
  }
172
224
  /**
173
225
  * Recursively scan context tree for archive candidates.
174
226
  */
175
- async scanForCandidates(currentDir, contextTreeDir, candidates) {
227
+ async scanForCandidates(currentDir, contextTreeDir, candidates, signalsByPath) {
176
228
  const { readdir: readdirFs } = await import('node:fs/promises');
177
229
  let entries;
178
230
  try {
@@ -189,23 +241,30 @@ ${content.slice(0, 8000)}
189
241
  if (entry.isDirectory()) {
190
242
  if (entryName === ARCHIVE_DIR)
191
243
  continue;
192
- await this.scanForCandidates(fullPath, contextTreeDir, candidates);
244
+ await this.scanForCandidates(fullPath, contextTreeDir, candidates, signalsByPath);
193
245
  }
194
246
  else if (entry.isFile() && entryName.endsWith(CONTEXT_FILE_EXTENSION)) {
195
247
  const relativePath = toUnixPath(fullPath.slice(contextTreeDir.length + 1));
196
248
  if (isDerivedArtifact(relativePath) || isArchiveStub(relativePath))
197
249
  continue;
198
250
  try {
199
- const content = await readFile(fullPath, 'utf8');
200
- const scoring = this.parseScoring(content);
251
+ // Runtime signals come from the sidecar; `updatedAt` stays in
252
+ // markdown because it reflects content modification time, not a
253
+ // ranking signal. Paths without a sidecar entry use defaults —
254
+ // maturity 'draft' passes the gate, importance 50 stays above
255
+ // ARCHIVE_IMPORTANCE_THRESHOLD (which is < 50), so files without
256
+ // recorded signals are correctly excluded from archival.
257
+ const signals = signalsByPath.get(relativePath) ?? createDefaultRuntimeSignals();
201
258
  // Only archive draft entries below importance threshold
202
- if (scoring.maturity !== 'draft')
259
+ if (signals.maturity !== 'draft')
203
260
  continue;
204
- const daysSinceUpdate = scoring.updatedAt
205
- ? (now - new Date(scoring.updatedAt).getTime()) / (1000 * 60 * 60 * 24)
261
+ const content = await readFile(fullPath, 'utf8');
262
+ const updatedAt = this.parseUpdatedAt(content);
263
+ const daysSinceUpdate = updatedAt
264
+ ? (now - new Date(updatedAt).getTime()) / (1000 * 60 * 60 * 24)
206
265
  : 0;
207
- const decayed = applyDecay(scoring, daysSinceUpdate);
208
- if ((decayed.importance ?? 50) < ARCHIVE_IMPORTANCE_THRESHOLD) {
266
+ const decayed = applyDecay(signals, daysSinceUpdate);
267
+ if (decayed.importance < ARCHIVE_IMPORTANCE_THRESHOLD) {
209
268
  candidates.push(relativePath);
210
269
  }
211
270
  }
@@ -12,10 +12,24 @@
12
12
  * are preserved. Acceptable because writeFile() updates mtime and the next
13
13
  * curate run will rebuild the manifest anyway.
14
14
  */
15
+ import type { ILogger } from '../../../agent/core/interfaces/i-logger.js';
15
16
  import type { ContextManifest, LaneTokens, ResolvedEntry } from '../../core/domain/knowledge/summary-types.js';
16
17
  import type { IContextTreeManifestService } from '../../core/interfaces/context-tree/i-context-tree-manifest-service.js';
18
+ import type { IRuntimeSignalStore } from '../../core/interfaces/storage/i-runtime-signal-store.js';
17
19
  export interface ManifestServiceConfig {
18
20
  baseDirectory?: string;
21
+ /**
22
+ * Optional logger. When provided, sidecar list failures during manifest
23
+ * build emit a warn so the fail-open degradation is visible.
24
+ */
25
+ logger?: ILogger;
26
+ /**
27
+ * Optional. Source of truth for per-context `importance` used in lane
28
+ * allocation. When absent or when a path has no entry, the default
29
+ * importance (50) is used — same effective sort as the pre-migration
30
+ * fallback of `scoring?.importance ?? 50`.
31
+ */
32
+ runtimeSignalStore?: IRuntimeSignalStore;
19
33
  }
20
34
  export declare class FileContextTreeManifestService implements IContextTreeManifestService {
21
35
  private readonly config;
@@ -16,7 +16,7 @@
16
16
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
17
17
  import { join, relative } from 'node:path';
18
18
  import { ABSTRACT_EXTENSION, ARCHIVE_DIR, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE, } from '../../constants.js';
19
- import { parseFrontmatterScoring } from '../../core/domain/knowledge/markdown-writer.js';
19
+ import { warnSidecarFailure } from '../../core/domain/knowledge/sidecar-logging.js';
20
20
  import { DEFAULT_LANE_BUDGETS } from '../../core/domain/knowledge/summary-types.js';
21
21
  import { estimateTokens } from '../executor/pre-compaction/compaction-escalation.js';
22
22
  import { isArchiveStub, isDerivedArtifact } from './derived-artifact.js';
@@ -32,11 +32,22 @@ export class FileContextTreeManifestService {
32
32
  const baseDir = directory ?? this.config.baseDirectory ?? process.cwd();
33
33
  const contextTreeDir = join(baseDir, BRV_DIR, CONTEXT_TREE_DIR);
34
34
  const budgets = laneBudgets ?? DEFAULT_LANE_BUDGETS;
35
+ // Preload sidecar signals once — used to read `importance` per context.
36
+ // Fail-open: on sidecar error we treat every path as having no entry,
37
+ // which falls back to default importance (50) at the read site.
38
+ let signalsByPath;
39
+ try {
40
+ signalsByPath = this.config.runtimeSignalStore ? await this.config.runtimeSignalStore.list() : new Map();
41
+ }
42
+ catch (error) {
43
+ warnSidecarFailure(this.config.logger, 'manifest-service', 'list', 'buildManifest', error);
44
+ signalsByPath = new Map();
45
+ }
35
46
  // Scan all entries
36
47
  const summaries = [];
37
48
  const contexts = [];
38
49
  const stubs = [];
39
- await this.scanForManifest(contextTreeDir, contextTreeDir, summaries, contexts, stubs);
50
+ await this.scanForManifest(contextTreeDir, contextTreeDir, summaries, contexts, stubs, signalsByPath);
40
51
  // Lane allocation with prioritized fill
41
52
  const activeSummaries = this.allocateLane(summaries.sort((a, b) => (b.order ?? 0) - (a.order ?? 0)), budgets.summaries);
42
53
  const activeContexts = this.allocateLane(contexts.sort((a, b) => (b.importance ?? 50) - (a.importance ?? 50)), budgets.contexts);
@@ -195,7 +206,7 @@ export class FileContextTreeManifestService {
195
206
  /**
196
207
  * Recursively scan context tree, collecting entries for manifest building.
197
208
  */
198
- async scanForManifest(currentDir, contextTreeDir, summaries, contexts, stubs) {
209
+ async scanForManifest(currentDir, contextTreeDir, summaries, contexts, stubs, signalsByPath) {
199
210
  let entries;
200
211
  try {
201
212
  entries = await readdir(currentDir, { withFileTypes: true });
@@ -217,7 +228,7 @@ export class FileContextTreeManifestService {
217
228
  // Scan _archived/ for .stub.md files only; recurse otherwise
218
229
  await (entryName === ARCHIVE_DIR
219
230
  ? this.scanArchivedStubs(fullPath, contextTreeDir, stubs)
220
- : this.scanForManifest(fullPath, contextTreeDir, summaries, contexts, stubs));
231
+ : this.scanForManifest(fullPath, contextTreeDir, summaries, contexts, stubs, signalsByPath));
221
232
  }
222
233
  else if (entry.isFile() && entryName.endsWith(CONTEXT_FILE_EXTENSION)) {
223
234
  const relativePath = toUnixPath(relative(contextTreeDir, fullPath));
@@ -238,10 +249,12 @@ export class FileContextTreeManifestService {
238
249
  }
239
250
  }
240
251
  else if (!isDerivedArtifact(relativePath) && !isArchiveStub(relativePath)) {
241
- // Regular context entry — extract importance from frontmatter
252
+ // Regular context entry — importance comes from the sidecar, not
253
+ // markdown frontmatter. Paths without a sidecar entry fall back to
254
+ // default importance (50), matching the prior `?? 50` behaviour.
242
255
  try {
243
256
  const content = await readFile(fullPath, 'utf8');
244
- const scoring = parseFrontmatterScoring(content);
257
+ const importance = signalsByPath.get(relativePath)?.importance ?? 50;
245
258
  // Use abstract sibling for token budgeting only if it is known to exist
246
259
  // (checked via abstractsInDir set, avoiding ENOENT as control flow).
247
260
  const abstractRelPath = relativePath.replace(/\.md$/, ABSTRACT_EXTENSION);
@@ -257,7 +270,7 @@ export class FileContextTreeManifestService {
257
270
  contexts.push({
258
271
  abstractPath: abstractTokens === undefined ? undefined : abstractRelPath,
259
272
  abstractTokens,
260
- importance: scoring?.importance ?? 50,
273
+ importance,
261
274
  path: relativePath,
262
275
  tokens: abstractTokens ?? estimateTokens(content),
263
276
  type: 'context',