byterover-cli 3.5.0 → 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/mcp/mcp-server.js +3 -0
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +3 -7
- package/dist/server/infra/mcp/tools/brv-query-tool.js +3 -7
- package/dist/server/infra/mcp/tools/index.d.ts +1 -0
- package/dist/server/infra/mcp/tools/index.js +1 -0
- package/dist/server/infra/mcp/tools/shared-schema.d.ts +3 -0
- package/dist/server/infra/mcp/tools/shared-schema.js +17 -0
- 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 +439 -184
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { connectToTransport } from '@campfirein/brv-transport-client';
|
|
22
22
|
import { randomUUID } from 'node:crypto';
|
|
23
23
|
import { appendFileSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
24
25
|
import { SESSIONS_DIR } from '../../../agent/core/domain/session/session-metadata.js';
|
|
25
26
|
import { CipherAgent } from '../../../agent/infra/agent/index.js';
|
|
26
27
|
import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
|
|
@@ -30,14 +31,22 @@ import { createSearchKnowledgeService } from '../../../agent/infra/tools/impleme
|
|
|
30
31
|
import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
|
|
31
32
|
import { decodeSearchContent } from '../../../shared/transport/search-content.js';
|
|
32
33
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
33
|
-
import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
34
|
+
import { BRV_DIR, DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
34
35
|
import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/errors/task-error.js';
|
|
35
36
|
import { loadSources } from '../../core/domain/source/source-schema.js';
|
|
36
37
|
import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
38
|
+
import { FileContextTreeArchiveService } from '../context-tree/file-context-tree-archive-service.js';
|
|
39
|
+
import { DreamLockService } from '../dream/dream-lock-service.js';
|
|
40
|
+
import { DreamLogStore } from '../dream/dream-log-store.js';
|
|
41
|
+
import { DreamStateService } from '../dream/dream-state-service.js';
|
|
42
|
+
import { DreamTrigger } from '../dream/dream-trigger.js';
|
|
37
43
|
import { CurateExecutor } from '../executor/curate-executor.js';
|
|
44
|
+
import { DreamExecutor } from '../executor/dream-executor.js';
|
|
38
45
|
import { FolderPackExecutor } from '../executor/folder-pack-executor.js';
|
|
39
46
|
import { QueryExecutor } from '../executor/query-executor.js';
|
|
40
47
|
import { SearchExecutor } from '../executor/search-executor.js';
|
|
48
|
+
import { FileCurateLogStore } from '../storage/file-curate-log-store.js';
|
|
49
|
+
import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
|
|
41
50
|
import { AgentInstanceDiscovery } from '../transport/agent-instance-discovery.js';
|
|
42
51
|
import { createAgentLogger } from './agent-logger.js';
|
|
43
52
|
import { resolveSessionId } from './session-resolver.js';
|
|
@@ -198,7 +207,7 @@ async function start() {
|
|
|
198
207
|
const sharedAllowedPaths = (sourcesData?.origins ?? []).map((o) => o.contextTreeRoot);
|
|
199
208
|
const envConfig = getCurrentConfig();
|
|
200
209
|
const agentConfig = {
|
|
201
|
-
apiBaseUrl: envConfig.
|
|
210
|
+
apiBaseUrl: envConfig.llmBaseUrl,
|
|
202
211
|
fileSystem: { allowedPaths: ['.', ...sharedAllowedPaths], workingDirectory: projectPath },
|
|
203
212
|
llm: {
|
|
204
213
|
maxIterations: 10,
|
|
@@ -307,7 +316,7 @@ async function start() {
|
|
|
307
316
|
transport.on(TransportTaskEventNames.EXECUTE, (task) => {
|
|
308
317
|
agentLog(`task:execute received taskId=${task.taskId} type=${task.type} activeTaskCount=${activeTaskCount + 1}`);
|
|
309
318
|
// eslint-disable-next-line no-void
|
|
310
|
-
void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor);
|
|
319
|
+
void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchService, configResult.storagePath);
|
|
311
320
|
});
|
|
312
321
|
// 8. Register with transport server (for TransportHandlers tracking)
|
|
313
322
|
await transport.requestWithAck('agent:register', { projectPath });
|
|
@@ -315,8 +324,8 @@ async function start() {
|
|
|
315
324
|
process.send?.({ clientId, type: 'ready' });
|
|
316
325
|
agentLog('Ready — listening for tasks');
|
|
317
326
|
}
|
|
318
|
-
async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor) {
|
|
319
|
-
const { clientCwd, clientId, content, files, folderPath, taskId, type, worktreeRoot } = task;
|
|
327
|
+
async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchKnowledgeService, storagePath) {
|
|
328
|
+
const { clientCwd, clientId, content, files, folderPath, force, taskId, trigger, type, worktreeRoot } = task;
|
|
320
329
|
if (!transport || !agent)
|
|
321
330
|
return;
|
|
322
331
|
// Search tasks are pure BM25 retrieval — no LLM, no provider needed.
|
|
@@ -385,9 +394,17 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
385
394
|
}
|
|
386
395
|
try {
|
|
387
396
|
let result;
|
|
397
|
+
let logId;
|
|
388
398
|
switch (type) {
|
|
389
399
|
case 'curate': {
|
|
390
|
-
result = await curateExecutor.executeWithAgent(agent, {
|
|
400
|
+
result = await curateExecutor.executeWithAgent(agent, {
|
|
401
|
+
clientCwd,
|
|
402
|
+
content,
|
|
403
|
+
files,
|
|
404
|
+
projectRoot: projectPath,
|
|
405
|
+
taskId,
|
|
406
|
+
worktreeRoot,
|
|
407
|
+
});
|
|
391
408
|
break;
|
|
392
409
|
}
|
|
393
410
|
case 'curate-folder': {
|
|
@@ -401,8 +418,61 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
401
418
|
});
|
|
402
419
|
break;
|
|
403
420
|
}
|
|
421
|
+
case 'dream': {
|
|
422
|
+
const brvDir = join(projectPath, BRV_DIR);
|
|
423
|
+
const dreamLockService = new DreamLockService({ baseDir: brvDir });
|
|
424
|
+
const dreamStateService = new DreamStateService({ baseDir: brvDir });
|
|
425
|
+
// Run trigger check (acquires lock if eligible).
|
|
426
|
+
// Gate 3 (queue) is pre-checked by the daemon (TransportHandlers.preDispatchCheck
|
|
427
|
+
// for CLI dispatch, onAgentIdle for idle-trigger dispatch), so the agent treats
|
|
428
|
+
// its own queue view as empty. Gates 1 (time) and 2 (activity) are re-checked here
|
|
429
|
+
// as defense-in-depth in case state drifted between dispatch and execution.
|
|
430
|
+
const dreamTrigger = new DreamTrigger({
|
|
431
|
+
dreamLockService,
|
|
432
|
+
dreamStateService,
|
|
433
|
+
getQueueLength: () => 0,
|
|
434
|
+
});
|
|
435
|
+
const eligibility = await dreamTrigger.tryStartDream(projectPath, force);
|
|
436
|
+
if (!eligibility.eligible) {
|
|
437
|
+
result = `Dream skipped: ${eligibility.reason}`;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
const dreamExecutor = new DreamExecutor({
|
|
441
|
+
archiveService: new FileContextTreeArchiveService(),
|
|
442
|
+
curateLogStore: new FileCurateLogStore({ baseDir: storagePath }),
|
|
443
|
+
dreamLockService,
|
|
444
|
+
dreamLogStore: new DreamLogStore({ baseDir: brvDir }),
|
|
445
|
+
dreamStateService,
|
|
446
|
+
reviewBackupStore: new FileReviewBackupStore(brvDir),
|
|
447
|
+
searchService: searchKnowledgeService,
|
|
448
|
+
});
|
|
449
|
+
const dreamResult = await dreamExecutor.executeWithAgent(agent, {
|
|
450
|
+
priorMtime: eligibility.priorMtime,
|
|
451
|
+
projectRoot: projectPath,
|
|
452
|
+
taskId,
|
|
453
|
+
trigger: trigger ?? 'cli',
|
|
454
|
+
});
|
|
455
|
+
result = dreamResult.result;
|
|
456
|
+
logId = dreamResult.logId;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
404
459
|
case 'query': {
|
|
405
|
-
|
|
460
|
+
const queryResult = await queryExecutor.executeWithAgent(agent, { query: content, taskId, worktreeRoot });
|
|
461
|
+
result = queryResult.response;
|
|
462
|
+
// Send query metadata to daemon for QueryLogHandler (crosses process boundary via transport).
|
|
463
|
+
// Must arrive BEFORE task:completed so setQueryResult runs before onTaskCompleted.
|
|
464
|
+
try {
|
|
465
|
+
transport.request(TransportTaskEventNames.QUERY_RESULT, {
|
|
466
|
+
matchedDocs: queryResult.matchedDocs,
|
|
467
|
+
searchMetadata: queryResult.searchMetadata,
|
|
468
|
+
taskId,
|
|
469
|
+
tier: queryResult.tier,
|
|
470
|
+
timing: queryResult.timing,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
agentLog(`task:queryResult send failed taskId=${taskId}`);
|
|
475
|
+
}
|
|
406
476
|
break;
|
|
407
477
|
}
|
|
408
478
|
case 'search': {
|
|
@@ -415,7 +485,7 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
415
485
|
// Emit task:completed
|
|
416
486
|
agentLog(`task:completed taskId=${taskId}`);
|
|
417
487
|
try {
|
|
418
|
-
transport.request(TransportTaskEventNames.COMPLETED, { clientId, result, taskId });
|
|
488
|
+
transport.request(TransportTaskEventNames.COMPLETED, { clientId, ...(logId ? { logId } : {}), projectPath, result, taskId });
|
|
419
489
|
}
|
|
420
490
|
catch (error) {
|
|
421
491
|
agentLog(`task:completed send failed taskId=${taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -426,7 +496,7 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
426
496
|
const errorData = serializeTaskError(error);
|
|
427
497
|
agentLog(`task:error taskId=${taskId} error=${errorData.message}`);
|
|
428
498
|
try {
|
|
429
|
-
transport.request(TransportTaskEventNames.ERROR, { clientId, error: errorData, taskId });
|
|
499
|
+
transport.request(TransportTaskEventNames.ERROR, { clientId, error: errorData, projectPath, taskId });
|
|
430
500
|
}
|
|
431
501
|
catch (error_) {
|
|
432
502
|
agentLog(`task:error send failed taskId=${taskId}: ${error_ instanceof Error ? error_.message : String(error_)}`);
|
|
@@ -23,21 +23,25 @@
|
|
|
23
23
|
import { GlobalInstanceManager } from '@campfirein/brv-transport-client';
|
|
24
24
|
import express from 'express';
|
|
25
25
|
import { fork } from 'node:child_process';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
26
27
|
import { mkdirSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
27
28
|
import { dirname, join } from 'node:path';
|
|
28
29
|
import { fileURLToPath } from 'node:url';
|
|
29
30
|
import { ReviewEvents } from '../../../shared/transport/events/review-events.js';
|
|
30
31
|
import { AGENT_IDLE_CHECK_INTERVAL_MS, AGENT_IDLE_TIMEOUT_MS, AGENT_POOL_MAX_SIZE, BRV_DIR, HEARTBEAT_FILE, } from '../../constants.js';
|
|
31
|
-
import { TransportStateEventNames } from '../../core/domain/transport/schemas.js';
|
|
32
|
+
import { TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
32
33
|
import { getGlobalDataDir } from '../../utils/global-data-path.js';
|
|
33
34
|
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
34
35
|
import { crashLog, processLog } from '../../utils/process-logger.js';
|
|
35
36
|
import { ClientManager } from '../client/client-manager.js';
|
|
36
37
|
import { ProjectConfigStore } from '../config/file-config-store.js';
|
|
38
|
+
import { DreamStateService } from '../dream/dream-state-service.js';
|
|
39
|
+
import { DreamTrigger } from '../dream/dream-trigger.js';
|
|
37
40
|
import { createReviewApiRouter } from '../http/review-api-handler.js';
|
|
38
41
|
import { broadcastToProjectRoom } from '../process/broadcast-utils.js';
|
|
39
42
|
import { CurateLogHandler } from '../process/curate-log-handler.js';
|
|
40
43
|
import { setupFeatureHandlers } from '../process/feature-handlers.js';
|
|
44
|
+
import { QueryLogHandler } from '../process/query-log-handler.js';
|
|
41
45
|
import { TransportHandlers } from '../process/transport-handlers.js';
|
|
42
46
|
import { ProjectRegistry } from '../project/project-registry.js';
|
|
43
47
|
import { createProviderOAuthTokenStore } from '../provider-oauth/provider-oauth-token-store.js';
|
|
@@ -188,12 +192,26 @@ async function main() {
|
|
|
188
192
|
log,
|
|
189
193
|
projectRegistry,
|
|
190
194
|
});
|
|
195
|
+
// Shared queue-length resolver — used by both idle timeout policy and dream trigger
|
|
196
|
+
const getQueueLength = (projectPath) => agentPool?.getQueueState().find((q) => q.projectPath === projectPath)?.queueLength ?? 0;
|
|
197
|
+
// Shared dream pre-check trigger factory.
|
|
198
|
+
// The lock service explicitly throws if invoked — gate 4 (lock) is the agent's job;
|
|
199
|
+
// the daemon must only ever evaluate gates 1-3 via checkEligibility().
|
|
200
|
+
const makeDreamPreCheckTrigger = (projectPath) => new DreamTrigger({
|
|
201
|
+
dreamLockService: {
|
|
202
|
+
tryAcquire() {
|
|
203
|
+
throw new Error('Lock must not be acquired during daemon eligibility pre-check');
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
dreamStateService: new DreamStateService({ baseDir: join(projectPath, BRV_DIR) }),
|
|
207
|
+
getQueueLength,
|
|
208
|
+
});
|
|
191
209
|
// Agent idle timeout policy — kills agents after period of inactivity
|
|
192
210
|
const agentIdleTimeoutPolicy = new AgentIdleTimeoutPolicy({
|
|
193
211
|
checkIntervalMs: AGENT_IDLE_CHECK_INTERVAL_MS,
|
|
194
|
-
getQueueLength
|
|
212
|
+
getQueueLength,
|
|
195
213
|
log,
|
|
196
|
-
onAgentIdle(projectPath, queueLength) {
|
|
214
|
+
async onAgentIdle(projectPath, queueLength) {
|
|
197
215
|
// Don't kill agents that have queued tasks waiting
|
|
198
216
|
if (queueLength > 0) {
|
|
199
217
|
log(`Skipping idle cleanup: ${projectPath} has ${queueLength} queued tasks`);
|
|
@@ -205,7 +223,28 @@ async function main() {
|
|
|
205
223
|
log(`Skipping idle cleanup: ${projectPath} has active task`);
|
|
206
224
|
return;
|
|
207
225
|
}
|
|
208
|
-
|
|
226
|
+
// Check dream eligibility before killing (gates 1-3 only, no lock).
|
|
227
|
+
// Lock acquisition happens in the agent process when the dream task executes.
|
|
228
|
+
try {
|
|
229
|
+
const result = await makeDreamPreCheckTrigger(projectPath).checkEligibility(projectPath);
|
|
230
|
+
if (result.eligible) {
|
|
231
|
+
log(`Dream eligible, dispatching dream task: ${projectPath}`);
|
|
232
|
+
agentPool?.submitTask({
|
|
233
|
+
clientId: 'daemon',
|
|
234
|
+
content: 'Memory consolidation (idle trigger)',
|
|
235
|
+
force: false,
|
|
236
|
+
projectPath,
|
|
237
|
+
taskId: randomUUID(),
|
|
238
|
+
trigger: 'agent-idle',
|
|
239
|
+
type: 'dream',
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
log(`Dream not eligible (${result.reason}), killing idle agent: ${projectPath}`);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
log(`Dream eligibility check failed, killing idle agent: ${projectPath}`);
|
|
247
|
+
}
|
|
209
248
|
agentPool?.handleAgentDisconnected(projectPath);
|
|
210
249
|
},
|
|
211
250
|
timeoutMs: AGENT_IDLE_TIMEOUT_MS,
|
|
@@ -247,15 +286,45 @@ async function main() {
|
|
|
247
286
|
// Also broadcast to the project room so TUI and other connected clients are notified
|
|
248
287
|
broadcastToProjectRoom(projectRegistry, projectRouter, info.projectPath, ReviewEvents.NOTIFY, payload, info.clientId);
|
|
249
288
|
});
|
|
289
|
+
const queryLogHandler = new QueryLogHandler();
|
|
250
290
|
const transportHandlers = new TransportHandlers({
|
|
251
291
|
agentPool,
|
|
252
292
|
clientManager,
|
|
253
|
-
lifecycleHooks: [curateLogHandler],
|
|
293
|
+
lifecycleHooks: [curateLogHandler, queryLogHandler],
|
|
294
|
+
// Daemon-side gate for dream task:create — mirrors the idle-trigger pre-check
|
|
295
|
+
// in this file so the CLI path (brv dream without --force) actually honors
|
|
296
|
+
// gate 3 (queue). The agent-side check kept gate 3 hardcoded to skip,
|
|
297
|
+
// which made the CLI ignore the spec when other tasks were queued.
|
|
298
|
+
async preDispatchCheck(task, projectPath) {
|
|
299
|
+
if (task.type !== 'dream' || task.force)
|
|
300
|
+
return { eligible: true };
|
|
301
|
+
if (!projectPath)
|
|
302
|
+
return { eligible: true };
|
|
303
|
+
try {
|
|
304
|
+
const result = await makeDreamPreCheckTrigger(projectPath).checkEligibility(projectPath);
|
|
305
|
+
return result.eligible ? { eligible: true } : { eligible: false, skipResult: `Dream skipped: ${result.reason}` };
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Fail-open on pre-check errors: let the agent's own gate check be the fallback.
|
|
309
|
+
return { eligible: true };
|
|
310
|
+
}
|
|
311
|
+
},
|
|
254
312
|
projectRegistry,
|
|
255
313
|
projectRouter,
|
|
256
314
|
transport: transportServer,
|
|
257
315
|
});
|
|
258
316
|
transportHandlers.setup();
|
|
317
|
+
// Wire query metadata from agent process → QueryLogHandler.
|
|
318
|
+
// Agent sends task:queryResult BEFORE task:completed (Socket.IO preserves order),
|
|
319
|
+
// so setQueryResult runs before onTaskCompleted merges the metadata.
|
|
320
|
+
transportServer.onRequest(TransportTaskEventNames.QUERY_RESULT, (data) => {
|
|
321
|
+
queryLogHandler.setQueryResult(data.taskId, {
|
|
322
|
+
matchedDocs: data.matchedDocs,
|
|
323
|
+
searchMetadata: data.searchMetadata,
|
|
324
|
+
tier: data.tier,
|
|
325
|
+
timing: data.timing,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
259
328
|
// 8. Create idle timeout policy + shutdown handler
|
|
260
329
|
// (must be created before wiring closures that reference them)
|
|
261
330
|
// onIdle captures shutdownHandler via closure; safe because
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type DreamLockServiceOptions = {
|
|
2
|
+
baseDir: string;
|
|
3
|
+
staleTimeoutMs?: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* PID-based lock for dream execution.
|
|
7
|
+
*
|
|
8
|
+
* Lock file contains the owning process PID. Staleness is determined by mtime.
|
|
9
|
+
* Uses write-then-verify to handle race conditions between concurrent acquirers.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DreamLockService {
|
|
12
|
+
private readonly lockFilePath;
|
|
13
|
+
private readonly staleTimeoutMs;
|
|
14
|
+
constructor(opts: DreamLockServiceOptions);
|
|
15
|
+
/** Clear PID content but keep the file. mtime becomes "last dream completion time". */
|
|
16
|
+
release(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Restore mtime after a failed dream so the time gate isn't fooled.
|
|
19
|
+
* If priorMtime is 0 (lock didn't exist before), delete the file.
|
|
20
|
+
*/
|
|
21
|
+
rollback(priorMtime: number): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Try to acquire the dream lock.
|
|
24
|
+
*
|
|
25
|
+
* Returns `{ acquired: true, priorMtime }` on success, where priorMtime
|
|
26
|
+
* is the lock file's mtime before acquisition (0 if file didn't exist).
|
|
27
|
+
*
|
|
28
|
+
* Returns `{ acquired: false }` if another live, non-stale process holds the lock.
|
|
29
|
+
*/
|
|
30
|
+
tryAcquire(): Promise<{
|
|
31
|
+
acquired: false;
|
|
32
|
+
} | {
|
|
33
|
+
acquired: true;
|
|
34
|
+
priorMtime: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, unlink, utimes, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const LOCK_FILENAME = 'dream.lock';
|
|
4
|
+
const DEFAULT_STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
5
|
+
/**
|
|
6
|
+
* PID-based lock for dream execution.
|
|
7
|
+
*
|
|
8
|
+
* Lock file contains the owning process PID. Staleness is determined by mtime.
|
|
9
|
+
* Uses write-then-verify to handle race conditions between concurrent acquirers.
|
|
10
|
+
*/
|
|
11
|
+
export class DreamLockService {
|
|
12
|
+
lockFilePath;
|
|
13
|
+
staleTimeoutMs;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.lockFilePath = join(opts.baseDir, LOCK_FILENAME);
|
|
16
|
+
this.staleTimeoutMs = opts.staleTimeoutMs ?? DEFAULT_STALE_TIMEOUT_MS;
|
|
17
|
+
}
|
|
18
|
+
/** Clear PID content but keep the file. mtime becomes "last dream completion time". */
|
|
19
|
+
async release() {
|
|
20
|
+
await writeFile(this.lockFilePath, '', 'utf8');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Restore mtime after a failed dream so the time gate isn't fooled.
|
|
24
|
+
* If priorMtime is 0 (lock didn't exist before), delete the file.
|
|
25
|
+
*/
|
|
26
|
+
async rollback(priorMtime) {
|
|
27
|
+
if (priorMtime === 0) {
|
|
28
|
+
await unlink(this.lockFilePath).catch(() => { });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await writeFile(this.lockFilePath, '', 'utf8');
|
|
32
|
+
const time = new Date(priorMtime);
|
|
33
|
+
await utimes(this.lockFilePath, time, time);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Try to acquire the dream lock.
|
|
37
|
+
*
|
|
38
|
+
* Returns `{ acquired: true, priorMtime }` on success, where priorMtime
|
|
39
|
+
* is the lock file's mtime before acquisition (0 if file didn't exist).
|
|
40
|
+
*
|
|
41
|
+
* Returns `{ acquired: false }` if another live, non-stale process holds the lock.
|
|
42
|
+
*/
|
|
43
|
+
async tryAcquire() {
|
|
44
|
+
let priorMtime = 0;
|
|
45
|
+
try {
|
|
46
|
+
const lockStat = await stat(this.lockFilePath);
|
|
47
|
+
priorMtime = lockStat.mtimeMs;
|
|
48
|
+
const content = await readFile(this.lockFilePath, 'utf8');
|
|
49
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
50
|
+
if (!Number.isNaN(pid) && pid > 0) {
|
|
51
|
+
const alive = isProcessAlive(pid);
|
|
52
|
+
const stale = Date.now() - lockStat.mtimeMs > this.staleTimeoutMs;
|
|
53
|
+
if (alive && !stale) {
|
|
54
|
+
return { acquired: false };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// File doesn't exist — priorMtime stays 0
|
|
60
|
+
}
|
|
61
|
+
// Write our PID
|
|
62
|
+
await mkdir(dirname(this.lockFilePath), { recursive: true });
|
|
63
|
+
await writeFile(this.lockFilePath, String(process.pid), 'utf8');
|
|
64
|
+
// Write-then-verify: re-read to confirm we won the race
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(this.lockFilePath, 'utf8');
|
|
67
|
+
if (content.trim() !== String(process.pid)) {
|
|
68
|
+
return { acquired: false };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { acquired: false };
|
|
73
|
+
}
|
|
74
|
+
return { acquired: true, priorMtime };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isProcessAlive(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// EPERM: process exists but we can't signal it — treat as alive
|
|
84
|
+
if (error.code === 'EPERM')
|
|
85
|
+
return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|