byterover-cli 3.0.1 → 3.1.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 (73) hide show
  1. package/dist/agent/core/domain/tools/constants.d.ts +1 -0
  2. package/dist/agent/core/domain/tools/constants.js +1 -0
  3. package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
  4. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  5. package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
  6. package/dist/agent/infra/agent/agent-error-codes.js +0 -1
  7. package/dist/agent/infra/agent/agent-error.d.ts +0 -1
  8. package/dist/agent/infra/agent/agent-error.js +0 -1
  9. package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
  10. package/dist/agent/infra/agent/agent-state-manager.js +1 -3
  11. package/dist/agent/infra/agent/base-agent.d.ts +1 -1
  12. package/dist/agent/infra/agent/base-agent.js +1 -1
  13. package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
  14. package/dist/agent/infra/agent/cipher-agent.js +188 -3
  15. package/dist/agent/infra/agent/index.d.ts +1 -1
  16. package/dist/agent/infra/agent/index.js +1 -1
  17. package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
  18. package/dist/agent/infra/agent/service-initializer.js +14 -8
  19. package/dist/agent/infra/agent/types.d.ts +0 -1
  20. package/dist/agent/infra/file-system/file-system-service.js +6 -5
  21. package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
  22. package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
  23. package/dist/agent/infra/llm/providers/openai.js +12 -0
  24. package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
  25. package/dist/agent/infra/llm/stream-to-text.js +14 -0
  26. package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
  27. package/dist/agent/infra/map/abstract-generator.js +67 -0
  28. package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
  29. package/dist/agent/infra/map/abstract-queue.js +218 -0
  30. package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
  31. package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
  32. package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
  33. package/dist/agent/infra/memory/memory-manager.js +6 -5
  34. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  35. package/dist/agent/infra/sandbox/curate-service.js +6 -7
  36. package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
  37. package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
  38. package/dist/agent/infra/sandbox/tools-sdk.d.ts +3 -1
  39. package/dist/agent/infra/session/session-compressor.d.ts +43 -0
  40. package/dist/agent/infra/session/session-compressor.js +296 -0
  41. package/dist/agent/infra/session/session-manager.d.ts +7 -0
  42. package/dist/agent/infra/session/session-manager.js +9 -0
  43. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
  44. package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
  45. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
  46. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
  47. package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
  48. package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
  49. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
  50. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
  51. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +207 -34
  52. package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
  53. package/dist/agent/infra/tools/tool-provider.js +1 -0
  54. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  55. package/dist/agent/infra/tools/tool-registry.js +15 -4
  56. package/dist/server/constants.d.ts +2 -0
  57. package/dist/server/constants.js +2 -0
  58. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
  59. package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
  60. package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
  61. package/dist/server/core/domain/transport/schemas.d.ts +10 -10
  62. package/dist/server/infra/context-tree/derived-artifact.js +5 -1
  63. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
  64. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
  65. package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
  66. package/dist/server/infra/executor/curate-executor.js +2 -1
  67. package/dist/server/infra/executor/folder-pack-executor.js +72 -2
  68. package/dist/server/infra/executor/query-executor.js +11 -3
  69. package/dist/server/infra/transport/handlers/status-handler.js +10 -0
  70. package/dist/server/utils/curate-result-parser.d.ts +4 -4
  71. package/dist/shared/transport/types/dto.d.ts +7 -0
  72. package/oclif.manifest.json +160 -160
  73. package/package.json +10 -4
@@ -521,18 +521,18 @@ export declare const TaskExecuteSchema: z.ZodObject<{
521
521
  taskId: string;
522
522
  clientId: string;
523
523
  files?: string[] | undefined;
524
- projectPath?: string | undefined;
525
- folderPath?: string | undefined;
526
524
  clientCwd?: string | undefined;
525
+ folderPath?: string | undefined;
526
+ projectPath?: string | undefined;
527
527
  }, {
528
528
  type: "curate" | "query" | "curate-folder";
529
529
  content: string;
530
530
  taskId: string;
531
531
  clientId: string;
532
532
  files?: string[] | undefined;
533
- projectPath?: string | undefined;
534
- folderPath?: string | undefined;
535
533
  clientCwd?: string | undefined;
534
+ folderPath?: string | undefined;
535
+ projectPath?: string | undefined;
536
536
  }>;
537
537
  /**
538
538
  * task:cancel - Transport tells Agent to cancel a task
@@ -689,15 +689,15 @@ export declare const TaskCreatedSchema: z.ZodObject<{
689
689
  content: string;
690
690
  taskId: string;
691
691
  files?: string[] | undefined;
692
- folderPath?: string | undefined;
693
692
  clientCwd?: string | undefined;
693
+ folderPath?: string | undefined;
694
694
  }, {
695
695
  type: "curate" | "query" | "curate-folder";
696
696
  content: string;
697
697
  taskId: string;
698
698
  files?: string[] | undefined;
699
- folderPath?: string | undefined;
700
699
  clientCwd?: string | undefined;
700
+ folderPath?: string | undefined;
701
701
  }>;
702
702
  /**
703
703
  * task:started - Agent begins processing the task
@@ -965,17 +965,17 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
965
965
  content: string;
966
966
  taskId: string;
967
967
  files?: string[] | undefined;
968
- projectPath?: string | undefined;
969
- folderPath?: string | undefined;
970
968
  clientCwd?: string | undefined;
969
+ folderPath?: string | undefined;
970
+ projectPath?: string | undefined;
971
971
  }, {
972
972
  type: "curate" | "query" | "curate-folder";
973
973
  content: string;
974
974
  taskId: string;
975
975
  files?: string[] | undefined;
976
- projectPath?: string | undefined;
977
- folderPath?: string | undefined;
978
976
  clientCwd?: string | undefined;
977
+ folderPath?: string | undefined;
978
+ projectPath?: string | undefined;
979
979
  }>;
980
980
  /**
981
981
  * Response after task creation
@@ -6,7 +6,7 @@
6
6
  * - isArchiveStub() — searchable stubs (included in BM25 index and fingerprint)
7
7
  * - isExcludedFromSync() — union of above (excluded from snapshot/sync/merge/push)
8
8
  */
9
- import { ARCHIVE_DIR, FULL_ARCHIVE_EXTENSION, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE } from '../../constants.js';
9
+ import { ABSTRACT_EXTENSION, ARCHIVE_DIR, FULL_ARCHIVE_EXTENSION, MANIFEST_FILE, OVERVIEW_EXTENSION, STUB_EXTENSION, SUMMARY_INDEX_FILE } from '../../constants.js';
10
10
  import { toUnixPath } from './path-utils.js';
11
11
  /**
12
12
  * Returns true if the given relative path is a derived artifact
@@ -24,6 +24,10 @@ export function isDerivedArtifact(relativePath) {
24
24
  return true;
25
25
  if (fileName === MANIFEST_FILE)
26
26
  return true;
27
+ if (fileName.endsWith(ABSTRACT_EXTENSION))
28
+ return true;
29
+ if (fileName.endsWith(OVERVIEW_EXTENSION))
30
+ return true;
27
31
  if (segments.includes(ARCHIVE_DIR) && fileName.endsWith(FULL_ARCHIVE_EXTENSION))
28
32
  return true;
29
33
  return false;
@@ -44,7 +44,8 @@ export declare class FileContextTreeManifestService implements IContextTreeManif
44
44
  private scanForManifest;
45
45
  /**
46
46
  * Recursively collect stat data for all source files (for fingerprint).
47
- * Excludes derived artifacts.
47
+ * Excludes derived artifacts except .abstract.md siblings, which are included
48
+ * so abstract generation invalidates the manifest without a second tree walk.
48
49
  */
49
50
  private scanSourceStats;
50
51
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
17
17
  import { join, relative } from 'node:path';
18
- import { ARCHIVE_DIR, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE, } from '../../constants.js';
18
+ import { ABSTRACT_EXTENSION, ARCHIVE_DIR, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE, } from '../../constants.js';
19
19
  import { parseFrontmatterScoring } from '../../core/domain/knowledge/markdown-writer.js';
20
20
  import { DEFAULT_LANE_BUDGETS } from '../../core/domain/knowledge/summary-types.js';
21
21
  import { estimateTokens } from '../executor/pre-compaction/compaction-escalation.js';
@@ -98,8 +98,23 @@ export class FileContextTreeManifestService {
98
98
  /* eslint-disable no-await-in-loop */
99
99
  for (const entry of ordered) {
100
100
  try {
101
- const fullPath = join(contextTreeDir, entry.path);
102
- const content = await readFile(fullPath, 'utf8');
101
+ let content;
102
+ // For context entries, prefer .abstract.md sibling if it exists on disk
103
+ // (dynamic read avoids stale-manifest issues since .abstract.md is a derived artifact)
104
+ if (entry.type === 'context') {
105
+ const abstractRelPath = entry.path.replace(/\.md$/, ABSTRACT_EXTENSION);
106
+ const abstractFullPath = join(contextTreeDir, abstractRelPath);
107
+ try {
108
+ content = await readFile(abstractFullPath, 'utf8');
109
+ }
110
+ catch {
111
+ // Abstract not ready yet — fall back to full content
112
+ content = await readFile(join(contextTreeDir, entry.path), 'utf8');
113
+ }
114
+ }
115
+ else {
116
+ content = await readFile(join(contextTreeDir, entry.path), 'utf8');
117
+ }
103
118
  resolved.push({
104
119
  content,
105
120
  path: entry.path,
@@ -188,6 +203,12 @@ export class FileContextTreeManifestService {
188
203
  catch {
189
204
  return;
190
205
  }
206
+ // Build a set of abstract sibling paths present in this directory so we can
207
+ // check existence without throwing ENOENT for every context file that has
208
+ // no abstract yet (the common case early in a project's lifetime).
209
+ const abstractsInDir = new Set(entries
210
+ .filter((e) => e.isFile() && e.name.endsWith(ABSTRACT_EXTENSION))
211
+ .map((e) => join(currentDir, e.name)));
191
212
  /* eslint-disable no-await-in-loop */
192
213
  for (const entry of entries) {
193
214
  const entryName = entry.name;
@@ -221,10 +242,24 @@ export class FileContextTreeManifestService {
221
242
  try {
222
243
  const content = await readFile(fullPath, 'utf8');
223
244
  const scoring = parseFrontmatterScoring(content);
245
+ // Use abstract sibling for token budgeting only if it is known to exist
246
+ // (checked via abstractsInDir set, avoiding ENOENT as control flow).
247
+ const abstractRelPath = relativePath.replace(/\.md$/, ABSTRACT_EXTENSION);
248
+ const abstractFullPath = join(contextTreeDir, abstractRelPath);
249
+ let abstractTokens;
250
+ if (abstractsInDir.has(abstractFullPath)) {
251
+ try {
252
+ const abstractContent = await readFile(abstractFullPath, 'utf8');
253
+ abstractTokens = estimateTokens(abstractContent);
254
+ }
255
+ catch { /* unreadable — treat as absent */ }
256
+ }
224
257
  contexts.push({
258
+ abstractPath: abstractTokens === undefined ? undefined : abstractRelPath,
259
+ abstractTokens,
225
260
  importance: scoring?.importance ?? 50,
226
261
  path: relativePath,
227
- tokens: estimateTokens(content),
262
+ tokens: abstractTokens ?? estimateTokens(content),
228
263
  type: 'context',
229
264
  });
230
265
  }
@@ -238,7 +273,8 @@ export class FileContextTreeManifestService {
238
273
  }
239
274
  /**
240
275
  * Recursively collect stat data for all source files (for fingerprint).
241
- * Excludes derived artifacts.
276
+ * Excludes derived artifacts except .abstract.md siblings, which are included
277
+ * so abstract generation invalidates the manifest without a second tree walk.
242
278
  */
243
279
  async scanSourceStats(currentDir, contextTreeDir, entries) {
244
280
  let dirEntries;
@@ -257,8 +293,8 @@ export class FileContextTreeManifestService {
257
293
  }
258
294
  else if (entry.isFile() && entryName.endsWith(CONTEXT_FILE_EXTENSION)) {
259
295
  const relativePath = toUnixPath(relative(contextTreeDir, fullPath));
260
- // Include all non-derived files (contexts + stubs) in fingerprint
261
- if (isDerivedArtifact(relativePath))
296
+ const isAbstractSibling = entryName.endsWith(ABSTRACT_EXTENSION);
297
+ if (!isAbstractSibling && isDerivedArtifact(relativePath))
262
298
  continue;
263
299
  try {
264
300
  const fileStat = await stat(fullPath);
@@ -74,7 +74,18 @@ export class FileContextTreeSummaryService {
74
74
  // Step 5: Three-tier escalation via CipherAgent
75
75
  const taskId = `summary_${directoryPath.replaceAll('/', '_') || 'root'}`;
76
76
  const childEntries = children.map((c) => ({ content: c.content, name: c.name }));
77
- const summaryText = await this.generateWithEscalation(agent, taskId, childEntries, level, totalInputTokens);
77
+ let summaryText;
78
+ try {
79
+ summaryText = await this.generateWithEscalation(agent, taskId, childEntries, level, totalInputTokens);
80
+ }
81
+ catch {
82
+ const combinedInput = childEntries.map((entry) => `## ${entry.name}\n${entry.content}`).join('\n\n');
83
+ summaryText = buildDeterministicFallbackCompaction({
84
+ inputTokens: totalInputTokens,
85
+ sourceText: combinedInput,
86
+ suffixLabel: 'summary compaction',
87
+ });
88
+ }
78
89
  // Step 6: Write _index.md
79
90
  const summaryTokens = estimateTokens(summaryText);
80
91
  const frontmatter = {
@@ -289,7 +300,14 @@ export class FileContextTreeSummaryService {
289
300
  });
290
301
  }
291
302
  finally {
292
- await agent.deleteTaskSession(sessionId);
303
+ try {
304
+ // Cleanup is best-effort. A generated summary should still be written even
305
+ // if the backing task session cannot be torn down cleanly.
306
+ await agent.deleteTaskSession(sessionId);
307
+ }
308
+ catch {
309
+ // Ignore cleanup failures
310
+ }
293
311
  }
294
312
  }
295
313
  /**
@@ -54,7 +54,7 @@ export class CurateExecutor {
54
54
  catch {
55
55
  // Fail-open: if snapshot fails, skip summary propagation
56
56
  }
57
- const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true });
57
+ const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true, userFacing: true });
58
58
  try {
59
59
  // Task-scoped variable names for RLM pattern.
60
60
  // Replace hyphens with underscores: UUIDs have hyphens which are invalid in JS identifiers,
@@ -124,6 +124,7 @@ export class CurateExecutor {
124
124
  // Fail-open: summary/manifest errors never block curation
125
125
  }
126
126
  }
127
+ await agent.drainBackgroundWork?.();
127
128
  return response;
128
129
  }
129
130
  finally {
@@ -1,6 +1,10 @@
1
1
  import { appendFileSync } from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
5
+ import { FileContextTreeSnapshotService } from '../context-tree/file-context-tree-snapshot-service.js';
6
+ import { FileContextTreeSummaryService } from '../context-tree/file-context-tree-summary-service.js';
7
+ import { diffStates } from '../context-tree/snapshot-diff.js';
4
8
  const LOG_PATH = process.env.BRV_SESSION_LOG;
5
9
  function folderPackLog(message) {
6
10
  if (!LOG_PATH)
@@ -41,6 +45,14 @@ export class FolderPackExecutor {
41
45
  // Resolve folder path
42
46
  const basePath = clientCwd ?? process.cwd();
43
47
  const absoluteFolderPath = path.isAbsolute(folderPath) ? folderPath : path.resolve(basePath, folderPath);
48
+ const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: basePath });
49
+ let preState;
50
+ try {
51
+ preState = await snapshotService.getCurrentState(basePath);
52
+ }
53
+ catch {
54
+ // Fail-open: if snapshot fails, skip summary propagation
55
+ }
44
56
  // Pack the folder
45
57
  const packResult = await this.folderPackService.pack(absoluteFolderPath, {
46
58
  extractDocuments: true,
@@ -50,7 +62,26 @@ export class FolderPackExecutor {
50
62
  // Use iterative extraction strategy (inspired by rlm)
51
63
  // Stores packed folder in sandbox environment and lets agent iteratively query/extract
52
64
  // This avoids token limits entirely - works for folders of any size
53
- return this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, basePath);
65
+ const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, basePath);
66
+ if (preState) {
67
+ try {
68
+ const postState = await snapshotService.getCurrentState(basePath);
69
+ const changedPaths = diffStates(preState, postState);
70
+ if (changedPaths.length > 0) {
71
+ const summaryService = new FileContextTreeSummaryService();
72
+ const results = await summaryService.propagateStaleness(changedPaths, agent, basePath);
73
+ if (results.some((result) => result.actionTaken)) {
74
+ const manifestService = new FileContextTreeManifestService({ baseDirectory: basePath });
75
+ await manifestService.buildManifest(basePath);
76
+ }
77
+ }
78
+ }
79
+ catch {
80
+ // Fail-open: summary/manifest errors never block curation
81
+ }
82
+ }
83
+ await agent.drainBackgroundWork?.();
84
+ return response;
54
85
  }
55
86
  /**
56
87
  * Build iterative extraction prompt with file-based access.
@@ -133,6 +164,15 @@ Use **code_exec with tools.readFile/tools.grep** to extract knowledge:
133
164
 
134
165
  **Important**: All tools.* methods are async - always use \`await\`!
135
166
 
167
+ ## Curate Shape Constraints
168
+
169
+ - Prefer one concrete knowledge entry per relevant source file when curating a small leaf folder.
170
+ - For folders with 3 or fewer relevant source files, keep the number of curated leaf entries at or below the number of curated source files by default.
171
+ - Do **NOT** create an extra module/folder "overview" leaf at the bare topic path just because the folder has multiple files.
172
+ - Treat bare topic paths as scopes for \`topicContext\`, not as default destinations for standalone knowledge files.
173
+ - If you need topic-level framing, provide \`topicContext\` on the operation that creates the topic. The system will create/update \`context.md\` and higher-level summaries separately.
174
+ - Only add a standalone overview leaf when the user explicitly asks for it or when there is a distinct cross-file concept that cannot be represented by the per-file entries.
175
+
136
176
  ## Common Mistakes to Avoid
137
177
 
138
178
  **❌ WRONG - Using require() inside code_exec:**
@@ -773,7 +813,7 @@ await tools.curate([{
773
813
  throw new Error(`Failed to write temp file: ${error instanceof Error ? error.message : String(error)}`);
774
814
  }
775
815
  // Create per-task session for parallel isolation (own sandbox + history + LLM service)
776
- const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true });
816
+ const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true, userFacing: true });
777
817
  // Step 3: Store full instructions as sandbox variable (lazy prompt loading).
778
818
  // This saves ~12-15K tokens by keeping the massive instruction set out of the prompt.
779
819
  // The LLM reads instructions on-demand via code_exec.
@@ -783,15 +823,45 @@ await tools.curate([{
783
823
  const taskIdSafe = taskId.replaceAll('-', '_');
784
824
  const instructionsVar = `__curate_instructions_${taskIdSafe}`;
785
825
  agent.setSandboxVariableOnSession(taskSessionId, instructionsVar, fullInstructions);
826
+ const smallFolderFilesVar = `__curate_files_${taskIdSafe}`;
827
+ const shouldExposePackedFiles = packResult.files.length > 0 && packResult.files.length <= 10 && packResult.totalCharacters <= 80_000;
828
+ if (shouldExposePackedFiles) {
829
+ agent.setSandboxVariableOnSession(taskSessionId, smallFolderFilesVar, packResult.files.map((file) => ({
830
+ content: file.content,
831
+ fileType: file.fileType,
832
+ lineCount: file.lineCount,
833
+ path: file.path,
834
+ size: file.size,
835
+ truncated: file.truncated,
836
+ })));
837
+ }
786
838
  // Compact prompt with variable reference and essential metadata
787
839
  const contextSection = userContext?.trim() ? `\nUser context: ${userContext}\n` : '';
840
+ const sourceFilePaths = packResult.files.map((file) => file.path);
841
+ const sourceFilesSection = sourceFilePaths.length > 0
842
+ ? `Relevant source files: ${sourceFilePaths.join(', ')} (these paths are relative to the packed folder root; do not prefix them with parent directories like src/auth/)`
843
+ : undefined;
844
+ const smallFolderLeafQuota = sourceFilePaths.length > 0 && sourceFilePaths.length <= 3
845
+ ? `Leaf quota: create no more than ${sourceFilePaths.length} curated leaf knowledge files for this folder unless the user explicitly asks for more.`
846
+ : undefined;
847
+ const smallFolderQuotaWarning = sourceFilePaths.length > 0 && sourceFilePaths.length <= 3
848
+ ? `A topic-level overview leaf counts toward that quota and is usually incorrect here; keep folder-level framing in topicContext instead.`
849
+ : undefined;
788
850
  const compactPrompt = [
789
851
  `# Folder Curation Task`,
790
852
  ``,
791
853
  `Folder: ${folderPath} (${packResult.fileCount} files, ${packResult.totalLines} lines)`,
792
854
  `Data file: \`${tmpFilePath}\` (repomix-style XML format)`,
793
855
  `Full instructions: variable \`${instructionsVar}\``,
856
+ shouldExposePackedFiles
857
+ ? `Relevant files variable: \`${smallFolderFilesVar}\` (array of packed files; for this small folder, prefer using it directly instead of parsing XML with brittle regexes).`
858
+ : undefined,
794
859
  contextSection,
860
+ sourceFilesSection,
861
+ `Small-folder rule: for folders with 3 or fewer relevant source files, create at most one leaf knowledge entry per file by default.`,
862
+ smallFolderLeafQuota,
863
+ smallFolderQuotaWarning,
864
+ `Do not create an extra overview leaf at the bare topic path; use topicContext for topic-level framing instead.`,
795
865
  `**Start by reading instructions**: Use code_exec to read \`${instructionsVar}.slice(0, 5000)\` for the strategy section, then \`${instructionsVar}.slice(5000, 10000)\` for content rules.`,
796
866
  `Use \`tools.readFile()\` and \`tools.grep()\` inside code_exec to process the XML data file.`,
797
867
  `Use \`tools.curate()\` to create knowledge topics. Use \`setFinalResult()\` when done.`,
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
2
+ import { ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
3
3
  import { isDerivedArtifact } from '../context-tree/derived-artifact.js';
4
4
  import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
5
5
  import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } from './direct-search-responder.js';
@@ -124,7 +124,7 @@ export class QueryExecutor {
124
124
  }
125
125
  }
126
126
  // Create per-task session for parallel isolation (own sandbox + history + LLM service)
127
- const taskSessionId = await agent.createTaskSession(taskId, 'query');
127
+ const taskSessionId = await agent.createTaskSession(taskId, 'query', { userFacing: true });
128
128
  // Task-scoped variable names for sandbox injection (RLM pattern).
129
129
  // Replace hyphens with underscores: UUIDs have hyphens which are invalid in JS identifiers,
130
130
  // so the LLM uses underscores when writing code-exec calls — matching curate-executor pattern.
@@ -273,7 +273,15 @@ ${responseFormat}`;
273
273
  mtime: f.modified?.getTime() ?? 0,
274
274
  path: f.path,
275
275
  }));
276
- const fingerprint = QueryResultCache.computeFingerprint(files);
276
+ // Include .abstract.md siblings so newly-written abstracts invalidate the cache.
277
+ // They are excluded by isDerivedArtifact() above, so we append them separately.
278
+ const abstractFiles = globResult.files
279
+ .filter((f) => f.path.endsWith(ABSTRACT_EXTENSION))
280
+ .map((f) => ({
281
+ mtime: f.modified?.getTime() ?? 0,
282
+ path: f.path,
283
+ }));
284
+ const fingerprint = QueryResultCache.computeFingerprint([...files, ...abstractFiles]);
277
285
  this.cachedFingerprint = {
278
286
  expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
279
287
  value: fingerprint,
@@ -1,3 +1,4 @@
1
+ import { readFile } from 'node:fs/promises';
1
2
  import { join } from 'node:path';
2
3
  import { StatusEvents } from '../../../../shared/transport/events/status-events.js';
3
4
  import { BRV_DIR, CONTEXT_TREE_DIR } from '../../../constants.js';
@@ -65,6 +66,15 @@ export class StatusHandler {
65
66
  }
66
67
  }
67
68
  catch { }
69
+ // Abstract generation queue status (written by agent process via abstract-queue.ts)
70
+ try {
71
+ const queueStatusPath = join(projectPath, BRV_DIR, '_queue_status.json');
72
+ const raw = await readFile(queueStatusPath, 'utf8');
73
+ result.abstractQueue = JSON.parse(raw);
74
+ }
75
+ catch {
76
+ // File doesn't exist yet — no queue running
77
+ }
68
78
  // Context tree status
69
79
  try {
70
80
  const contextTreeExists = await this.contextTreeService.exists(projectPath);
@@ -98,22 +98,22 @@ export declare const CurateResultSchema: z.ZodObject<{
98
98
  failed?: number | undefined;
99
99
  deleted?: number | undefined;
100
100
  updated?: number | undefined;
101
- added?: number | undefined;
102
101
  merged?: number | undefined;
102
+ added?: number | undefined;
103
103
  }, {
104
104
  failed?: number | undefined;
105
105
  deleted?: number | undefined;
106
106
  updated?: number | undefined;
107
- added?: number | undefined;
108
107
  merged?: number | undefined;
108
+ added?: number | undefined;
109
109
  }>>;
110
110
  }, "strip", z.ZodTypeAny, {
111
111
  summary?: {
112
112
  failed?: number | undefined;
113
113
  deleted?: number | undefined;
114
114
  updated?: number | undefined;
115
- added?: number | undefined;
116
115
  merged?: number | undefined;
116
+ added?: number | undefined;
117
117
  } | undefined;
118
118
  applied?: {
119
119
  path: string;
@@ -135,8 +135,8 @@ export declare const CurateResultSchema: z.ZodObject<{
135
135
  failed?: number | undefined;
136
136
  deleted?: number | undefined;
137
137
  updated?: number | undefined;
138
- added?: number | undefined;
139
138
  merged?: number | undefined;
139
+ added?: number | undefined;
140
140
  } | undefined;
141
141
  applied?: {
142
142
  path: string;
@@ -114,6 +114,13 @@ export interface ProjectLocationDTO {
114
114
  projectPath: string;
115
115
  }
116
116
  export interface StatusDTO {
117
+ /** Current state of the background abstract generation queue, if active */
118
+ abstractQueue?: {
119
+ failed: number;
120
+ pending: number;
121
+ processed: number;
122
+ processing: boolean;
123
+ };
117
124
  authStatus: 'expired' | 'logged_in' | 'not_logged_in' | 'unknown';
118
125
  contextTreeChanges?: ContextTreeChanges;
119
126
  /** Absolute path to the context tree directory (e.g., '/Users/foo/project/.brv/context-tree') */