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
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ No Node.js required - everything is bundled.
|
|
|
71
71
|
curl -fsSL https://byterover.dev/install.sh | sh
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Supported platforms: macOS ARM64, Linux x64, Linux ARM64.
|
|
74
|
+
Supported platforms: macOS ARM64, macOS x64 (Intel), Linux x64, Linux ARM64.
|
|
75
75
|
|
|
76
76
|
### npm (All Platforms)
|
|
77
77
|
|
|
@@ -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
|
-
|
|
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.
|
|
143
|
+
// 6b. Storage layer — initialised 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
223
|
+
// 11. Tool scheduler (orchestrates policy check → execution)
|
|
203
224
|
const toolScheduler = new CoreToolScheduler(toolProvider, policyEngine, undefined, {
|
|
204
225
|
verbose,
|
|
205
226
|
});
|
|
206
|
-
//
|
|
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,
|
|
6
|
-
import {
|
|
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
|
|
690
|
+
// Read existing file to detect structural loss
|
|
610
691
|
const existingContent = await DirectoryManager.readFile(contextPath);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
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
|
/**
|