byterover-cli 3.5.1 → 3.6.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 -6
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.js +1 -0
- package/dist/oclif/commands/curate/view.js +5 -25
- package/dist/oclif/commands/dream.d.ts +18 -0
- package/dist/oclif/commands/dream.js +230 -0
- package/dist/oclif/commands/query-log/summary.d.ts +18 -0
- package/dist/oclif/commands/query-log/summary.js +75 -0
- package/dist/oclif/commands/query-log/view.d.ts +23 -0
- package/dist/oclif/commands/query-log/view.js +95 -0
- package/dist/oclif/lib/time-filter.d.ts +10 -0
- package/dist/oclif/lib/time-filter.js +21 -0
- package/dist/server/config/environment.d.ts +10 -3
- package/dist/server/config/environment.js +34 -15
- package/dist/server/constants.d.ts +5 -0
- package/dist/server/constants.js +7 -0
- package/dist/server/core/domain/entities/query-log-entry.d.ts +61 -0
- package/dist/server/core/domain/entities/query-log-entry.js +40 -0
- package/dist/server/core/domain/transport/schemas.d.ts +108 -7
- package/dist/server/core/domain/transport/schemas.js +34 -2
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +23 -2
- package/dist/server/core/interfaces/i-terminal.d.ts +3 -0
- package/dist/server/core/interfaces/i-terminal.js +1 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.d.ts +23 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.js +2 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.d.ts +44 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.js +1 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.d.ts +13 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.js +3 -0
- package/dist/server/infra/daemon/agent-process.js +79 -9
- package/dist/server/infra/daemon/brv-server.js +74 -5
- package/dist/server/infra/dream/dream-lock-service.d.ts +37 -0
- package/dist/server/infra/dream/dream-lock-service.js +88 -0
- package/dist/server/infra/dream/dream-log-schema.d.ts +966 -0
- package/dist/server/infra/dream/dream-log-schema.js +57 -0
- package/dist/server/infra/dream/dream-log-store.d.ts +55 -0
- package/dist/server/infra/dream/dream-log-store.js +141 -0
- package/dist/server/infra/dream/dream-response-schemas.d.ts +219 -0
- package/dist/server/infra/dream/dream-response-schemas.js +38 -0
- package/dist/server/infra/dream/dream-state-schema.d.ts +67 -0
- package/dist/server/infra/dream/dream-state-schema.js +23 -0
- package/dist/server/infra/dream/dream-state-service.d.ts +38 -0
- package/dist/server/infra/dream/dream-state-service.js +91 -0
- package/dist/server/infra/dream/dream-trigger.d.ts +46 -0
- package/dist/server/infra/dream/dream-trigger.js +65 -0
- package/dist/server/infra/dream/dream-undo.d.ts +38 -0
- package/dist/server/infra/dream/dream-undo.js +293 -0
- package/dist/server/infra/dream/operations/consolidate.d.ts +52 -0
- package/dist/server/infra/dream/operations/consolidate.js +514 -0
- package/dist/server/infra/dream/operations/prune.d.ts +45 -0
- package/dist/server/infra/dream/operations/prune.js +362 -0
- package/dist/server/infra/dream/operations/synthesize.d.ts +37 -0
- package/dist/server/infra/dream/operations/synthesize.js +278 -0
- package/dist/server/infra/dream/parse-dream-response.d.ts +11 -0
- package/dist/server/infra/dream/parse-dream-response.js +35 -0
- package/dist/server/infra/executor/curate-executor.js +10 -0
- package/dist/server/infra/executor/dream-executor.d.ts +97 -0
- package/dist/server/infra/executor/dream-executor.js +431 -0
- package/dist/server/infra/executor/query-executor.d.ts +2 -2
- package/dist/server/infra/executor/query-executor.js +92 -22
- package/dist/server/infra/process/feature-handlers.js +10 -6
- package/dist/server/infra/process/query-log-handler.d.ts +42 -0
- package/dist/server/infra/process/query-log-handler.js +150 -0
- package/dist/server/infra/process/task-router.d.ts +40 -0
- package/dist/server/infra/process/task-router.js +67 -9
- package/dist/server/infra/process/transport-handlers.d.ts +4 -0
- package/dist/server/infra/process/transport-handlers.js +1 -0
- package/dist/server/infra/storage/file-curate-log-store.js +1 -1
- package/dist/server/infra/storage/file-query-log-store.d.ts +81 -0
- package/dist/server/infra/storage/file-query-log-store.js +249 -0
- package/dist/server/infra/transport/handlers/config-handler.js +1 -1
- package/dist/server/infra/usecase/curate-log-use-case.js +7 -3
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.d.ts +15 -0
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.js +79 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.d.ts +13 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.js +217 -0
- package/dist/server/infra/usecase/query-log-use-case.d.ts +31 -0
- package/dist/server/infra/usecase/query-log-use-case.js +128 -0
- package/dist/server/utils/log-format-utils.d.ts +5 -0
- package/dist/server/utils/log-format-utils.js +23 -0
- package/dist/shared/transport/events/config-events.d.ts +1 -1
- package/oclif.manifest.json +258 -3
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract and validate a JSON response from LLM output.
|
|
3
|
+
*
|
|
4
|
+
* Tries two strategies in order:
|
|
5
|
+
* 1. JSON inside a ```json code fence (first match, non-greedy)
|
|
6
|
+
* 2. Raw JSON (first { to last })
|
|
7
|
+
*
|
|
8
|
+
* Returns null if no valid JSON matching the schema is found.
|
|
9
|
+
*/
|
|
10
|
+
export function parseDreamResponse(response, schema) {
|
|
11
|
+
// Strategy 1: JSON in code fence (labeled ```json or plain ```)
|
|
12
|
+
const fenceMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
13
|
+
if (fenceMatch) {
|
|
14
|
+
const result = tryParse(fenceMatch[1], schema);
|
|
15
|
+
if (result !== null)
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
// Strategy 2: Raw JSON (first { to last })
|
|
19
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
20
|
+
if (jsonMatch) {
|
|
21
|
+
const result = tryParse(jsonMatch[0], schema);
|
|
22
|
+
if (result !== null)
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function tryParse(raw, schema) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return schema.parse(parsed);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { BRV_DIR } from '../../constants.js';
|
|
2
3
|
import { FileValidationError } from '../../core/domain/errors/task-error.js';
|
|
3
4
|
import { createFileContentReader, } from '../../utils/file-content-reader.js';
|
|
4
5
|
import { validateFileForCurate } from '../../utils/file-validator.js';
|
|
@@ -6,6 +7,7 @@ import { FileContextTreeManifestService } from '../context-tree/file-context-tre
|
|
|
6
7
|
import { FileContextTreeSnapshotService } from '../context-tree/file-context-tree-snapshot-service.js';
|
|
7
8
|
import { FileContextTreeSummaryService } from '../context-tree/file-context-tree-summary-service.js';
|
|
8
9
|
import { diffStates } from '../context-tree/snapshot-diff.js';
|
|
10
|
+
import { DreamStateService } from '../dream/dream-state-service.js';
|
|
9
11
|
import { PreCompactionService } from './pre-compaction/pre-compaction-service.js';
|
|
10
12
|
/**
|
|
11
13
|
* CurateExecutor - Executes curate tasks with an injected CipherAgent.
|
|
@@ -126,6 +128,14 @@ export class CurateExecutor {
|
|
|
126
128
|
// Fail-open: summary/manifest errors never block curation
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
// Increment dream curation counter (fail-open: non-critical for curation)
|
|
132
|
+
try {
|
|
133
|
+
const dreamStateService = new DreamStateService({ baseDir: path.join(baseDir, BRV_DIR) });
|
|
134
|
+
await dreamStateService.incrementCurationCount();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Dream state tracking is non-critical — don't block curation
|
|
138
|
+
}
|
|
129
139
|
await agent.drainBackgroundWork?.();
|
|
130
140
|
return response;
|
|
131
141
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DreamExecutor - Orchestrates background memory consolidation ("dreaming").
|
|
3
|
+
*
|
|
4
|
+
* 8-step flow:
|
|
5
|
+
* 1. Capture pre-state snapshot
|
|
6
|
+
* 2. Load dream state
|
|
7
|
+
* 3. Find changed files since last dream (via curate log scanning)
|
|
8
|
+
* 4. Run operations (consolidate, synthesize, prune)
|
|
9
|
+
* 5. Post-dream propagation (staleness + manifest rebuild)
|
|
10
|
+
* 6. Write dream log
|
|
11
|
+
* 7. Update dream state
|
|
12
|
+
* 8. Release lock (in finally block)
|
|
13
|
+
*
|
|
14
|
+
* Lock lifecycle: caller acquires lock via DreamTrigger; this executor releases on
|
|
15
|
+
* success or rolls back on error so the time gate isn't fooled.
|
|
16
|
+
*/
|
|
17
|
+
import type { ICipherAgent } from '../../../agent/core/interfaces/i-cipher-agent.js';
|
|
18
|
+
import type { CurateLogEntry } from '../../core/domain/entities/curate-log-entry.js';
|
|
19
|
+
import type { CurateLogStatus } from '../../core/interfaces/storage/i-curate-log-store.js';
|
|
20
|
+
import type { DreamLogEntry, DreamOperation } from '../dream/dream-log-schema.js';
|
|
21
|
+
import { type ConsolidateDeps } from '../dream/operations/consolidate.js';
|
|
22
|
+
export type DreamExecutorDeps = {
|
|
23
|
+
archiveService: {
|
|
24
|
+
archiveEntry(relativePath: string, agent: ICipherAgent, directory?: string): Promise<{
|
|
25
|
+
fullPath: string;
|
|
26
|
+
originalPath: string;
|
|
27
|
+
stubPath: string;
|
|
28
|
+
}>;
|
|
29
|
+
findArchiveCandidates(directory?: string): Promise<string[]>;
|
|
30
|
+
};
|
|
31
|
+
curateLogStore: {
|
|
32
|
+
getNextId(): Promise<string>;
|
|
33
|
+
list(filters?: {
|
|
34
|
+
after?: number;
|
|
35
|
+
before?: number;
|
|
36
|
+
limit?: number;
|
|
37
|
+
status?: CurateLogStatus[];
|
|
38
|
+
}): Promise<CurateLogEntry[]>;
|
|
39
|
+
save(entry: CurateLogEntry): Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
dreamLockService: {
|
|
42
|
+
release(): Promise<void>;
|
|
43
|
+
rollback(priorMtime: number): Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
dreamLogStore: {
|
|
46
|
+
getNextId(): Promise<string>;
|
|
47
|
+
save(entry: DreamLogEntry): Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
dreamStateService: {
|
|
50
|
+
read(): Promise<import('../dream/dream-state-schema.js').DreamState>;
|
|
51
|
+
update(updater: (state: import('../dream/dream-state-schema.js').DreamState) => import('../dream/dream-state-schema.js').DreamState): Promise<import('../dream/dream-state-schema.js').DreamState>;
|
|
52
|
+
write(state: import('../dream/dream-state-schema.js').DreamState): Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
reviewBackupStore?: {
|
|
55
|
+
save(relativePath: string, content: string): Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
searchService: ConsolidateDeps['searchService'];
|
|
58
|
+
};
|
|
59
|
+
type DreamExecuteOptions = {
|
|
60
|
+
priorMtime: number;
|
|
61
|
+
projectRoot: string;
|
|
62
|
+
taskId: string;
|
|
63
|
+
trigger: 'agent-idle' | 'cli' | 'manual';
|
|
64
|
+
};
|
|
65
|
+
export declare class DreamExecutor {
|
|
66
|
+
private readonly deps;
|
|
67
|
+
constructor(deps: DreamExecutorDeps);
|
|
68
|
+
executeWithAgent(agent: ICipherAgent, options: DreamExecuteOptions): Promise<{
|
|
69
|
+
logId: string;
|
|
70
|
+
result: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Runs the three dream operations sequentially, pushing results into `out` after
|
|
74
|
+
* each step. Extracted so the executor can preserve partial work when a later step
|
|
75
|
+
* throws — and so tests can inject controlled ops without a full LLM round-trip.
|
|
76
|
+
*/
|
|
77
|
+
protected runOperations(args: {
|
|
78
|
+
agent: ICipherAgent;
|
|
79
|
+
changedFiles: Set<string>;
|
|
80
|
+
contextTreeDir: string;
|
|
81
|
+
logId: string;
|
|
82
|
+
out: DreamOperation[];
|
|
83
|
+
projectRoot: string;
|
|
84
|
+
signal: AbortSignal;
|
|
85
|
+
taskId: string;
|
|
86
|
+
}): Promise<void>;
|
|
87
|
+
/** Errors are tracked at the log level (status='error'), not per-operation — always 0 here. */
|
|
88
|
+
private computeSummary;
|
|
89
|
+
/**
|
|
90
|
+
* Dual-write: create curate log entries for dream operations that need human review.
|
|
91
|
+
* This surfaces them in `brv review pending` without modifying the review system.
|
|
92
|
+
*/
|
|
93
|
+
private createReviewEntries;
|
|
94
|
+
private findChangedFilesSinceLastDream;
|
|
95
|
+
private formatResult;
|
|
96
|
+
}
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DreamExecutor - Orchestrates background memory consolidation ("dreaming").
|
|
3
|
+
*
|
|
4
|
+
* 8-step flow:
|
|
5
|
+
* 1. Capture pre-state snapshot
|
|
6
|
+
* 2. Load dream state
|
|
7
|
+
* 3. Find changed files since last dream (via curate log scanning)
|
|
8
|
+
* 4. Run operations (consolidate, synthesize, prune)
|
|
9
|
+
* 5. Post-dream propagation (staleness + manifest rebuild)
|
|
10
|
+
* 6. Write dream log
|
|
11
|
+
* 7. Update dream state
|
|
12
|
+
* 8. Release lock (in finally block)
|
|
13
|
+
*
|
|
14
|
+
* Lock lifecycle: caller acquires lock via DreamTrigger; this executor releases on
|
|
15
|
+
* success or rolls back on error so the time gate isn't fooled.
|
|
16
|
+
*/
|
|
17
|
+
import { access } from 'node:fs/promises';
|
|
18
|
+
import { isAbsolute, join, sep } from 'node:path';
|
|
19
|
+
import { BRV_DIR, CONTEXT_TREE_DIR } from '../../constants.js';
|
|
20
|
+
import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
|
|
21
|
+
import { FileContextTreeSnapshotService } from '../context-tree/file-context-tree-snapshot-service.js';
|
|
22
|
+
import { FileContextTreeSummaryService } from '../context-tree/file-context-tree-summary-service.js';
|
|
23
|
+
import { diffStates } from '../context-tree/snapshot-diff.js';
|
|
24
|
+
import { consolidate } from '../dream/operations/consolidate.js';
|
|
25
|
+
import { prune } from '../dream/operations/prune.js';
|
|
26
|
+
import { synthesize } from '../dream/operations/synthesize.js';
|
|
27
|
+
const DREAM_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
28
|
+
export class DreamExecutor {
|
|
29
|
+
deps;
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
async executeWithAgent(agent, options) {
|
|
34
|
+
const { priorMtime, projectRoot, trigger } = options;
|
|
35
|
+
const contextTreeDir = join(projectRoot, BRV_DIR, CONTEXT_TREE_DIR);
|
|
36
|
+
// Timeout budget
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeout = setTimeout(() => controller.abort(), DREAM_TIMEOUT_MS);
|
|
39
|
+
const logId = await this.deps.dreamLogStore.getNextId();
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const zeroes = { consolidated: 0, errors: 0, flaggedForReview: 0, pruned: 0, synthesized: 0 };
|
|
42
|
+
// Save initial processing entry
|
|
43
|
+
const processingEntry = {
|
|
44
|
+
id: logId,
|
|
45
|
+
operations: [],
|
|
46
|
+
startedAt,
|
|
47
|
+
status: 'processing',
|
|
48
|
+
summary: zeroes,
|
|
49
|
+
taskId: options.taskId,
|
|
50
|
+
trigger,
|
|
51
|
+
};
|
|
52
|
+
await this.deps.dreamLogStore.save(processingEntry);
|
|
53
|
+
// Hoisted so the catch block can surface any work that completed before a
|
|
54
|
+
// timeout or error — keeps the dream log audit trail and `brv dream --undo`
|
|
55
|
+
// history accurate for partial runs.
|
|
56
|
+
const allOperations = [];
|
|
57
|
+
let succeeded = false;
|
|
58
|
+
// Tracks whether the success-path createReviewEntries already ran. The
|
|
59
|
+
// catch path also calls createReviewEntries for partial runs; without this
|
|
60
|
+
// flag, a failure that occurs after step 6b succeeds (e.g. step 7
|
|
61
|
+
// dreamStateService.update throws) would re-write the same review entries.
|
|
62
|
+
let reviewEntriesWritten = false;
|
|
63
|
+
try {
|
|
64
|
+
// Step 1: Capture pre-state
|
|
65
|
+
const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: projectRoot });
|
|
66
|
+
let preState;
|
|
67
|
+
try {
|
|
68
|
+
preState = await snapshotService.getCurrentState(projectRoot);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Fail-open: if snapshot fails, skip propagation
|
|
72
|
+
}
|
|
73
|
+
// Step 2: Load dream state
|
|
74
|
+
const dreamState = await this.deps.dreamStateService.read();
|
|
75
|
+
// Step 3: Find changed files since last dream
|
|
76
|
+
const changedFiles = await this.findChangedFilesSinceLastDream(dreamState.lastDreamAt, contextTreeDir);
|
|
77
|
+
// Step 4: Run operations, pushing results incrementally so partial work
|
|
78
|
+
// is preserved if a later step throws or the budget aborts.
|
|
79
|
+
await this.runOperations({
|
|
80
|
+
agent,
|
|
81
|
+
changedFiles,
|
|
82
|
+
contextTreeDir,
|
|
83
|
+
logId,
|
|
84
|
+
out: allOperations,
|
|
85
|
+
projectRoot,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
taskId: options.taskId,
|
|
88
|
+
});
|
|
89
|
+
// Step 5: Post-dream propagation (fail-open)
|
|
90
|
+
if (preState) {
|
|
91
|
+
try {
|
|
92
|
+
const postState = await snapshotService.getCurrentState(projectRoot);
|
|
93
|
+
const changedPaths = diffStates(preState, postState);
|
|
94
|
+
if (changedPaths.length > 0) {
|
|
95
|
+
const summaryService = new FileContextTreeSummaryService();
|
|
96
|
+
await summaryService.propagateStaleness(changedPaths, agent, projectRoot);
|
|
97
|
+
const manifestService = new FileContextTreeManifestService({ baseDirectory: projectRoot });
|
|
98
|
+
await manifestService.buildManifest(projectRoot);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Fail-open: propagation errors never block dream
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Step 6: Write dream log
|
|
106
|
+
const summary = this.computeSummary(allOperations);
|
|
107
|
+
const completedEntry = {
|
|
108
|
+
completedAt: Date.now(),
|
|
109
|
+
id: logId,
|
|
110
|
+
operations: allOperations,
|
|
111
|
+
startedAt,
|
|
112
|
+
status: 'completed',
|
|
113
|
+
summary,
|
|
114
|
+
taskId: options.taskId,
|
|
115
|
+
trigger,
|
|
116
|
+
};
|
|
117
|
+
await this.deps.dreamLogStore.save(completedEntry);
|
|
118
|
+
// Step 6b: Create curate log entries for needsReview operations (dual-write for review system).
|
|
119
|
+
// Runs after the completed dream log is durably written so review tasks never outlive their dream log.
|
|
120
|
+
await this.createReviewEntries(allOperations, contextTreeDir, options.taskId);
|
|
121
|
+
reviewEntriesWritten = true;
|
|
122
|
+
// Step 7: Update dream state — atomic RMW under the per-file mutex so a
|
|
123
|
+
// concurrent curate's incrementCurationCount can't be overwritten by the
|
|
124
|
+
// reset, and so pendingMerges written by prune are preserved by the spread.
|
|
125
|
+
await this.deps.dreamStateService.update((state) => ({
|
|
126
|
+
...state,
|
|
127
|
+
curationsSinceDream: 0,
|
|
128
|
+
lastDreamAt: new Date().toISOString(),
|
|
129
|
+
lastDreamLogId: logId,
|
|
130
|
+
totalDreams: state.totalDreams + 1,
|
|
131
|
+
}));
|
|
132
|
+
succeeded = true;
|
|
133
|
+
return { logId, result: this.formatResult(logId, summary) };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// Save error/partial log entry (best-effort). Use allOperations so any work
|
|
137
|
+
// that completed before the failure is captured — keeps the audit trail and
|
|
138
|
+
// undo history accurate even for partial runs.
|
|
139
|
+
const summary = this.computeSummary(allOperations);
|
|
140
|
+
if (controller.signal.aborted) {
|
|
141
|
+
const partialEntry = {
|
|
142
|
+
abortReason: 'Budget exceeded (5 min)',
|
|
143
|
+
completedAt: Date.now(),
|
|
144
|
+
id: logId,
|
|
145
|
+
operations: allOperations,
|
|
146
|
+
startedAt,
|
|
147
|
+
status: 'partial',
|
|
148
|
+
summary,
|
|
149
|
+
taskId: options.taskId,
|
|
150
|
+
trigger,
|
|
151
|
+
};
|
|
152
|
+
await this.deps.dreamLogStore.save(partialEntry).catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const errorEntry = {
|
|
156
|
+
completedAt: Date.now(),
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
id: logId,
|
|
159
|
+
operations: allOperations,
|
|
160
|
+
startedAt,
|
|
161
|
+
status: 'error',
|
|
162
|
+
summary,
|
|
163
|
+
taskId: options.taskId,
|
|
164
|
+
trigger,
|
|
165
|
+
};
|
|
166
|
+
await this.deps.dreamLogStore.save(errorEntry).catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
// Surface review-flagged ops that did complete into `brv review pending` even
|
|
169
|
+
// when the dream failed overall. Skipped when no work accumulated so the
|
|
170
|
+
// "no dream log, no review entries" invariant holds for errors that fire
|
|
171
|
+
// before any operation ran. Also skipped when the success-path call
|
|
172
|
+
// already wrote the entries (i.e. step 7 threw after step 6b succeeded)
|
|
173
|
+
// to prevent duplicate review items.
|
|
174
|
+
if (allOperations.length > 0 && !reviewEntriesWritten) {
|
|
175
|
+
await this.createReviewEntries(allOperations, contextTreeDir, options.taskId);
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
// Step 8: Lock management — release on success, rollback on error
|
|
182
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
183
|
+
if (succeeded) {
|
|
184
|
+
await this.deps.dreamLockService.release().catch(() => { });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await this.deps.dreamLockService.rollback(priorMtime).catch(() => { });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Runs the three dream operations sequentially, pushing results into `out` after
|
|
193
|
+
* each step. Extracted so the executor can preserve partial work when a later step
|
|
194
|
+
* throws — and so tests can inject controlled ops without a full LLM round-trip.
|
|
195
|
+
*/
|
|
196
|
+
async runOperations(args) {
|
|
197
|
+
const { agent, changedFiles, contextTreeDir, logId, out, projectRoot, signal, taskId } = args;
|
|
198
|
+
out.push(...(await consolidate([...changedFiles], {
|
|
199
|
+
agent,
|
|
200
|
+
contextTreeDir,
|
|
201
|
+
dreamStateService: this.deps.dreamStateService,
|
|
202
|
+
reviewBackupStore: this.deps.reviewBackupStore,
|
|
203
|
+
searchService: this.deps.searchService,
|
|
204
|
+
signal,
|
|
205
|
+
taskId,
|
|
206
|
+
})));
|
|
207
|
+
if (changedFiles.size > 0) {
|
|
208
|
+
out.push(...(await synthesize({
|
|
209
|
+
agent,
|
|
210
|
+
contextTreeDir,
|
|
211
|
+
searchService: this.deps.searchService,
|
|
212
|
+
signal,
|
|
213
|
+
taskId,
|
|
214
|
+
})));
|
|
215
|
+
}
|
|
216
|
+
out.push(...(await prune({
|
|
217
|
+
agent,
|
|
218
|
+
archiveService: this.deps.archiveService,
|
|
219
|
+
contextTreeDir,
|
|
220
|
+
dreamLogId: logId,
|
|
221
|
+
dreamStateService: this.deps.dreamStateService,
|
|
222
|
+
projectRoot,
|
|
223
|
+
reviewBackupStore: this.deps.reviewBackupStore,
|
|
224
|
+
signal,
|
|
225
|
+
taskId,
|
|
226
|
+
})));
|
|
227
|
+
}
|
|
228
|
+
/** Errors are tracked at the log level (status='error'), not per-operation — always 0 here. */
|
|
229
|
+
computeSummary(operations) {
|
|
230
|
+
const summary = { consolidated: 0, errors: 0, flaggedForReview: 0, pruned: 0, synthesized: 0 };
|
|
231
|
+
for (const op of operations) {
|
|
232
|
+
if (op.type === 'CONSOLIDATE')
|
|
233
|
+
summary.consolidated++;
|
|
234
|
+
if (op.type === 'SYNTHESIZE')
|
|
235
|
+
summary.synthesized++;
|
|
236
|
+
if (op.type === 'PRUNE')
|
|
237
|
+
summary.pruned++;
|
|
238
|
+
if (op.needsReview)
|
|
239
|
+
summary.flaggedForReview++;
|
|
240
|
+
}
|
|
241
|
+
return summary;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Dual-write: create curate log entries for dream operations that need human review.
|
|
245
|
+
* This surfaces them in `brv review pending` without modifying the review system.
|
|
246
|
+
*/
|
|
247
|
+
async createReviewEntries(operations, contextTreeDir, taskId) {
|
|
248
|
+
const reviewOps = operations.filter((op) => op.needsReview);
|
|
249
|
+
if (reviewOps.length === 0)
|
|
250
|
+
return;
|
|
251
|
+
const curateOps = reviewOps.map((op) => mapDreamOpToCurateOp(op, contextTreeDir));
|
|
252
|
+
try {
|
|
253
|
+
const logId = await this.deps.curateLogStore.getNextId();
|
|
254
|
+
const entry = {
|
|
255
|
+
completedAt: Date.now(),
|
|
256
|
+
id: logId,
|
|
257
|
+
input: { context: 'dream' },
|
|
258
|
+
operations: curateOps,
|
|
259
|
+
startedAt: Date.now(),
|
|
260
|
+
status: 'completed',
|
|
261
|
+
summary: {
|
|
262
|
+
added: curateOps.filter((op) => op.type === 'ADD').length,
|
|
263
|
+
deleted: curateOps.filter((op) => op.type === 'DELETE').length,
|
|
264
|
+
failed: 0,
|
|
265
|
+
merged: curateOps.filter((op) => op.type === 'MERGE').length,
|
|
266
|
+
updated: curateOps.filter((op) => op.type === 'UPDATE' || op.type === 'UPSERT').length,
|
|
267
|
+
},
|
|
268
|
+
taskId,
|
|
269
|
+
};
|
|
270
|
+
await this.deps.curateLogStore.save(entry);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Fail-open: review entry creation must not block dream
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async findChangedFilesSinceLastDream(lastDreamAt, contextTreeDir) {
|
|
277
|
+
// First dream (lastDreamAt=null): scan ALL curate logs — every curation happened "since never"
|
|
278
|
+
const afterTimestamp = lastDreamAt ? new Date(lastDreamAt).getTime() : 0;
|
|
279
|
+
const recentLogs = await this.deps.curateLogStore.list({
|
|
280
|
+
after: afterTimestamp,
|
|
281
|
+
status: ['completed'],
|
|
282
|
+
});
|
|
283
|
+
const changedFiles = new Set();
|
|
284
|
+
for (const log of recentLogs) {
|
|
285
|
+
if (log.input.context === 'dream')
|
|
286
|
+
continue;
|
|
287
|
+
for (const op of log.operations ?? []) {
|
|
288
|
+
// op.filePath is absolute; convert to relative for context tree operations
|
|
289
|
+
if (op.filePath) {
|
|
290
|
+
const relative = toContextTreeRelative(op.filePath, contextTreeDir);
|
|
291
|
+
if (relative)
|
|
292
|
+
changedFiles.add(relative);
|
|
293
|
+
}
|
|
294
|
+
if (op.additionalFilePaths) {
|
|
295
|
+
for (const p of op.additionalFilePaths) {
|
|
296
|
+
const relative = toContextTreeRelative(p, contextTreeDir);
|
|
297
|
+
if (relative)
|
|
298
|
+
changedFiles.add(relative);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Filter to files that still exist (concurrent with Promise.all to avoid no-await-in-loop)
|
|
304
|
+
const checks = [...changedFiles].map(async (file) => {
|
|
305
|
+
try {
|
|
306
|
+
await access(join(contextTreeDir, file));
|
|
307
|
+
return file;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
const results = await Promise.all(checks);
|
|
314
|
+
return new Set(results.filter((f) => f !== null));
|
|
315
|
+
}
|
|
316
|
+
formatResult(logId, summary) {
|
|
317
|
+
const parts = [`Dream completed (${logId})`];
|
|
318
|
+
const counts = [
|
|
319
|
+
summary.consolidated > 0 ? `${summary.consolidated} consolidated` : '',
|
|
320
|
+
summary.synthesized > 0 ? `${summary.synthesized} synthesized` : '',
|
|
321
|
+
summary.pruned > 0 ? `${summary.pruned} pruned` : '',
|
|
322
|
+
].filter(Boolean);
|
|
323
|
+
if (counts.length > 0) {
|
|
324
|
+
parts.push(counts.join(' | '));
|
|
325
|
+
}
|
|
326
|
+
else if (summary.errors === 0 && summary.flaggedForReview === 0) {
|
|
327
|
+
parts.push('No changes needed — context tree is up to date');
|
|
328
|
+
}
|
|
329
|
+
if (summary.errors > 0) {
|
|
330
|
+
parts.push(`${summary.errors} operations failed`);
|
|
331
|
+
}
|
|
332
|
+
if (summary.flaggedForReview > 0) {
|
|
333
|
+
parts.push(`${summary.flaggedForReview} operations flagged for review`);
|
|
334
|
+
}
|
|
335
|
+
return parts.join('\n');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Map a dream operation to a curate log operation for the review system. */
|
|
339
|
+
function mapDreamOpToCurateOp(op, contextTreeDir) {
|
|
340
|
+
if (op.type === 'PRUNE' && op.action === 'ARCHIVE') {
|
|
341
|
+
return {
|
|
342
|
+
filePath: join(contextTreeDir, op.file),
|
|
343
|
+
needsReview: true,
|
|
344
|
+
path: op.file,
|
|
345
|
+
reason: `[dream/prune] ${op.reason}`,
|
|
346
|
+
reviewStatus: 'pending',
|
|
347
|
+
status: 'success',
|
|
348
|
+
type: 'DELETE',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (op.type === 'CONSOLIDATE' && op.action === 'MERGE') {
|
|
352
|
+
return {
|
|
353
|
+
additionalFilePaths: op.inputFiles.filter((f) => f !== op.outputFile).map((f) => join(contextTreeDir, f)),
|
|
354
|
+
filePath: op.outputFile ? join(contextTreeDir, op.outputFile) : undefined,
|
|
355
|
+
needsReview: true,
|
|
356
|
+
path: op.outputFile ?? op.inputFiles[0],
|
|
357
|
+
reason: `[dream/consolidate] ${op.reason}`,
|
|
358
|
+
reviewStatus: 'pending',
|
|
359
|
+
status: 'success',
|
|
360
|
+
type: 'MERGE',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (op.type === 'CONSOLIDATE' && op.action === 'TEMPORAL_UPDATE') {
|
|
364
|
+
const targetFile = op.inputFiles[0];
|
|
365
|
+
return {
|
|
366
|
+
filePath: join(contextTreeDir, targetFile),
|
|
367
|
+
needsReview: true,
|
|
368
|
+
path: targetFile,
|
|
369
|
+
reason: `[dream/consolidate] ${op.reason}`,
|
|
370
|
+
reviewStatus: 'pending',
|
|
371
|
+
status: 'success',
|
|
372
|
+
type: 'UPDATE',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (op.type === 'CONSOLIDATE' && op.action === 'CROSS_REFERENCE') {
|
|
376
|
+
const [targetFile, ...relatedFiles] = op.inputFiles;
|
|
377
|
+
return {
|
|
378
|
+
additionalFilePaths: relatedFiles.map((file) => join(contextTreeDir, file)),
|
|
379
|
+
filePath: join(contextTreeDir, targetFile),
|
|
380
|
+
needsReview: true,
|
|
381
|
+
path: targetFile,
|
|
382
|
+
reason: `[dream/consolidate] ${op.reason}`,
|
|
383
|
+
reviewStatus: 'pending',
|
|
384
|
+
status: 'success',
|
|
385
|
+
type: 'UPDATE',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (op.type === 'SYNTHESIZE' && op.action === 'CREATE') {
|
|
389
|
+
return {
|
|
390
|
+
filePath: join(contextTreeDir, op.outputFile),
|
|
391
|
+
needsReview: true,
|
|
392
|
+
path: op.outputFile,
|
|
393
|
+
reason: '[dream/synthesize] Generated synthesis draft',
|
|
394
|
+
reviewStatus: 'pending',
|
|
395
|
+
status: 'success',
|
|
396
|
+
type: 'ADD',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const filePath = 'file' in op
|
|
400
|
+
? op.file
|
|
401
|
+
: 'inputFiles' in op
|
|
402
|
+
? op.outputFile ?? op.inputFiles[0]
|
|
403
|
+
: op.outputFile;
|
|
404
|
+
return {
|
|
405
|
+
filePath: join(contextTreeDir, filePath),
|
|
406
|
+
needsReview: true,
|
|
407
|
+
path: filePath,
|
|
408
|
+
reason: `[dream/${op.type.toLowerCase()}] ${'reason' in op ? op.reason : ''}`,
|
|
409
|
+
reviewStatus: 'pending',
|
|
410
|
+
status: 'success',
|
|
411
|
+
type: 'UPDATE',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/** Convert an absolute file path to a context-tree-relative path, or undefined if not inside the tree. */
|
|
415
|
+
function toContextTreeRelative(absolutePath, contextTreeDir) {
|
|
416
|
+
// Normalize separators for cross-platform (Windows uses backslash)
|
|
417
|
+
const normalized = absolutePath.replaceAll('\\', '/');
|
|
418
|
+
const normalizedDir = contextTreeDir.replaceAll('\\', '/');
|
|
419
|
+
if (normalized.startsWith(normalizedDir + '/')) {
|
|
420
|
+
return normalized.slice(normalizedDir.length + 1);
|
|
421
|
+
}
|
|
422
|
+
// Already relative? Validate it doesn't traverse outside the context tree
|
|
423
|
+
if (!isAbsolute(normalized)) {
|
|
424
|
+
const resolved = join(contextTreeDir, normalized);
|
|
425
|
+
if (resolved.startsWith(contextTreeDir + sep) || resolved.startsWith(contextTreeDir + '/')) {
|
|
426
|
+
return normalized;
|
|
427
|
+
}
|
|
428
|
+
return undefined; // Path traversal attempt (e.g., ../../secret.md)
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ICipherAgent } from '../../../agent/core/interfaces/i-cipher-agent.js';
|
|
2
2
|
import type { IFileSystem } from '../../../agent/core/interfaces/i-file-system.js';
|
|
3
3
|
import type { ISearchKnowledgeService } from '../../../agent/infra/sandbox/tools-sdk.js';
|
|
4
|
-
import type { IQueryExecutor, QueryExecuteOptions } from '../../core/interfaces/executor/i-query-executor.js';
|
|
4
|
+
import type { IQueryExecutor, QueryExecuteOptions, QueryExecutorResult } from '../../core/interfaces/executor/i-query-executor.js';
|
|
5
5
|
/**
|
|
6
6
|
* Optional dependencies for QueryExecutor.
|
|
7
7
|
* All fields are optional — without them, the executor falls back to the original behavior.
|
|
@@ -43,7 +43,7 @@ export declare class QueryExecutor implements IQueryExecutor {
|
|
|
43
43
|
private readonly fileSystem?;
|
|
44
44
|
private readonly searchService?;
|
|
45
45
|
constructor(deps?: QueryExecutorDeps);
|
|
46
|
-
executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<
|
|
46
|
+
executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<QueryExecutorResult>;
|
|
47
47
|
/**
|
|
48
48
|
* Build pre-fetched context string from search results for LLM prompt injection.
|
|
49
49
|
* Synchronous — uses already-fetched search results (no additional I/O for excerpts).
|