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.
- package/README.md +1 -1
- package/dist/agent/core/interfaces/cipher-services.d.ts +7 -0
- package/dist/agent/infra/agent/cipher-agent.js +4 -2
- package/dist/agent/infra/agent/service-initializer.js +28 -14
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +6 -4
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +4 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +169 -25
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +2 -0
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +1 -1
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +0 -8
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.js +3 -15
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +49 -4
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +123 -53
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.d.ts +2 -0
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +7 -0
- package/dist/agent/infra/tools/tool-registry.js +13 -6
- package/dist/oclif/commands/dream.d.ts +4 -0
- package/dist/oclif/commands/dream.js +31 -13
- package/dist/server/constants.d.ts +1 -1
- package/dist/server/constants.js +20 -1
- package/dist/server/core/domain/knowledge/markdown-writer.d.ts +12 -42
- package/dist/server/core/domain/knowledge/markdown-writer.js +55 -96
- package/dist/server/core/domain/knowledge/memory-scoring.d.ts +18 -37
- package/dist/server/core/domain/knowledge/memory-scoring.js +36 -85
- package/dist/server/core/domain/knowledge/runtime-signals-schema.d.ts +59 -0
- package/dist/server/core/domain/knowledge/runtime-signals-schema.js +46 -0
- package/dist/server/core/domain/knowledge/sidecar-logging.d.ts +14 -0
- package/dist/server/core/domain/knowledge/sidecar-logging.js +18 -0
- package/dist/server/core/interfaces/storage/i-runtime-signal-store.d.ts +111 -0
- package/dist/server/core/interfaces/storage/i-runtime-signal-store.js +38 -0
- package/dist/server/infra/context-tree/file-context-tree-archive-service.d.ts +16 -6
- package/dist/server/infra/context-tree/file-context-tree-archive-service.js +91 -32
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +14 -0
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +20 -7
- package/dist/server/infra/context-tree/runtime-signal-store.d.ts +46 -0
- package/dist/server/infra/context-tree/runtime-signal-store.js +118 -0
- package/dist/server/infra/daemon/agent-process.js +25 -4
- package/dist/server/infra/dream/operations/consolidate.d.ts +17 -0
- package/dist/server/infra/dream/operations/consolidate.js +40 -19
- package/dist/server/infra/dream/operations/prune.d.ts +18 -0
- package/dist/server/infra/dream/operations/prune.js +31 -20
- package/dist/server/infra/dream/operations/synthesize.d.ts +13 -0
- package/dist/server/infra/dream/operations/synthesize.js +15 -3
- package/dist/server/infra/executor/dream-executor.d.ts +8 -0
- package/dist/server/infra/executor/dream-executor.js +3 -0
- package/dist/server/templates/skill/SKILL.md +79 -22
- package/oclif.manifest.json +429 -429
- 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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
44
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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 (
|
|
259
|
+
if (signals.maturity !== 'draft')
|
|
203
260
|
continue;
|
|
204
|
-
const
|
|
205
|
-
|
|
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(
|
|
208
|
-
if (
|
|
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 {
|
|
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 —
|
|
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
|
|
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
|
|
273
|
+
importance,
|
|
261
274
|
path: relativePath,
|
|
262
275
|
tokens: abstractTokens ?? estimateTokens(content),
|
|
263
276
|
type: 'context',
|