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.
Files changed (196) hide show
  1. package/.env.production +4 -0
  2. package/README.md +17 -0
  3. package/dist/agent/core/domain/tools/constants.d.ts +1 -0
  4. package/dist/agent/core/domain/tools/constants.js +1 -0
  5. package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
  6. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  7. package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
  8. package/dist/agent/infra/agent/agent-error-codes.js +0 -1
  9. package/dist/agent/infra/agent/agent-error.d.ts +0 -1
  10. package/dist/agent/infra/agent/agent-error.js +0 -1
  11. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  12. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  13. package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
  14. package/dist/agent/infra/agent/agent-state-manager.js +1 -3
  15. package/dist/agent/infra/agent/base-agent.d.ts +1 -1
  16. package/dist/agent/infra/agent/base-agent.js +1 -1
  17. package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
  18. package/dist/agent/infra/agent/cipher-agent.js +188 -3
  19. package/dist/agent/infra/agent/index.d.ts +1 -1
  20. package/dist/agent/infra/agent/index.js +1 -1
  21. package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
  22. package/dist/agent/infra/agent/service-initializer.js +14 -8
  23. package/dist/agent/infra/agent/types.d.ts +0 -1
  24. package/dist/agent/infra/file-system/file-system-service.js +6 -5
  25. package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
  26. package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
  27. package/dist/agent/infra/llm/providers/openai.js +12 -0
  28. package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
  29. package/dist/agent/infra/llm/stream-to-text.js +14 -0
  30. package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
  31. package/dist/agent/infra/map/abstract-generator.js +67 -0
  32. package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
  33. package/dist/agent/infra/map/abstract-queue.js +218 -0
  34. package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
  35. package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
  36. package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
  37. package/dist/agent/infra/memory/memory-manager.js +6 -5
  38. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  39. package/dist/agent/infra/sandbox/curate-service.js +20 -7
  40. package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
  41. package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
  42. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  43. package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
  44. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  45. package/dist/agent/infra/session/session-compressor.d.ts +43 -0
  46. package/dist/agent/infra/session/session-compressor.js +296 -0
  47. package/dist/agent/infra/session/session-manager.d.ts +7 -0
  48. package/dist/agent/infra/session/session-manager.js +9 -0
  49. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
  50. package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
  51. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
  52. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
  53. package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
  54. package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
  55. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
  56. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
  57. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
  58. package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
  59. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  60. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  61. package/dist/agent/infra/tools/tool-provider.js +1 -0
  62. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  63. package/dist/agent/infra/tools/tool-registry.js +16 -5
  64. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  65. package/dist/agent/infra/tools/write-guard.js +48 -0
  66. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  67. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  68. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  69. package/dist/oclif/commands/curate/index.js +4 -3
  70. package/dist/oclif/commands/curate/view.js +2 -2
  71. package/dist/oclif/commands/main.js +13 -0
  72. package/dist/oclif/commands/query.js +4 -3
  73. package/dist/oclif/commands/source/add.d.ts +12 -0
  74. package/dist/oclif/commands/source/add.js +42 -0
  75. package/dist/oclif/commands/source/index.d.ts +6 -0
  76. package/dist/oclif/commands/source/index.js +8 -0
  77. package/dist/oclif/commands/source/list.d.ts +6 -0
  78. package/dist/oclif/commands/source/list.js +32 -0
  79. package/dist/oclif/commands/source/remove.d.ts +9 -0
  80. package/dist/oclif/commands/source/remove.js +33 -0
  81. package/dist/oclif/commands/status.d.ts +5 -1
  82. package/dist/oclif/commands/status.js +41 -6
  83. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  84. package/dist/oclif/commands/worktree/add.js +44 -0
  85. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  86. package/dist/oclif/commands/worktree/index.js +8 -0
  87. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  88. package/dist/oclif/commands/worktree/list.js +28 -0
  89. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  90. package/dist/oclif/commands/worktree/remove.js +35 -0
  91. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  92. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  93. package/dist/oclif/lib/daemon-client.js +19 -3
  94. package/dist/server/constants.d.ts +8 -0
  95. package/dist/server/constants.js +10 -0
  96. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  97. package/dist/server/core/domain/client/client-info.js +11 -0
  98. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
  99. package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
  100. package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
  101. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  102. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  103. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  104. package/dist/server/core/domain/source/source-operations.js +201 -0
  105. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  106. package/dist/server/core/domain/source/source-schema.js +121 -0
  107. package/dist/server/core/domain/transport/schemas.d.ts +18 -10
  108. package/dist/server/core/domain/transport/schemas.js +4 -0
  109. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  110. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  111. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  112. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  113. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  114. package/dist/server/infra/client/client-manager.d.ts +1 -0
  115. package/dist/server/infra/client/client-manager.js +16 -0
  116. package/dist/server/infra/context-tree/derived-artifact.js +5 -1
  117. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
  118. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
  119. package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
  120. package/dist/server/infra/daemon/agent-process.js +15 -5
  121. package/dist/server/infra/executor/curate-executor.js +6 -3
  122. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  123. package/dist/server/infra/executor/folder-pack-executor.js +88 -7
  124. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  125. package/dist/server/infra/executor/query-executor.js +125 -23
  126. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  127. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  128. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  129. package/dist/server/infra/mcp/mcp-server.js +11 -6
  130. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  131. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  132. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  133. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  134. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  135. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  136. package/dist/server/infra/process/connection-coordinator.js +11 -0
  137. package/dist/server/infra/process/feature-handlers.js +4 -1
  138. package/dist/server/infra/process/task-router.d.ts +1 -0
  139. package/dist/server/infra/process/task-router.js +60 -5
  140. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  141. package/dist/server/infra/project/resolve-project.js +473 -0
  142. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  143. package/dist/server/infra/transport/handlers/index.js +2 -0
  144. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  145. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  146. package/dist/server/infra/transport/handlers/status-handler.js +65 -13
  147. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  148. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  149. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  150. package/dist/server/infra/transport/transport-connector.js +2 -2
  151. package/dist/server/utils/curate-result-parser.d.ts +4 -4
  152. package/dist/server/utils/path-utils.d.ts +5 -0
  153. package/dist/server/utils/path-utils.js +11 -1
  154. package/dist/shared/transport/events/client-events.d.ts +3 -0
  155. package/dist/shared/transport/events/client-events.js +3 -0
  156. package/dist/shared/transport/events/index.d.ts +13 -0
  157. package/dist/shared/transport/events/index.js +9 -0
  158. package/dist/shared/transport/events/source-events.d.ts +30 -0
  159. package/dist/shared/transport/events/source-events.js +5 -0
  160. package/dist/shared/transport/events/status-events.d.ts +5 -0
  161. package/dist/shared/transport/events/task-events.d.ts +4 -1
  162. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  163. package/dist/shared/transport/events/worktree-events.js +5 -0
  164. package/dist/shared/transport/types/dto.d.ts +26 -0
  165. package/dist/tui/features/commands/definitions/index.js +6 -0
  166. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  167. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  168. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  169. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  170. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  171. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  172. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  173. package/dist/tui/features/commands/definitions/source.js +8 -0
  174. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  175. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  176. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  177. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  178. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  179. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  180. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  181. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  182. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  183. package/dist/tui/features/query/api/create-query-task.js +3 -1
  184. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  185. package/dist/tui/features/source/api/source-api.js +22 -0
  186. package/dist/tui/features/status/api/get-status.js +2 -1
  187. package/dist/tui/features/status/utils/format-status.js +23 -1
  188. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  189. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  190. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  191. package/dist/tui/repl-startup.d.ts +2 -0
  192. package/dist/tui/repl-startup.js +5 -3
  193. package/dist/tui/stores/transport-store.d.ts +6 -0
  194. package/dist/tui/stores/transport-store.js +6 -0
  195. package/oclif.manifest.json +261 -1
  196. 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
- await ensureContextMd(basePath, parsed, topicContext, subtopicContext);
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
- await ensureContextMd(basePath, parsed, topicContext, subtopicContext);
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 ensureContextMd(basePath, sourceParsed, topicContext, subtopicContext);
815
- await ensureContextMd(basePath, targetParsed, topicContext, subtopicContext);
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
- * Retrieves full content from archived knowledge entries by reading the
12
- * lossless .full.md file that the stub points to. No LLM call needed —
13
- * purely file-based lookup.
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