byterover-cli 3.0.1 → 3.2.0
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/.env.production +4 -0
- package/README.md +17 -0
- package/dist/agent/core/domain/tools/constants.d.ts +1 -0
- package/dist/agent/core/domain/tools/constants.js +1 -0
- package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error-codes.js +0 -1
- package/dist/agent/infra/agent/agent-error.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error.js +0 -1
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
- package/dist/agent/infra/agent/agent-state-manager.js +1 -3
- package/dist/agent/infra/agent/base-agent.d.ts +1 -1
- package/dist/agent/infra/agent/base-agent.js +1 -1
- package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
- package/dist/agent/infra/agent/cipher-agent.js +188 -3
- package/dist/agent/infra/agent/index.d.ts +1 -1
- package/dist/agent/infra/agent/index.js +1 -1
- package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
- package/dist/agent/infra/agent/service-initializer.js +14 -8
- package/dist/agent/infra/agent/types.d.ts +0 -1
- package/dist/agent/infra/file-system/file-system-service.js +6 -5
- package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
- package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
- package/dist/agent/infra/llm/providers/openai.js +12 -0
- package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
- package/dist/agent/infra/llm/stream-to-text.js +14 -0
- package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
- package/dist/agent/infra/map/abstract-generator.js +67 -0
- package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
- package/dist/agent/infra/map/abstract-queue.js +218 -0
- package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
- package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
- package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
- package/dist/agent/infra/memory/memory-manager.js +6 -5
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +20 -7
- package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
- package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/session/session-compressor.d.ts +43 -0
- package/dist/agent/infra/session/session-compressor.js +296 -0
- package/dist/agent/infra/session/session-manager.d.ts +7 -0
- package/dist/agent/infra/session/session-manager.js +9 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +16 -5
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.js +4 -3
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.js +4 -3
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +41 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -3
- package/dist/server/constants.d.ts +8 -0
- package/dist/server/constants.js +10 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
- package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
- package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
- package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +18 -10
- package/dist/server/core/domain/transport/schemas.js +4 -0
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- package/dist/server/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/context-tree/derived-artifact.js +5 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
- package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
- package/dist/server/infra/daemon/agent-process.js +15 -5
- package/dist/server/infra/executor/curate-executor.js +6 -3
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +88 -7
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +125 -23
- package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +65 -13
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/utils/curate-result-parser.d.ts +4 -4
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +26 -0
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +23 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/oclif.manifest.json +261 -1
- package/package.json +10 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IContentGenerator } from '../../core/interfaces/i-content-generator.js';
|
|
2
|
+
import type { InternalMessage } from '../../core/interfaces/message-types.js';
|
|
3
|
+
import type { MemoryDeduplicator } from '../memory/memory-deduplicator.js';
|
|
4
|
+
import type { MemoryManager } from '../memory/memory-manager.js';
|
|
5
|
+
/**
|
|
6
|
+
* Result of a session compression pass.
|
|
7
|
+
*/
|
|
8
|
+
export interface CompressionResult {
|
|
9
|
+
created: number;
|
|
10
|
+
merged: number;
|
|
11
|
+
skipped: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extracts and persists memories from a completed task session.
|
|
15
|
+
*
|
|
16
|
+
* Flow:
|
|
17
|
+
* 1. Serialize session messages into a text digest
|
|
18
|
+
* 2. LLM call: extract 5-category draft memories
|
|
19
|
+
* 3. Load existing agent memories for deduplication
|
|
20
|
+
* 4. Apply deduplication decisions (CREATE/MERGE/SKIP)
|
|
21
|
+
*/
|
|
22
|
+
export declare class SessionCompressor {
|
|
23
|
+
private readonly deduplicator;
|
|
24
|
+
private readonly generator;
|
|
25
|
+
private readonly memoryManager;
|
|
26
|
+
constructor(deduplicator: MemoryDeduplicator, generator: IContentGenerator, memoryManager: MemoryManager);
|
|
27
|
+
/**
|
|
28
|
+
* Compress a session into persistent memories.
|
|
29
|
+
*
|
|
30
|
+
* @param messages - Session message history
|
|
31
|
+
* @param commandType - Session command type (e.g. 'curate', 'query')
|
|
32
|
+
* @param options - Compression options
|
|
33
|
+
* @param options.minMessages - Minimum message count required before compression runs
|
|
34
|
+
* @returns Summary of actions taken
|
|
35
|
+
*/
|
|
36
|
+
compress(messages: InternalMessage[], commandType: string, options?: {
|
|
37
|
+
minMessages?: number;
|
|
38
|
+
}): Promise<CompressionResult>;
|
|
39
|
+
private buildFallbackDrafts;
|
|
40
|
+
private deduplicateFallbackDrafts;
|
|
41
|
+
private extractDrafts;
|
|
42
|
+
private serializeMessages;
|
|
43
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { streamToText } from '../llm/stream-to-text.js';
|
|
3
|
+
/**
|
|
4
|
+
* Five extraction categories for ByteRover session memories.
|
|
5
|
+
*/
|
|
6
|
+
const CATEGORIES = ['DECISIONS', 'ENTITIES', 'PATTERNS', 'PREFERENCES', 'SKILLS'];
|
|
7
|
+
const SYSTEM_PROMPT = `You are a session memory extractor for ByteRover, a code intelligence tool.
|
|
8
|
+
Extract reusable memories from the conversation in exactly these 5 categories:
|
|
9
|
+
- PATTERNS: reusable code or workflow patterns discovered
|
|
10
|
+
- PREFERENCES: user style/naming/structure decisions
|
|
11
|
+
- ENTITIES: key files, modules, APIs, dependencies discovered
|
|
12
|
+
- DECISIONS: architectural choices (always extract, even if already known — immutable log)
|
|
13
|
+
- SKILLS: tool invocation recipes that worked
|
|
14
|
+
|
|
15
|
+
Return ONLY a JSON array of memory objects:
|
|
16
|
+
[{"category": "PATTERNS", "content": "...", "tags": ["optional"]}, ...]
|
|
17
|
+
|
|
18
|
+
Extract 0-3 memories per category. Skip categories with nothing new. Be concise (max 200 chars per memory).`;
|
|
19
|
+
const MAX_DIGEST_CHARS = 12_000;
|
|
20
|
+
const FALLBACK_DIGEST_PREVIEW_CHARS = 4000;
|
|
21
|
+
const MIN_BOUNDARY_RATIO = 0.6;
|
|
22
|
+
const SOURCE_PATH_PATTERN = /\b(?:src|app|lib|packages|docs|test|tests)\/[A-Za-z0-9_./-]+\b/g;
|
|
23
|
+
function truncateDigestAtBoundary(digest, maxChars = MAX_DIGEST_CHARS) {
|
|
24
|
+
if (digest.length <= maxChars) {
|
|
25
|
+
return digest;
|
|
26
|
+
}
|
|
27
|
+
const clipped = digest.slice(0, maxChars);
|
|
28
|
+
const boundary = clipped.lastIndexOf('\n\n');
|
|
29
|
+
// Prefer a natural message boundary when it falls reasonably close to the cap.
|
|
30
|
+
return boundary >= Math.floor(maxChars * MIN_BOUNDARY_RATIO) ? clipped.slice(0, boundary) : clipped;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts and persists memories from a completed task session.
|
|
34
|
+
*
|
|
35
|
+
* Flow:
|
|
36
|
+
* 1. Serialize session messages into a text digest
|
|
37
|
+
* 2. LLM call: extract 5-category draft memories
|
|
38
|
+
* 3. Load existing agent memories for deduplication
|
|
39
|
+
* 4. Apply deduplication decisions (CREATE/MERGE/SKIP)
|
|
40
|
+
*/
|
|
41
|
+
export class SessionCompressor {
|
|
42
|
+
deduplicator;
|
|
43
|
+
generator;
|
|
44
|
+
memoryManager;
|
|
45
|
+
constructor(deduplicator, generator, memoryManager) {
|
|
46
|
+
this.deduplicator = deduplicator;
|
|
47
|
+
this.generator = generator;
|
|
48
|
+
this.memoryManager = memoryManager;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compress a session into persistent memories.
|
|
52
|
+
*
|
|
53
|
+
* @param messages - Session message history
|
|
54
|
+
* @param commandType - Session command type (e.g. 'curate', 'query')
|
|
55
|
+
* @param options - Compression options
|
|
56
|
+
* @param options.minMessages - Minimum message count required before compression runs
|
|
57
|
+
* @returns Summary of actions taken
|
|
58
|
+
*/
|
|
59
|
+
async compress(messages, commandType, options) {
|
|
60
|
+
const minMessages = options?.minMessages ?? 4;
|
|
61
|
+
const hasAssistantContent = messages.some((message) => (message.role === 'assistant' &&
|
|
62
|
+
getMessageText(message).trim().length > 0));
|
|
63
|
+
const effectiveMinMessages = commandType.startsWith('curate') && hasAssistantContent
|
|
64
|
+
? Math.min(minMessages, 1)
|
|
65
|
+
: minMessages;
|
|
66
|
+
if (messages.length < effectiveMinMessages) {
|
|
67
|
+
return { created: 0, merged: 0, skipped: 0 };
|
|
68
|
+
}
|
|
69
|
+
const digest = this.serializeMessages(messages);
|
|
70
|
+
if (!digest.trim()) {
|
|
71
|
+
return { created: 0, merged: 0, skipped: 0 };
|
|
72
|
+
}
|
|
73
|
+
// Step 1: Extract draft memories via LLM
|
|
74
|
+
const useFallbackDraftsFirst = shouldPreferFallbackDrafts(commandType);
|
|
75
|
+
let drafts = useFallbackDraftsFirst ? this.buildFallbackDrafts(digest, commandType) : await this.extractDrafts(digest, commandType);
|
|
76
|
+
let usedFallbackDrafts = useFallbackDraftsFirst && drafts.length > 0;
|
|
77
|
+
if (drafts.length === 0) {
|
|
78
|
+
drafts = this.buildFallbackDrafts(digest, commandType);
|
|
79
|
+
usedFallbackDrafts = drafts.length > 0;
|
|
80
|
+
}
|
|
81
|
+
if (drafts.length === 0) {
|
|
82
|
+
return { created: 0, merged: 0, skipped: 0 };
|
|
83
|
+
}
|
|
84
|
+
// Step 2: Load the most recently updated agent memories for deduplication.
|
|
85
|
+
// MemoryManager.list() sorts by updatedAt DESC before applying the limit.
|
|
86
|
+
const existing = await this.memoryManager.list({ limit: 60, source: 'agent' });
|
|
87
|
+
// Step 3: Deduplicate
|
|
88
|
+
const actions = usedFallbackDrafts
|
|
89
|
+
? this.deduplicateFallbackDrafts(drafts, existing)
|
|
90
|
+
: await this.deduplicator.deduplicate(drafts, existing);
|
|
91
|
+
// Step 4: Apply decisions
|
|
92
|
+
let created = 0;
|
|
93
|
+
let merged = 0;
|
|
94
|
+
let skipped = 0;
|
|
95
|
+
/* eslint-disable no-await-in-loop */
|
|
96
|
+
for (const action of actions) {
|
|
97
|
+
try {
|
|
98
|
+
if (action.action === 'CREATE') {
|
|
99
|
+
await this.memoryManager.create({
|
|
100
|
+
content: action.memory.content,
|
|
101
|
+
metadata: { category: action.memory.category, source: 'agent' },
|
|
102
|
+
tags: action.memory.tags,
|
|
103
|
+
});
|
|
104
|
+
created++;
|
|
105
|
+
}
|
|
106
|
+
else if (action.action === 'MERGE') {
|
|
107
|
+
await this.memoryManager.update(action.targetId, { content: action.mergedContent });
|
|
108
|
+
merged++;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
skipped++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// Fail-open: skip individual memory errors
|
|
116
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
117
|
+
console.debug(`[SessionCompressor] Failed to apply ${action.action} action: ${msg}`);
|
|
118
|
+
skipped++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/* eslint-enable no-await-in-loop */
|
|
122
|
+
return { created, merged, skipped };
|
|
123
|
+
}
|
|
124
|
+
buildFallbackDrafts(digest, commandType) {
|
|
125
|
+
if (!commandType.startsWith('curate')) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const preview = digest.slice(0, FALLBACK_DIGEST_PREVIEW_CHARS);
|
|
129
|
+
const fingerprint = computeFingerprint(preview);
|
|
130
|
+
const sourcePaths = [...new Set((preview.match(SOURCE_PATH_PATTERN) ?? []).filter((path) => !path.startsWith('.brv/')))];
|
|
131
|
+
const moduleLabel = deriveModuleLabel(sourcePaths);
|
|
132
|
+
const tags = moduleLabel === 'the working module' ? undefined : [moduleLabel];
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
category: 'DECISIONS',
|
|
136
|
+
content: `Session ${fingerprint}: curated ${moduleLabel} knowledge into the context tree.`,
|
|
137
|
+
tags,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
category: 'DECISIONS',
|
|
141
|
+
content: `Session ${fingerprint}: preserved ${moduleLabel} findings as durable knowledge instead of chat-only context.`,
|
|
142
|
+
tags,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
category: 'PATTERNS',
|
|
146
|
+
content: `Session ${fingerprint}: used recon -> extraction -> curate apply workflow for ${moduleLabel}.`,
|
|
147
|
+
tags,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
category: 'PATTERNS',
|
|
151
|
+
content: `Session ${fingerprint}: separated durable notes from raw source snippets while curating ${moduleLabel}.`,
|
|
152
|
+
tags,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
category: 'SKILLS',
|
|
156
|
+
content: `Session ${fingerprint}: start ${commandType} with tools.curation.recon, then mapExtract, then verify applied file paths.`,
|
|
157
|
+
tags,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
category: 'ENTITIES',
|
|
161
|
+
content: `${moduleLabel} is an actively curated module surfaced during ${commandType}.`,
|
|
162
|
+
tags,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
deduplicateFallbackDrafts(drafts, existing) {
|
|
167
|
+
const dedupCategories = ['ENTITIES', 'PATTERNS', 'SKILLS'];
|
|
168
|
+
const existingKeys = new Map();
|
|
169
|
+
for (const category of dedupCategories) {
|
|
170
|
+
existingKeys.set(category, new Set(existing
|
|
171
|
+
.filter((memory) => getMemoryCategory(memory) === category)
|
|
172
|
+
.map((memory) => normalizeForFallbackDedup(memory.content, category))));
|
|
173
|
+
}
|
|
174
|
+
return drafts.map((memory) => {
|
|
175
|
+
// DECISIONS always CREATE — temporal audit records that should accumulate
|
|
176
|
+
if (memory.category === 'DECISIONS') {
|
|
177
|
+
return { action: 'CREATE', memory };
|
|
178
|
+
}
|
|
179
|
+
const categorySet = existingKeys.get(memory.category);
|
|
180
|
+
if (!categorySet) {
|
|
181
|
+
return { action: 'CREATE', memory };
|
|
182
|
+
}
|
|
183
|
+
const key = normalizeForFallbackDedup(memory.content, memory.category);
|
|
184
|
+
if (categorySet.has(key)) {
|
|
185
|
+
return { action: 'SKIP', memory };
|
|
186
|
+
}
|
|
187
|
+
categorySet.add(key);
|
|
188
|
+
return { action: 'CREATE', memory };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async extractDrafts(digest, commandType) {
|
|
192
|
+
try {
|
|
193
|
+
const truncatedDigest = truncateDigestAtBoundary(digest);
|
|
194
|
+
const prompt = `## Session Type: ${commandType}
|
|
195
|
+
|
|
196
|
+
## Conversation
|
|
197
|
+
${truncatedDigest}
|
|
198
|
+
|
|
199
|
+
Extract reusable memories from this session.`;
|
|
200
|
+
// Use streaming — ChatGPT OAuth Codex endpoint requires stream: true
|
|
201
|
+
const responseText = await streamToText(this.generator, {
|
|
202
|
+
config: { maxTokens: 1000, temperature: 0 },
|
|
203
|
+
contents: [{ content: prompt, role: 'user' }],
|
|
204
|
+
model: 'default',
|
|
205
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
206
|
+
taskId: randomUUID(),
|
|
207
|
+
});
|
|
208
|
+
// Strip markdown code fences — some providers wrap JSON in ```json ... ```
|
|
209
|
+
const jsonText = responseText.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
210
|
+
const parsed = JSON.parse(jsonText);
|
|
211
|
+
if (!Array.isArray(parsed))
|
|
212
|
+
return [];
|
|
213
|
+
return parsed
|
|
214
|
+
.filter((item) => CATEGORIES.includes(item.category) && item.content?.trim())
|
|
215
|
+
.map((item) => ({
|
|
216
|
+
category: item.category,
|
|
217
|
+
content: item.content.trim(),
|
|
218
|
+
tags: item.tags,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
223
|
+
console.debug(`[SessionCompressor] Failed to extract drafts (${commandType}): ${msg}`);
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
serializeMessages(messages) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
for (const msg of messages) {
|
|
230
|
+
const role = msg.role.toUpperCase();
|
|
231
|
+
const text = getMessageText(msg);
|
|
232
|
+
if (text.trim()) {
|
|
233
|
+
lines.push(`[${role}]: ${text.slice(0, 2000)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return lines.join('\n\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function getMessageText(message) {
|
|
240
|
+
if (typeof message.content === 'string') {
|
|
241
|
+
return message.content;
|
|
242
|
+
}
|
|
243
|
+
if (!Array.isArray(message.content)) {
|
|
244
|
+
return '';
|
|
245
|
+
}
|
|
246
|
+
return message.content
|
|
247
|
+
.filter((part) => 'text' in part && typeof part.text === 'string')
|
|
248
|
+
.map((part) => part.text)
|
|
249
|
+
.join(' ');
|
|
250
|
+
}
|
|
251
|
+
function computeFingerprint(text) {
|
|
252
|
+
/* eslint-disable no-bitwise, unicorn/prefer-code-point */
|
|
253
|
+
let hash = 0;
|
|
254
|
+
for (const char of text) {
|
|
255
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
256
|
+
}
|
|
257
|
+
/* eslint-enable no-bitwise, unicorn/prefer-code-point */
|
|
258
|
+
return hash.toString(16).padStart(8, '0').slice(0, 8);
|
|
259
|
+
}
|
|
260
|
+
function deriveModuleLabel(sourcePaths) {
|
|
261
|
+
if (sourcePaths.length === 0) {
|
|
262
|
+
return 'the working module';
|
|
263
|
+
}
|
|
264
|
+
const firstPath = sourcePaths[0];
|
|
265
|
+
const segments = firstPath.split('/').filter(Boolean);
|
|
266
|
+
if (segments.length >= 2) {
|
|
267
|
+
return `${segments[0]}/${segments[1]}`;
|
|
268
|
+
}
|
|
269
|
+
return firstPath;
|
|
270
|
+
}
|
|
271
|
+
function normalizeMemoryContent(content) {
|
|
272
|
+
return content.trim().toLowerCase().replaceAll(/\s+/g, ' ');
|
|
273
|
+
}
|
|
274
|
+
function normalizeForFallbackDedup(content, category) {
|
|
275
|
+
let normalized = normalizeMemoryContent(content);
|
|
276
|
+
// PATTERNS and SKILLS are session-fingerprinted ("Session abc123: ...").
|
|
277
|
+
// Strip the prefix so repeated curate sessions on the same module are detected as duplicates.
|
|
278
|
+
if (category === 'PATTERNS' || category === 'SKILLS') {
|
|
279
|
+
normalized = normalized.replace(/^session\s+\S+:\s*/, '');
|
|
280
|
+
}
|
|
281
|
+
return normalized;
|
|
282
|
+
}
|
|
283
|
+
function getMemoryCategory(memory) {
|
|
284
|
+
if (!memory.metadata || typeof memory.metadata !== 'object') {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const { category } = memory.metadata;
|
|
288
|
+
return typeof category === 'string' ? category : undefined;
|
|
289
|
+
}
|
|
290
|
+
// Curate sessions always use deterministic fallback drafts instead of LLM extraction.
|
|
291
|
+
// Fallback drafts are faster (no LLM call), cheaper, and produce consistent categorized
|
|
292
|
+
// memories that can be deduped via string matching. LLM extraction is reserved for
|
|
293
|
+
// non-curate sessions (e.g., query) where conversation content is unpredictable.
|
|
294
|
+
function shouldPreferFallbackDrafts(commandType) {
|
|
295
|
+
return commandType.startsWith('curate');
|
|
296
|
+
}
|
|
@@ -160,6 +160,13 @@ export declare class SessionManager {
|
|
|
160
160
|
* @returns Session instance or undefined if not found
|
|
161
161
|
*/
|
|
162
162
|
getSession(id: string): IChatSession | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* Get the command type (agent name) registered for a session.
|
|
165
|
+
*
|
|
166
|
+
* @param id - Session ID
|
|
167
|
+
* @returns Command type string (e.g. 'curate', 'query') or undefined if not found
|
|
168
|
+
*/
|
|
169
|
+
getSessionCommandType(id: string): string | undefined;
|
|
163
170
|
/**
|
|
164
171
|
* Get the number of active sessions.
|
|
165
172
|
*
|
|
@@ -283,6 +283,15 @@ export class SessionManager {
|
|
|
283
283
|
getSession(id) {
|
|
284
284
|
return this.sessions.get(id);
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Get the command type (agent name) registered for a session.
|
|
288
|
+
*
|
|
289
|
+
* @param id - Session ID
|
|
290
|
+
* @returns Command type string (e.g. 'curate', 'query') or undefined if not found
|
|
291
|
+
*/
|
|
292
|
+
getSessionCommandType(id) {
|
|
293
|
+
return this.sessionAgentNames.get(id);
|
|
294
|
+
}
|
|
286
295
|
/**
|
|
287
296
|
* Get the number of active sessions.
|
|
288
297
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { Tool, ToolExecutionContext } from '../../../core/domain/tools/types.js';
|
|
3
|
+
import type { AbstractGenerationQueue } from '../../map/abstract-queue.js';
|
|
3
4
|
/**
|
|
4
5
|
* Operation types for curating knowledge topics.
|
|
5
6
|
* Inspired by ACE Curator patterns.
|
|
@@ -573,6 +574,6 @@ export interface CurateOutput {
|
|
|
573
574
|
* Execute curate operations on knowledge topics.
|
|
574
575
|
* Exported for use by CurateService in sandbox.
|
|
575
576
|
*/
|
|
576
|
-
export declare function executeCurate(input: unknown, _context?: ToolExecutionContext): Promise<CurateOutput>;
|
|
577
|
-
export declare function createCurateTool(workingDirectory?: string): Tool;
|
|
577
|
+
export declare function executeCurate(input: unknown, _context?: ToolExecutionContext, abstractQueue?: AbstractGenerationQueue): Promise<CurateOutput>;
|
|
578
|
+
export declare function createCurateTool(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue): Tool;
|
|
578
579
|
export {};
|
|
@@ -293,7 +293,7 @@ function generateSubtopicContextMarkdown(subtopicName, context) {
|
|
|
293
293
|
}
|
|
294
294
|
return sections.join('\n');
|
|
295
295
|
}
|
|
296
|
-
async function createDomainContextIfMissing(basePath, domain, domainContext) {
|
|
296
|
+
async function createDomainContextIfMissing(basePath, domain, domainContext, onAfterWrite) {
|
|
297
297
|
const normalizedDomain = toSnakeCase(domain);
|
|
298
298
|
const contextPath = join(basePath, normalizedDomain, 'context.md');
|
|
299
299
|
const exists = await DirectoryManager.fileExists(contextPath);
|
|
@@ -305,9 +305,10 @@ async function createDomainContextIfMissing(basePath, domain, domainContext) {
|
|
|
305
305
|
}
|
|
306
306
|
const content = generateDomainContextMarkdown(normalizedDomain, domainContext);
|
|
307
307
|
await DirectoryManager.writeFileAtomic(contextPath, content);
|
|
308
|
+
onAfterWrite?.(contextPath, content);
|
|
308
309
|
return { created: true, path: contextPath };
|
|
309
310
|
}
|
|
310
|
-
async function ensureTopicContextMd(basePath, domain, topic, topicContext) {
|
|
311
|
+
async function ensureTopicContextMd(basePath, domain, topic, topicContext, onAfterWrite) {
|
|
311
312
|
const normalizedDomain = toSnakeCase(domain);
|
|
312
313
|
const normalizedTopic = toSnakeCase(topic);
|
|
313
314
|
const topicPath = join(basePath, normalizedDomain, normalizedTopic);
|
|
@@ -327,6 +328,7 @@ async function ensureTopicContextMd(basePath, domain, topic, topicContext) {
|
|
|
327
328
|
}
|
|
328
329
|
const content = generateTopicContextMarkdown(normalizedTopic, topicContext);
|
|
329
330
|
await DirectoryManager.writeFileAtomic(contextPath, content);
|
|
331
|
+
onAfterWrite?.(contextPath, content);
|
|
330
332
|
return { created: true, path: contextPath };
|
|
331
333
|
}
|
|
332
334
|
/**
|
|
@@ -334,7 +336,7 @@ async function ensureTopicContextMd(basePath, domain, topic, topicContext) {
|
|
|
334
336
|
* Only creates context.md if LLM provides subtopicContext - no static templates.
|
|
335
337
|
*/
|
|
336
338
|
async function ensureSubtopicContextMd(options) {
|
|
337
|
-
const { basePath, domain, subtopic, subtopicContext, topic } = options;
|
|
339
|
+
const { basePath, domain, onAfterWrite, subtopic, subtopicContext, topic } = options;
|
|
338
340
|
const normalizedDomain = toSnakeCase(domain);
|
|
339
341
|
const normalizedTopic = toSnakeCase(topic);
|
|
340
342
|
const normalizedSubtopic = toSnakeCase(subtopic);
|
|
@@ -355,26 +357,43 @@ async function ensureSubtopicContextMd(options) {
|
|
|
355
357
|
}
|
|
356
358
|
const content = generateSubtopicContextMarkdown(normalizedSubtopic, subtopicContext);
|
|
357
359
|
await DirectoryManager.writeFileAtomic(contextPath, content);
|
|
360
|
+
onAfterWrite?.(contextPath, content);
|
|
358
361
|
return { created: true, path: contextPath };
|
|
359
362
|
}
|
|
360
363
|
/**
|
|
361
364
|
* Ensure context.md exists at all levels for a given path (topic and subtopic).
|
|
362
365
|
* This is called during ADD operations to create context.md files with LLM-provided content.
|
|
363
366
|
*/
|
|
364
|
-
async function ensureContextMd(basePath, parsed, topicContext, subtopicContext) {
|
|
367
|
+
async function ensureContextMd(basePath, parsed, topicContext, subtopicContext, onAfterWrite) {
|
|
365
368
|
// Ensure topic-level context.md exists
|
|
366
|
-
await ensureTopicContextMd(basePath, parsed.domain, parsed.topic, topicContext);
|
|
369
|
+
await ensureTopicContextMd(basePath, parsed.domain, parsed.topic, topicContext, onAfterWrite);
|
|
367
370
|
// If subtopic exists, ensure subtopic-level context.md exists
|
|
368
371
|
if (parsed.subtopic) {
|
|
369
372
|
await ensureSubtopicContextMd({
|
|
370
373
|
basePath,
|
|
371
374
|
domain: parsed.domain,
|
|
375
|
+
onAfterWrite,
|
|
372
376
|
subtopic: parsed.subtopic,
|
|
373
377
|
subtopicContext,
|
|
374
378
|
topic: parsed.topic,
|
|
375
379
|
});
|
|
376
380
|
}
|
|
377
381
|
}
|
|
382
|
+
async function deleteDerivedSiblings(contextPath) {
|
|
383
|
+
const siblingPaths = [
|
|
384
|
+
contextPath.replace(/\.md$/, '.abstract.md'),
|
|
385
|
+
contextPath.replace(/\.md$/, '.overview.md'),
|
|
386
|
+
];
|
|
387
|
+
/* eslint-disable no-await-in-loop */
|
|
388
|
+
for (const siblingPath of siblingPaths) {
|
|
389
|
+
if (siblingPath === contextPath)
|
|
390
|
+
continue;
|
|
391
|
+
if (await DirectoryManager.fileExists(siblingPath)) {
|
|
392
|
+
await DirectoryManager.deleteFile(siblingPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/* eslint-enable no-await-in-loop */
|
|
396
|
+
}
|
|
378
397
|
/**
|
|
379
398
|
* Parse a path into domain, topic, and optional subtopic.
|
|
380
399
|
*/
|
|
@@ -432,7 +451,7 @@ function buildFullPath(basePath, knowledgePath) {
|
|
|
432
451
|
/**
|
|
433
452
|
* Execute ADD operation - create new domain/topic/subtopic with {title}.md
|
|
434
453
|
*/
|
|
435
|
-
async function executeAdd(basePath, operation) {
|
|
454
|
+
async function executeAdd(basePath, operation, onAfterWrite) {
|
|
436
455
|
const { confidence, content, domainContext, impact, path, reason, subtopicContext, summary, title, topicContext } = operation;
|
|
437
456
|
const reviewMeta = deriveReviewMetadata('ADD', confidence, impact);
|
|
438
457
|
if (!title) {
|
|
@@ -478,7 +497,7 @@ async function executeAdd(basePath, operation) {
|
|
|
478
497
|
type: 'ADD',
|
|
479
498
|
};
|
|
480
499
|
}
|
|
481
|
-
await createDomainContextIfMissing(basePath, parsed.domain, domainContext);
|
|
500
|
+
await createDomainContextIfMissing(basePath, parsed.domain, domainContext, onAfterWrite);
|
|
482
501
|
const domainPath = join(basePath, toSnakeCase(parsed.domain));
|
|
483
502
|
const topicPath = join(domainPath, toSnakeCase(parsed.topic));
|
|
484
503
|
const finalPath = parsed.subtopic ? join(topicPath, toSnakeCase(parsed.subtopic)) : topicPath;
|
|
@@ -500,7 +519,8 @@ async function executeAdd(basePath, operation) {
|
|
|
500
519
|
const filename = `${toSnakeCase(title)}.md`;
|
|
501
520
|
const contextPath = join(finalPath, filename);
|
|
502
521
|
await DirectoryManager.writeFileAtomic(contextPath, contextContent);
|
|
503
|
-
|
|
522
|
+
onAfterWrite?.(contextPath, contextContent);
|
|
523
|
+
await ensureContextMd(basePath, parsed, topicContext, subtopicContext, onAfterWrite);
|
|
504
524
|
return {
|
|
505
525
|
...reviewMeta,
|
|
506
526
|
filePath: contextPath,
|
|
@@ -533,7 +553,7 @@ function maxImpact(a, b) {
|
|
|
533
553
|
/**
|
|
534
554
|
* Execute UPDATE operation - modify existing {title}.md
|
|
535
555
|
*/
|
|
536
|
-
async function executeUpdate(basePath, operation) {
|
|
556
|
+
async function executeUpdate(basePath, operation, onAfterWrite) {
|
|
537
557
|
const { confidence, content, domainContext, impact, path, reason, subtopicContext, summary, title, topicContext } = operation;
|
|
538
558
|
// Used for early-exit validation failures (before structural loss can be assessed)
|
|
539
559
|
const baseReviewMeta = deriveReviewMetadata('UPDATE', confidence, impact);
|
|
@@ -583,7 +603,7 @@ async function executeUpdate(basePath, operation) {
|
|
|
583
603
|
type: 'UPDATE',
|
|
584
604
|
};
|
|
585
605
|
}
|
|
586
|
-
await createDomainContextIfMissing(basePath, parsed.domain, domainContext);
|
|
606
|
+
await createDomainContextIfMissing(basePath, parsed.domain, domainContext, onAfterWrite);
|
|
587
607
|
// Read existing file to preserve scoring metadata and detect structural loss
|
|
588
608
|
const existingContent = await DirectoryManager.readFile(contextPath);
|
|
589
609
|
const existingScoring = existingContent ? parseFrontmatterScoring(existingContent) : undefined;
|
|
@@ -623,7 +643,8 @@ async function executeUpdate(basePath, operation) {
|
|
|
623
643
|
});
|
|
624
644
|
await backupBeforeWrite(contextPath, basePath);
|
|
625
645
|
await DirectoryManager.writeFileAtomic(contextPath, contextContent);
|
|
626
|
-
|
|
646
|
+
onAfterWrite?.(contextPath, contextContent);
|
|
647
|
+
await ensureContextMd(basePath, parsed, topicContext, subtopicContext, onAfterWrite);
|
|
627
648
|
return {
|
|
628
649
|
...reviewMeta,
|
|
629
650
|
filePath: contextPath,
|
|
@@ -651,7 +672,7 @@ async function executeUpdate(basePath, operation) {
|
|
|
651
672
|
* Execute UPSERT operation - automatically creates or updates based on file existence
|
|
652
673
|
* This is the recommended operation type as it eliminates the need for pre-checks.
|
|
653
674
|
*/
|
|
654
|
-
async function executeUpsert(basePath, operation) {
|
|
675
|
+
async function executeUpsert(basePath, operation, onAfterWrite) {
|
|
655
676
|
const { path, reason, title } = operation;
|
|
656
677
|
const reviewMeta = deriveReviewMetadata('UPSERT', operation.confidence, operation.impact);
|
|
657
678
|
if (!title) {
|
|
@@ -693,7 +714,7 @@ async function executeUpsert(basePath, operation) {
|
|
|
693
714
|
const exists = await DirectoryManager.fileExists(contextPath);
|
|
694
715
|
if (exists) {
|
|
695
716
|
// File exists - delegate to UPDATE logic
|
|
696
|
-
const result = await executeUpdate(basePath, { ...operation, type: 'UPDATE' });
|
|
717
|
+
const result = await executeUpdate(basePath, { ...operation, type: 'UPDATE' }, onAfterWrite);
|
|
697
718
|
// Return with UPSERT type but indicate it was an update
|
|
698
719
|
return {
|
|
699
720
|
...result,
|
|
@@ -702,7 +723,7 @@ async function executeUpsert(basePath, operation) {
|
|
|
702
723
|
};
|
|
703
724
|
}
|
|
704
725
|
// File doesn't exist - delegate to ADD logic
|
|
705
|
-
const result = await executeAdd(basePath, { ...operation, type: 'ADD' });
|
|
726
|
+
const result = await executeAdd(basePath, { ...operation, type: 'ADD' }, onAfterWrite);
|
|
706
727
|
// Return with UPSERT type but indicate it was an add
|
|
707
728
|
return {
|
|
708
729
|
...result,
|
|
@@ -724,7 +745,7 @@ async function executeUpsert(basePath, operation) {
|
|
|
724
745
|
/**
|
|
725
746
|
* Execute MERGE operation - combine source file into target file, delete source file
|
|
726
747
|
*/
|
|
727
|
-
async function executeMerge(basePath, operation) {
|
|
748
|
+
async function executeMerge(basePath, operation, onAfterWrite) {
|
|
728
749
|
const { confidence, domainContext, impact, mergeTarget, mergeTargetTitle, path, reason, subtopicContext, summary, title, topicContext, } = operation;
|
|
729
750
|
const reviewMeta = deriveReviewMetadata('MERGE', confidence, impact);
|
|
730
751
|
if (!title) {
|
|
@@ -798,8 +819,8 @@ async function executeMerge(basePath, operation) {
|
|
|
798
819
|
type: 'MERGE',
|
|
799
820
|
};
|
|
800
821
|
}
|
|
801
|
-
await createDomainContextIfMissing(basePath, sourceParsed.domain, domainContext);
|
|
802
|
-
await createDomainContextIfMissing(basePath, targetParsed.domain, domainContext);
|
|
822
|
+
await createDomainContextIfMissing(basePath, sourceParsed.domain, domainContext, onAfterWrite);
|
|
823
|
+
await createDomainContextIfMissing(basePath, targetParsed.domain, domainContext, onAfterWrite);
|
|
803
824
|
const sourceContent = await DirectoryManager.readFile(sourceContextPath);
|
|
804
825
|
const targetContent = await DirectoryManager.readFile(targetContextPath);
|
|
805
826
|
// Extract previous summary from target file (for review UI)
|
|
@@ -810,9 +831,11 @@ async function executeMerge(basePath, operation) {
|
|
|
810
831
|
await backupBeforeWrite(sourceContextPath, basePath);
|
|
811
832
|
const mergedContent = MarkdownWriter.mergeContexts(sourceContent, targetContent, reason, summary);
|
|
812
833
|
await DirectoryManager.writeFileAtomic(targetContextPath, mergedContent);
|
|
834
|
+
onAfterWrite?.(targetContextPath, mergedContent);
|
|
813
835
|
await DirectoryManager.deleteFile(sourceContextPath);
|
|
814
|
-
await
|
|
815
|
-
await ensureContextMd(basePath,
|
|
836
|
+
await deleteDerivedSiblings(sourceContextPath);
|
|
837
|
+
await ensureContextMd(basePath, sourceParsed, topicContext, subtopicContext, onAfterWrite);
|
|
838
|
+
await ensureContextMd(basePath, targetParsed, topicContext, subtopicContext, onAfterWrite);
|
|
816
839
|
return {
|
|
817
840
|
...reviewMeta,
|
|
818
841
|
additionalFilePaths: [sourceContextPath],
|
|
@@ -874,6 +897,7 @@ async function executeDelete(basePath, operation) {
|
|
|
874
897
|
}
|
|
875
898
|
await backupBeforeWrite(filePath, basePath);
|
|
876
899
|
await DirectoryManager.deleteFile(filePath);
|
|
900
|
+
await deleteDerivedSiblings(filePath);
|
|
877
901
|
return {
|
|
878
902
|
...reviewMeta,
|
|
879
903
|
filePath,
|
|
@@ -948,7 +972,7 @@ async function executeDelete(basePath, operation) {
|
|
|
948
972
|
* Execute curate operations on knowledge topics.
|
|
949
973
|
* Exported for use by CurateService in sandbox.
|
|
950
974
|
*/
|
|
951
|
-
export async function executeCurate(input, _context) {
|
|
975
|
+
export async function executeCurate(input, _context, abstractQueue) {
|
|
952
976
|
const parseResult = CurateInputSchema.safeParse(input);
|
|
953
977
|
if (!parseResult.success) {
|
|
954
978
|
return {
|
|
@@ -974,6 +998,9 @@ export async function executeCurate(input, _context) {
|
|
|
974
998
|
};
|
|
975
999
|
}
|
|
976
1000
|
const { basePath, operations } = parseResult.data;
|
|
1001
|
+
const onAfterWrite = abstractQueue
|
|
1002
|
+
? (contextPath, content) => { abstractQueue.enqueue({ contextPath, fullContent: content }); }
|
|
1003
|
+
: undefined;
|
|
977
1004
|
const applied = [];
|
|
978
1005
|
const summary = {
|
|
979
1006
|
added: 0,
|
|
@@ -987,7 +1014,7 @@ export async function executeCurate(input, _context) {
|
|
|
987
1014
|
let result;
|
|
988
1015
|
switch (operation.type) {
|
|
989
1016
|
case 'ADD': {
|
|
990
|
-
result = await executeAdd(basePath, operation);
|
|
1017
|
+
result = await executeAdd(basePath, operation, onAfterWrite);
|
|
991
1018
|
if (result.status === 'success')
|
|
992
1019
|
summary.added++;
|
|
993
1020
|
break;
|
|
@@ -999,19 +1026,19 @@ export async function executeCurate(input, _context) {
|
|
|
999
1026
|
break;
|
|
1000
1027
|
}
|
|
1001
1028
|
case 'MERGE': {
|
|
1002
|
-
result = await executeMerge(basePath, operation);
|
|
1029
|
+
result = await executeMerge(basePath, operation, onAfterWrite);
|
|
1003
1030
|
if (result.status === 'success')
|
|
1004
1031
|
summary.merged++;
|
|
1005
1032
|
break;
|
|
1006
1033
|
}
|
|
1007
1034
|
case 'UPDATE': {
|
|
1008
|
-
result = await executeUpdate(basePath, operation);
|
|
1035
|
+
result = await executeUpdate(basePath, operation, onAfterWrite);
|
|
1009
1036
|
if (result.status === 'success')
|
|
1010
1037
|
summary.updated++;
|
|
1011
1038
|
break;
|
|
1012
1039
|
}
|
|
1013
1040
|
case 'UPSERT': {
|
|
1014
|
-
result = await executeUpsert(basePath, operation);
|
|
1041
|
+
result = await executeUpsert(basePath, operation, onAfterWrite);
|
|
1015
1042
|
// UPSERT counts as either added or updated based on what happened
|
|
1016
1043
|
if (result.status === 'success') {
|
|
1017
1044
|
if (result.message?.includes('created new')) {
|
|
@@ -1046,7 +1073,7 @@ export async function executeCurate(input, _context) {
|
|
|
1046
1073
|
/* eslint-enable no-await-in-loop */
|
|
1047
1074
|
return { applied, summary };
|
|
1048
1075
|
}
|
|
1049
|
-
export function createCurateTool(workingDirectory) {
|
|
1076
|
+
export function createCurateTool(workingDirectory, abstractQueue) {
|
|
1050
1077
|
return {
|
|
1051
1078
|
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.
|
|
1052
1079
|
|
|
@@ -1246,10 +1273,10 @@ export function createCurateTool(workingDirectory) {
|
|
|
1246
1273
|
const parseResult = CurateInputSchema.safeParse(input);
|
|
1247
1274
|
if (parseResult.success) {
|
|
1248
1275
|
parseResult.data.basePath = resolve(workingDirectory, parseResult.data.basePath);
|
|
1249
|
-
return executeCurate(parseResult.data, context);
|
|
1276
|
+
return executeCurate(parseResult.data, context, abstractQueue);
|
|
1250
1277
|
}
|
|
1251
1278
|
}
|
|
1252
|
-
return executeCurate(input, context);
|
|
1279
|
+
return executeCurate(input, context, abstractQueue);
|
|
1253
1280
|
},
|
|
1254
1281
|
id: ToolName.CURATE,
|
|
1255
1282
|
inputSchema: CurateInputSchema,
|
|
@@ -8,9 +8,9 @@ export interface ExpandKnowledgeToolConfig {
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates the expand knowledge tool.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Two modes:
|
|
12
|
+
* - stubPath: retrieves full content from archived knowledge entries (archive drill-down)
|
|
13
|
+
* - overviewPath: retrieves L1 overview content from .overview.md sibling files
|
|
14
14
|
*
|
|
15
15
|
* @param config - Optional configuration
|
|
16
16
|
* @returns Configured expand knowledge tool
|