byterover-cli 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +4 -0
- package/README.md +17 -0
- package/dist/agent/core/domain/tools/constants.d.ts +1 -0
- package/dist/agent/core/domain/tools/constants.js +1 -0
- package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error-codes.js +0 -1
- package/dist/agent/infra/agent/agent-error.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error.js +0 -1
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
- package/dist/agent/infra/agent/agent-state-manager.js +1 -3
- package/dist/agent/infra/agent/base-agent.d.ts +1 -1
- package/dist/agent/infra/agent/base-agent.js +1 -1
- package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
- package/dist/agent/infra/agent/cipher-agent.js +188 -3
- package/dist/agent/infra/agent/index.d.ts +1 -1
- package/dist/agent/infra/agent/index.js +1 -1
- package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
- package/dist/agent/infra/agent/service-initializer.js +14 -8
- package/dist/agent/infra/agent/types.d.ts +0 -1
- package/dist/agent/infra/file-system/file-system-service.js +6 -5
- package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
- package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
- package/dist/agent/infra/llm/providers/openai.js +12 -0
- package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
- package/dist/agent/infra/llm/stream-to-text.js +14 -0
- package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
- package/dist/agent/infra/map/abstract-generator.js +67 -0
- package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
- package/dist/agent/infra/map/abstract-queue.js +218 -0
- package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
- package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
- package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
- package/dist/agent/infra/memory/memory-manager.js +6 -5
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +20 -7
- package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
- package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/session/session-compressor.d.ts +43 -0
- package/dist/agent/infra/session/session-compressor.js +296 -0
- package/dist/agent/infra/session/session-manager.d.ts +7 -0
- package/dist/agent/infra/session/session-manager.js +9 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +16 -5
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.js +4 -3
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.js +4 -3
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +41 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -3
- package/dist/server/constants.d.ts +8 -0
- package/dist/server/constants.js +10 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
- package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
- package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
- package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +18 -10
- package/dist/server/core/domain/transport/schemas.js +4 -0
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- package/dist/server/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/context-tree/derived-artifact.js +5 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
- package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
- package/dist/server/infra/daemon/agent-process.js +15 -5
- package/dist/server/infra/executor/curate-executor.js +6 -3
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +88 -7
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +125 -23
- package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +65 -13
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/utils/curate-result-parser.d.ts +4 -4
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +26 -0
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +23 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/oclif.manifest.json +261 -1
- package/package.json +10 -4
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { generateFileAbstracts } from './abstract-generator.js';
|
|
5
|
+
const QUEUE_TRACE_ENABLED = process.env.BRV_QUEUE_TRACE === '1';
|
|
6
|
+
const LOG_PATH = process.env.BRV_SESSION_LOG;
|
|
7
|
+
function queueLog(message) {
|
|
8
|
+
if (!QUEUE_TRACE_ENABLED || !LOG_PATH)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
appendFileSync(LOG_PATH, `${new Date().toISOString()} [abstract-queue] ${message}\n`);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore — tracing must never block queue progress
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Background queue for generating L0/L1 abstract files (.abstract.md, .overview.md).
|
|
19
|
+
*
|
|
20
|
+
* - Generator is injected lazily via setGenerator() (mirrors rebindMapTools pattern)
|
|
21
|
+
* - Items arriving before setGenerator() are buffered and processed once generator is set
|
|
22
|
+
* - Writes status to <projectRoot>/.brv/_queue_status.json after each state transition
|
|
23
|
+
* - Retries up to maxAttempts with exponential backoff (500ms base)
|
|
24
|
+
* - drain() waits for all pending/processing items to complete (for graceful shutdown)
|
|
25
|
+
*/
|
|
26
|
+
export class AbstractGenerationQueue {
|
|
27
|
+
projectRoot;
|
|
28
|
+
maxAttempts;
|
|
29
|
+
drainResolvers = [];
|
|
30
|
+
failed = 0;
|
|
31
|
+
generator;
|
|
32
|
+
onBeforeProcess;
|
|
33
|
+
pending = [];
|
|
34
|
+
processed = 0;
|
|
35
|
+
processing = false;
|
|
36
|
+
/** Number of items currently in retry backoff (removed from pending but not yet re-enqueued). */
|
|
37
|
+
retrying = 0;
|
|
38
|
+
statusDirCreated = false;
|
|
39
|
+
statusWriteFailed = false;
|
|
40
|
+
statusWritePromise = Promise.resolve();
|
|
41
|
+
constructor(projectRoot, maxAttempts = 3) {
|
|
42
|
+
this.projectRoot = projectRoot;
|
|
43
|
+
this.maxAttempts = maxAttempts;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Wait for all pending items to finish processing (graceful shutdown).
|
|
47
|
+
* Includes items currently in retry backoff so drain() does not resolve prematurely.
|
|
48
|
+
*/
|
|
49
|
+
async drain() {
|
|
50
|
+
queueLog(`drain:start idle=${this.isIdle()} pending=${this.pending.length} retrying=${this.retrying} processing=${this.processing}`);
|
|
51
|
+
if (this.isIdle()) {
|
|
52
|
+
await this.statusWritePromise.catch(() => { });
|
|
53
|
+
queueLog('drain:resolved-immediate');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
this.drainResolvers.push(resolve);
|
|
58
|
+
this.resolveDrainersIfIdle();
|
|
59
|
+
});
|
|
60
|
+
queueLog('drain:resolved-deferred');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Add a file to the abstract generation queue.
|
|
64
|
+
*/
|
|
65
|
+
enqueue(item) {
|
|
66
|
+
// Guard against paths that must never trigger abstract generation:
|
|
67
|
+
// - derived artifacts (.abstract.md, .overview.md) — would produce .abstract.abstract.md
|
|
68
|
+
// - summary index files (_index.md) — domain/topic summaries, not knowledge nodes
|
|
69
|
+
// - hierarchy scaffolding (context.md) — helper files, not leaf knowledge entries
|
|
70
|
+
const fileName = item.contextPath.split('/').at(-1) ?? item.contextPath;
|
|
71
|
+
if (fileName === 'context.md' ||
|
|
72
|
+
fileName === '_index.md' ||
|
|
73
|
+
item.contextPath.endsWith('.abstract.md') ||
|
|
74
|
+
item.contextPath.endsWith('.overview.md')) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.pending.push({ attempts: 0, contextPath: item.contextPath, fullContent: item.fullContent });
|
|
78
|
+
queueLog(`enqueue path=${item.contextPath} pending=${this.pending.length} retrying=${this.retrying} processing=${this.processing}`);
|
|
79
|
+
this.queueStatusWrite();
|
|
80
|
+
this.scheduleNext();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Return current queue status snapshot.
|
|
84
|
+
*/
|
|
85
|
+
getStatus() {
|
|
86
|
+
return {
|
|
87
|
+
failed: this.failed,
|
|
88
|
+
// Items in retry backoff are still pending work — include them so the status
|
|
89
|
+
// does not falsely report the queue as idle during backoff windows.
|
|
90
|
+
pending: this.pending.length + this.retrying,
|
|
91
|
+
processed: this.processed,
|
|
92
|
+
processing: this.processing,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set a callback that runs before each item is processed.
|
|
97
|
+
* Used to refresh OAuth tokens before LLM calls.
|
|
98
|
+
*/
|
|
99
|
+
setBeforeProcess(fn) {
|
|
100
|
+
this.onBeforeProcess = fn;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Inject the LLM generator. Triggers processing of any buffered items.
|
|
104
|
+
*/
|
|
105
|
+
setGenerator(generator) {
|
|
106
|
+
this.generator = generator;
|
|
107
|
+
this.scheduleNext();
|
|
108
|
+
}
|
|
109
|
+
isIdle() {
|
|
110
|
+
return this.pending.length === 0 && !this.processing && this.retrying === 0;
|
|
111
|
+
}
|
|
112
|
+
async processNext() {
|
|
113
|
+
if (!this.generator || this.processing || this.pending.length === 0) {
|
|
114
|
+
this.resolveDrainersIfIdle();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.processing = true;
|
|
118
|
+
this.queueStatusWrite();
|
|
119
|
+
const item = this.pending.shift();
|
|
120
|
+
queueLog(`process:start path=${item.contextPath} remaining=${this.pending.length} retrying=${this.retrying}`);
|
|
121
|
+
try {
|
|
122
|
+
// Refresh credentials before each generation (OAuth tokens may expire)
|
|
123
|
+
try {
|
|
124
|
+
await this.onBeforeProcess?.();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
128
|
+
console.debug(`[AbstractQueue] token refresh failed, proceeding with existing generator: ${msg}`);
|
|
129
|
+
}
|
|
130
|
+
const { abstractContent, overviewContent } = await generateFileAbstracts(item.fullContent, this.generator);
|
|
131
|
+
// Derive sibling paths: replace .md with .abstract.md and .overview.md
|
|
132
|
+
const abstractPath = item.contextPath.replace(/\.md$/, '.abstract.md');
|
|
133
|
+
const overviewPath = item.contextPath.replace(/\.md$/, '.overview.md');
|
|
134
|
+
await Promise.all([
|
|
135
|
+
writeFile(abstractPath, abstractContent, 'utf8'),
|
|
136
|
+
writeFile(overviewPath, overviewContent, 'utf8'),
|
|
137
|
+
]);
|
|
138
|
+
this.processed++;
|
|
139
|
+
queueLog(`process:success path=${item.contextPath} processed=${this.processed}`);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
143
|
+
console.debug(`[AbstractQueue] ${item.contextPath} attempt ${item.attempts + 1}/${this.maxAttempts}: ${msg}`);
|
|
144
|
+
item.attempts++;
|
|
145
|
+
if (item.attempts < this.maxAttempts) {
|
|
146
|
+
// Exponential backoff: 500ms, 1000ms, 2000ms, ...
|
|
147
|
+
const delay = 500 * 2 ** (item.attempts - 1);
|
|
148
|
+
this.retrying++;
|
|
149
|
+
this.queueStatusWrite();
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
this.retrying--;
|
|
152
|
+
this.pending.unshift(item);
|
|
153
|
+
queueLog(`process:retry-requeue path=${item.contextPath} pending=${this.pending.length} retrying=${this.retrying}`);
|
|
154
|
+
this.queueStatusWrite();
|
|
155
|
+
this.scheduleNext();
|
|
156
|
+
}, delay);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.failed++;
|
|
160
|
+
queueLog(`process:failed path=${item.contextPath} failed=${this.failed}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
this.processing = false;
|
|
165
|
+
queueLog(`process:finally path=${item.contextPath} pending=${this.pending.length} retrying=${this.retrying} processed=${this.processed} failed=${this.failed}`);
|
|
166
|
+
this.queueStatusWrite();
|
|
167
|
+
}
|
|
168
|
+
this.scheduleNext();
|
|
169
|
+
this.resolveDrainersIfIdle();
|
|
170
|
+
}
|
|
171
|
+
queueStatusWrite() {
|
|
172
|
+
this.statusWritePromise = this.statusWritePromise
|
|
173
|
+
.catch(() => { })
|
|
174
|
+
.then(async () => this.writeStatusFile());
|
|
175
|
+
}
|
|
176
|
+
resolveDrainersIfIdle() {
|
|
177
|
+
if (!this.isIdle() || this.drainResolvers.length === 0) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
queueLog(`drain:idle pending=${this.pending.length} retrying=${this.retrying} processed=${this.processed} failed=${this.failed}`);
|
|
181
|
+
const resolvers = this.drainResolvers.splice(0);
|
|
182
|
+
const settledStatusWrite = this.statusWritePromise.catch(() => { });
|
|
183
|
+
for (const resolve of resolvers) {
|
|
184
|
+
settledStatusWrite.then(() => resolve()).catch(() => { });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
scheduleNext() {
|
|
188
|
+
if (!this.generator || this.processing || this.pending.length === 0) {
|
|
189
|
+
this.resolveDrainersIfIdle();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// eslint-disable-next-line no-void
|
|
193
|
+
setImmediate(() => { void this.processNext(); });
|
|
194
|
+
}
|
|
195
|
+
async writeStatusFile() {
|
|
196
|
+
const statusPath = join(this.projectRoot, '.brv', '_queue_status.json');
|
|
197
|
+
try {
|
|
198
|
+
if (!this.statusDirCreated) {
|
|
199
|
+
await mkdir(join(this.projectRoot, '.brv'), { recursive: true });
|
|
200
|
+
this.statusDirCreated = true;
|
|
201
|
+
}
|
|
202
|
+
await writeFile(statusPath, JSON.stringify(this.getStatus()), 'utf8');
|
|
203
|
+
this.statusWriteFailed = false;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const errorCode = typeof error === 'object' && error !== null && 'code' in error
|
|
207
|
+
? error.code
|
|
208
|
+
: undefined;
|
|
209
|
+
if (errorCode === 'ENOENT') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!this.statusWriteFailed) {
|
|
213
|
+
this.statusWriteFailed = true;
|
|
214
|
+
console.debug(`[AbstractGenerationQueue] Failed to write queue status: ${error instanceof Error ? error.message : String(error)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Memory } from '../../core/domain/memory/types.js';
|
|
2
|
+
import type { IContentGenerator } from '../../core/interfaces/i-content-generator.js';
|
|
3
|
+
/**
|
|
4
|
+
* A draft memory extracted from a session, before deduplication.
|
|
5
|
+
*/
|
|
6
|
+
export interface DraftMemory {
|
|
7
|
+
category: string;
|
|
8
|
+
content: string;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Deduplication decision for a single draft memory.
|
|
13
|
+
*/
|
|
14
|
+
export type DeduplicationAction = {
|
|
15
|
+
action: 'CREATE';
|
|
16
|
+
memory: DraftMemory;
|
|
17
|
+
} | {
|
|
18
|
+
action: 'MERGE';
|
|
19
|
+
memory: DraftMemory;
|
|
20
|
+
mergedContent: string;
|
|
21
|
+
targetId: string;
|
|
22
|
+
} | {
|
|
23
|
+
action: 'SKIP';
|
|
24
|
+
memory: DraftMemory;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* LLM-based deduplicator for agent-extracted memories.
|
|
28
|
+
*
|
|
29
|
+
* For each draft, checks against existing memories via an LLM call.
|
|
30
|
+
* DECISIONS category drafts always result in CREATE (immutable log).
|
|
31
|
+
*/
|
|
32
|
+
export declare class MemoryDeduplicator {
|
|
33
|
+
private readonly generator;
|
|
34
|
+
constructor(generator: IContentGenerator);
|
|
35
|
+
/**
|
|
36
|
+
* Deduplicate a list of draft memories against existing stored memories.
|
|
37
|
+
*
|
|
38
|
+
* @param drafts - Draft memories to check
|
|
39
|
+
* @param existing - Existing memories to compare against
|
|
40
|
+
* @returns Deduplication action for each draft
|
|
41
|
+
*/
|
|
42
|
+
deduplicate(drafts: DraftMemory[], existing: Memory[]): Promise<DeduplicationAction[]>;
|
|
43
|
+
private deduplicateSingle;
|
|
44
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { streamToText } from '../llm/stream-to-text.js';
|
|
3
|
+
const SYSTEM_PROMPT = `You are a memory deduplication assistant. Given a new draft memory and a list of existing memories, decide one of:
|
|
4
|
+
- CREATE: the draft is new and should be stored as-is
|
|
5
|
+
- MERGE: the draft overlaps with an existing memory; provide merged content
|
|
6
|
+
- SKIP: the draft is already covered by an existing memory
|
|
7
|
+
|
|
8
|
+
Respond with ONLY a JSON object:
|
|
9
|
+
{"action": "CREATE"}
|
|
10
|
+
{"action": "MERGE", "targetId": "<id>", "mergedContent": "<combined content>"}
|
|
11
|
+
{"action": "SKIP"}`;
|
|
12
|
+
const DEDUPLICATION_CONCURRENCY = 4;
|
|
13
|
+
/**
|
|
14
|
+
* LLM-based deduplicator for agent-extracted memories.
|
|
15
|
+
*
|
|
16
|
+
* For each draft, checks against existing memories via an LLM call.
|
|
17
|
+
* DECISIONS category drafts always result in CREATE (immutable log).
|
|
18
|
+
*/
|
|
19
|
+
export class MemoryDeduplicator {
|
|
20
|
+
generator;
|
|
21
|
+
constructor(generator) {
|
|
22
|
+
this.generator = generator;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deduplicate a list of draft memories against existing stored memories.
|
|
26
|
+
*
|
|
27
|
+
* @param drafts - Draft memories to check
|
|
28
|
+
* @param existing - Existing memories to compare against
|
|
29
|
+
* @returns Deduplication action for each draft
|
|
30
|
+
*/
|
|
31
|
+
async deduplicate(drafts, existing) {
|
|
32
|
+
if (existing.length === 0) {
|
|
33
|
+
return drafts.map((memory) => ({ action: 'CREATE', memory }));
|
|
34
|
+
}
|
|
35
|
+
const actions = Array.from({ length: drafts.length });
|
|
36
|
+
const concurrency = Math.min(DEDUPLICATION_CONCURRENCY, drafts.length);
|
|
37
|
+
const worker = async (workerIndex) => {
|
|
38
|
+
for (let draftIndex = workerIndex; draftIndex < drafts.length; draftIndex += concurrency) {
|
|
39
|
+
const draft = drafts[draftIndex];
|
|
40
|
+
if (draft.category === 'DECISIONS') {
|
|
41
|
+
actions[draftIndex] = { action: 'CREATE', memory: draft };
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line no-await-in-loop
|
|
45
|
+
actions[draftIndex] = await this.deduplicateSingle(draft, existing);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await Promise.all(Array.from({ length: concurrency }, async (_, workerIndex) => worker(workerIndex)));
|
|
49
|
+
return actions;
|
|
50
|
+
}
|
|
51
|
+
async deduplicateSingle(draft, existing) {
|
|
52
|
+
const existingSummary = JSON.stringify(existing.map((m) => ({ content: m.content.slice(0, 300), id: m.id })));
|
|
53
|
+
const prompt = `## Draft Memory (category: ${draft.category})
|
|
54
|
+
${draft.content}
|
|
55
|
+
|
|
56
|
+
## Existing Memories (JSON)
|
|
57
|
+
${existingSummary}
|
|
58
|
+
|
|
59
|
+
Decide: CREATE, MERGE (with targetId and mergedContent), or SKIP.`;
|
|
60
|
+
try {
|
|
61
|
+
// Use streaming — ChatGPT OAuth Codex endpoint requires stream: true
|
|
62
|
+
const responseText = await streamToText(this.generator, {
|
|
63
|
+
config: { maxTokens: 300, temperature: 0 },
|
|
64
|
+
contents: [{ content: prompt, role: 'user' }],
|
|
65
|
+
model: 'default',
|
|
66
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
67
|
+
taskId: randomUUID(),
|
|
68
|
+
});
|
|
69
|
+
// Strip markdown code fences — some providers wrap JSON in ```json ... ```
|
|
70
|
+
const jsonText = responseText.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
71
|
+
const parsed = JSON.parse(jsonText);
|
|
72
|
+
const targetExists = parsed.targetId ? existing.some((memory) => memory.id === parsed.targetId) : false;
|
|
73
|
+
if (parsed.action === 'MERGE' && targetExists && parsed.mergedContent && parsed.targetId) {
|
|
74
|
+
return { action: 'MERGE', memory: draft, mergedContent: parsed.mergedContent, targetId: parsed.targetId };
|
|
75
|
+
}
|
|
76
|
+
if (parsed.action === 'SKIP') {
|
|
77
|
+
return { action: 'SKIP', memory: draft };
|
|
78
|
+
}
|
|
79
|
+
return { action: 'CREATE', memory: draft };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// On any error, default to CREATE (fail-open)
|
|
83
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
84
|
+
console.debug(`[MemoryDeduplicator] Failed for ${draft.category}: ${msg}`);
|
|
85
|
+
return { action: 'CREATE', memory: draft };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -20,6 +20,7 @@ import type { ILogger } from '../../core/interfaces/i-logger.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export declare class MemoryManager {
|
|
22
22
|
private blobStorage;
|
|
23
|
+
private static readonly MEMORY_ID_LENGTH;
|
|
23
24
|
private static readonly MEMORY_KEY_PREFIX;
|
|
24
25
|
private readonly logger;
|
|
25
26
|
constructor(blobStorage: IBlobStorage, logger?: ILogger);
|
|
@@ -105,6 +105,7 @@ const ListMemoriesOptionsSchema = z
|
|
|
105
105
|
*/
|
|
106
106
|
export class MemoryManager {
|
|
107
107
|
blobStorage;
|
|
108
|
+
static MEMORY_ID_LENGTH = 12;
|
|
108
109
|
static MEMORY_KEY_PREFIX = 'memory-';
|
|
109
110
|
logger;
|
|
110
111
|
constructor(blobStorage, logger) {
|
|
@@ -468,11 +469,11 @@ export class MemoryManager {
|
|
|
468
469
|
if (!key.startsWith(MemoryManager.MEMORY_KEY_PREFIX)) {
|
|
469
470
|
return false;
|
|
470
471
|
}
|
|
471
|
-
// Memory keys have format: memory-{id}
|
|
472
|
-
// Attachment keys
|
|
473
|
-
//
|
|
474
|
-
const
|
|
475
|
-
return
|
|
472
|
+
// Memory keys have format: memory-{id} where id is a fixed-length nanoid(12).
|
|
473
|
+
// Attachment keys append an extra suffix: memory-{id}-{suffix}.
|
|
474
|
+
// A valid memory id may itself contain '-', so counting dashes is incorrect.
|
|
475
|
+
const suffix = key.slice(MemoryManager.MEMORY_KEY_PREFIX.length);
|
|
476
|
+
return suffix.length === MemoryManager.MEMORY_ID_LENGTH;
|
|
476
477
|
}
|
|
477
478
|
/**
|
|
478
479
|
* Load all memories from blob storage
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* Wraps the curate-tool logic for use in the sandbox's tools.* SDK.
|
|
4
4
|
*/
|
|
5
5
|
import type { CurateOperation, CurateOptions, CurateResult, DetectDomainsInput, DetectDomainsResult, ICurateService } from '../../core/interfaces/i-curate-service.js';
|
|
6
|
+
import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
|
|
6
7
|
/**
|
|
7
8
|
* Curate service implementation.
|
|
8
9
|
* Provides curate and domain detection operations for the sandbox.
|
|
9
10
|
*/
|
|
10
11
|
export declare class CurateService implements ICurateService {
|
|
12
|
+
private readonly abstractQueue?;
|
|
11
13
|
private readonly workingDirectory;
|
|
12
|
-
constructor(workingDirectory?: string);
|
|
14
|
+
constructor(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue | undefined);
|
|
13
15
|
/**
|
|
14
16
|
* Execute curate operations on knowledge topics.
|
|
15
17
|
*
|
|
@@ -33,4 +35,4 @@ export declare class CurateService implements ICurateService {
|
|
|
33
35
|
* @param workingDirectory - Working directory for resolving relative paths
|
|
34
36
|
* @returns CurateService instance
|
|
35
37
|
*/
|
|
36
|
-
export declare function createCurateService(workingDirectory?: string): ICurateService;
|
|
38
|
+
export declare function createCurateService(workingDirectory?: string, abstractQueue?: AbstractGenerationQueue): ICurateService;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { resolve } from 'node:path';
|
|
6
6
|
import { executeCurate } from '../tools/implementations/curate-tool.js';
|
|
7
|
+
import { validateWriteTarget } from '../tools/write-guard.js';
|
|
7
8
|
/**
|
|
8
9
|
* Default base path for knowledge storage.
|
|
9
10
|
*/
|
|
@@ -72,8 +73,10 @@ function validateOperations(operations) {
|
|
|
72
73
|
* Provides curate and domain detection operations for the sandbox.
|
|
73
74
|
*/
|
|
74
75
|
export class CurateService {
|
|
76
|
+
abstractQueue;
|
|
75
77
|
workingDirectory;
|
|
76
|
-
constructor(workingDirectory) {
|
|
78
|
+
constructor(workingDirectory, abstractQueue) {
|
|
79
|
+
this.abstractQueue = abstractQueue;
|
|
77
80
|
this.workingDirectory = workingDirectory ?? process.cwd();
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
@@ -88,6 +91,19 @@ export class CurateService {
|
|
|
88
91
|
// Resolve relative basePath against the working directory to ensure
|
|
89
92
|
// files are written to the correct project directory, not process.cwd()
|
|
90
93
|
const basePath = resolve(this.workingDirectory, rawBasePath);
|
|
94
|
+
// Source write guard: block curate to shared source context trees
|
|
95
|
+
const writeError = validateWriteTarget(basePath, this.workingDirectory);
|
|
96
|
+
if (writeError) {
|
|
97
|
+
return {
|
|
98
|
+
applied: [{
|
|
99
|
+
message: writeError,
|
|
100
|
+
path: rawBasePath,
|
|
101
|
+
status: 'failed',
|
|
102
|
+
type: 'ADD',
|
|
103
|
+
}],
|
|
104
|
+
summary: { added: 0, deleted: 0, failed: 1, merged: 0, updated: 0 },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
91
107
|
// Pre-validate operations to catch common mistakes early
|
|
92
108
|
const validationFailures = validateOperations(operations);
|
|
93
109
|
if (validationFailures.length > 0) {
|
|
@@ -104,10 +120,7 @@ export class CurateService {
|
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
// Call the underlying executeCurate function from curate-tool
|
|
107
|
-
const result = await executeCurate({
|
|
108
|
-
basePath,
|
|
109
|
-
operations,
|
|
110
|
-
});
|
|
123
|
+
const result = await executeCurate({ basePath, operations }, undefined, this.abstractQueue);
|
|
111
124
|
return result;
|
|
112
125
|
}
|
|
113
126
|
/**
|
|
@@ -146,6 +159,6 @@ export class CurateService {
|
|
|
146
159
|
* @param workingDirectory - Working directory for resolving relative paths
|
|
147
160
|
* @returns CurateService instance
|
|
148
161
|
*/
|
|
149
|
-
export function createCurateService(workingDirectory) {
|
|
150
|
-
return new CurateService(workingDirectory);
|
|
162
|
+
export function createCurateService(workingDirectory, abstractQueue) {
|
|
163
|
+
return new CurateService(workingDirectory, abstractQueue);
|
|
151
164
|
}
|
|
@@ -22,6 +22,8 @@ export declare class LocalSandbox {
|
|
|
22
22
|
/** Value set by setFinalResult() — signals early exit */
|
|
23
23
|
private finalResult?;
|
|
24
24
|
private outputBuffer;
|
|
25
|
+
/** Async tool calls started inside the sandbox, including unawaited ones. */
|
|
26
|
+
private pendingToolPromises;
|
|
25
27
|
/** Current stdout cap in chars (undefined = unlimited) */
|
|
26
28
|
private stdoutCap?;
|
|
27
29
|
/** Running count of chars written to outputBuffer in current execution */
|
|
@@ -50,9 +52,12 @@ export declare class LocalSandbox {
|
|
|
50
52
|
* @param updates - Key-value pairs to add to context
|
|
51
53
|
*/
|
|
52
54
|
updateContext(updates: Record<string, unknown>): void;
|
|
55
|
+
private awaitPendingToolPromises;
|
|
53
56
|
/**
|
|
54
57
|
* Push a line to stdout, respecting the optional character cap.
|
|
55
58
|
* When the cap is reached, appends a truncation notice and drops further output.
|
|
56
59
|
*/
|
|
57
60
|
private pushStdout;
|
|
61
|
+
private trackPromise;
|
|
62
|
+
private wrapToolObject;
|
|
58
63
|
}
|
|
@@ -87,6 +87,8 @@ export class LocalSandbox {
|
|
|
87
87
|
/** Value set by setFinalResult() — signals early exit */
|
|
88
88
|
finalResult;
|
|
89
89
|
outputBuffer = [];
|
|
90
|
+
/** Async tool calls started inside the sandbox, including unawaited ones. */
|
|
91
|
+
pendingToolPromises = new Set();
|
|
90
92
|
/** Current stdout cap in chars (undefined = unlimited) */
|
|
91
93
|
stdoutCap;
|
|
92
94
|
/** Running count of chars written to outputBuffer in current execution */
|
|
@@ -118,7 +120,7 @@ export class LocalSandbox {
|
|
|
118
120
|
};
|
|
119
121
|
// Inject Tools SDK if provided (for file system operations)
|
|
120
122
|
if (toolsSDK) {
|
|
121
|
-
sandbox.tools = toolsSDK;
|
|
123
|
+
sandbox.tools = this.wrapToolObject(toolsSDK);
|
|
122
124
|
}
|
|
123
125
|
// Inject environment context as `env` object if provided
|
|
124
126
|
if (environmentContext) {
|
|
@@ -152,6 +154,7 @@ export class LocalSandbox {
|
|
|
152
154
|
this.outputBuffer = [];
|
|
153
155
|
this.errorBuffer = [];
|
|
154
156
|
this.finalResult = undefined;
|
|
157
|
+
this.pendingToolPromises.clear();
|
|
155
158
|
this.stdoutCap = config?.maxStdoutChars;
|
|
156
159
|
this.stdoutCharsWritten = 0;
|
|
157
160
|
this.stdoutTruncated = false;
|
|
@@ -192,6 +195,8 @@ export class LocalSandbox {
|
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
}
|
|
198
|
+
const elapsedMs = performance.now() - startTime;
|
|
199
|
+
await this.awaitPendingToolPromises(Math.max(0, timeout - elapsedMs));
|
|
195
200
|
}
|
|
196
201
|
catch (error) {
|
|
197
202
|
const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
@@ -243,6 +248,32 @@ export class LocalSandbox {
|
|
|
243
248
|
this.context[key] = value;
|
|
244
249
|
}
|
|
245
250
|
}
|
|
251
|
+
async awaitPendingToolPromises(timeoutMs) {
|
|
252
|
+
const deadline = Date.now() + timeoutMs;
|
|
253
|
+
/* eslint-disable no-await-in-loop */
|
|
254
|
+
while (this.pendingToolPromises.size > 0) {
|
|
255
|
+
const pending = [...this.pendingToolPromises];
|
|
256
|
+
const remaining = deadline - Date.now();
|
|
257
|
+
if (remaining <= 0) {
|
|
258
|
+
throw new Error('Async execution timeout');
|
|
259
|
+
}
|
|
260
|
+
let timeoutId;
|
|
261
|
+
try {
|
|
262
|
+
await Promise.race([
|
|
263
|
+
Promise.allSettled(pending),
|
|
264
|
+
new Promise((_, reject) => {
|
|
265
|
+
timeoutId = setTimeout(() => reject(new Error('Async execution timeout')), remaining);
|
|
266
|
+
}),
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (timeoutId !== undefined) {
|
|
271
|
+
clearTimeout(timeoutId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/* eslint-enable no-await-in-loop */
|
|
276
|
+
}
|
|
246
277
|
/**
|
|
247
278
|
* Push a line to stdout, respecting the optional character cap.
|
|
248
279
|
* When the cap is reached, appends a truncation notice and drops further output.
|
|
@@ -263,4 +294,29 @@ export class LocalSandbox {
|
|
|
263
294
|
this.stdoutCharsWritten += line.length + 1; // +1 for join newline
|
|
264
295
|
this.outputBuffer.push(line);
|
|
265
296
|
}
|
|
297
|
+
trackPromise(promise) {
|
|
298
|
+
const trackedPromise = promise.finally(() => {
|
|
299
|
+
this.pendingToolPromises.delete(trackedPromise);
|
|
300
|
+
});
|
|
301
|
+
this.pendingToolPromises.add(trackedPromise);
|
|
302
|
+
return trackedPromise;
|
|
303
|
+
}
|
|
304
|
+
wrapToolObject(value) {
|
|
305
|
+
if (typeof value === 'function') {
|
|
306
|
+
const original = value;
|
|
307
|
+
const wrapped = (...args) => {
|
|
308
|
+
const result = original(...args);
|
|
309
|
+
const isPromiseLike = result !== null &&
|
|
310
|
+
typeof result === 'object' &&
|
|
311
|
+
typeof result.then === 'function';
|
|
312
|
+
return isPromiseLike ? this.trackPromise(Promise.resolve(result)) : result;
|
|
313
|
+
};
|
|
314
|
+
return wrapped;
|
|
315
|
+
}
|
|
316
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
317
|
+
const wrappedEntries = Object.entries(value).map(([key, child]) => ([key, this.wrapToolObject(child)]));
|
|
318
|
+
return Object.fromEntries(wrappedEntries);
|
|
319
|
+
}
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
266
322
|
}
|
|
@@ -215,6 +215,7 @@ export class SandboxService {
|
|
|
215
215
|
curateService: this.curateService,
|
|
216
216
|
fileSystem: this.fileSystem,
|
|
217
217
|
parentSessionId: sessionId,
|
|
218
|
+
projectRoot: this.environmentContext?.workingDirectory,
|
|
218
219
|
sandboxService: this,
|
|
219
220
|
searchKnowledgeService: this.searchKnowledgeService,
|
|
220
221
|
sessionManager: this.sessionManager,
|
|
@@ -62,6 +62,8 @@ export interface ListDirectoryOptions {
|
|
|
62
62
|
export interface SearchKnowledgeOptions {
|
|
63
63
|
/** Maximum number of results to return (default: 10) */
|
|
64
64
|
limit?: number;
|
|
65
|
+
/** Path prefix to scope search within (e.g. "auth" or "packages/api") */
|
|
66
|
+
scope?: string;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* Result type for searchKnowledge operation.
|
|
@@ -74,11 +76,19 @@ export interface SearchKnowledgeResult {
|
|
|
74
76
|
/** Number of other memories that reference this one */
|
|
75
77
|
backlinkCount?: number;
|
|
76
78
|
excerpt: string;
|
|
79
|
+
/** Origin: 'local' for this project, 'shared' for results from knowledge source */
|
|
80
|
+
origin?: 'local' | 'shared';
|
|
81
|
+
/** Alias of the shared source (undefined for local results) */
|
|
82
|
+
originAlias?: string;
|
|
83
|
+
/** Absolute path to the context tree root this result belongs to. Use join(originContextTreeRoot, path) to read. */
|
|
84
|
+
originContextTreeRoot?: string;
|
|
85
|
+
/** Path to .overview.md for this entry; present when L1 overview exists */
|
|
86
|
+
overviewPath?: string;
|
|
77
87
|
path: string;
|
|
78
88
|
/** Top backlink source paths (max 3) */
|
|
79
89
|
relatedPaths?: string[];
|
|
80
90
|
score: number;
|
|
81
|
-
/** Symbol kind: 'domain' | 'topic' | 'subtopic' | 'context' | 'archive_stub' */
|
|
91
|
+
/** Symbol kind: 'domain' | 'topic' | 'subtopic' | 'context' | 'archive_stub' | 'summary' */
|
|
82
92
|
symbolKind?: string;
|
|
83
93
|
/** Resolved hierarchical path in the symbol tree */
|
|
84
94
|
symbolPath?: string;
|
|
@@ -223,6 +233,8 @@ export interface CreateToolsSDKOptions {
|
|
|
223
233
|
fileSystem: IFileSystem;
|
|
224
234
|
/** Parent session ID for creating child sessions (required for agentQuery) */
|
|
225
235
|
parentSessionId?: string;
|
|
236
|
+
/** Project root for write guard validation (blocks writes to shared source context trees) */
|
|
237
|
+
projectRoot?: string;
|
|
226
238
|
/** Sandbox service for variable injection into child sessions (optional, enables contextData in agentQuery) */
|
|
227
239
|
sandboxService?: ISandboxService;
|
|
228
240
|
/** Search knowledge service */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextTreeStore } from '../map/context-tree-store.js';
|
|
2
2
|
import { executeLlmMapMemory } from '../map/llm-map-memory.js';
|
|
3
|
+
import { validateWriteTarget } from '../tools/write-guard.js';
|
|
3
4
|
import { chunk, dedup, detectMessageBoundaries, groupBySubject, recon, recordProgress, } from './curation-helpers.js';
|
|
4
5
|
/**
|
|
5
6
|
* Creates a Tools SDK instance for sandbox code execution.
|
|
@@ -11,7 +12,7 @@ import { chunk, dedup, detectMessageBoundaries, groupBySubject, recon, recordPro
|
|
|
11
12
|
* @returns ToolsSDK instance ready to be injected into sandbox context
|
|
12
13
|
*/
|
|
13
14
|
export function createToolsSDK(options) {
|
|
14
|
-
const { commandType, contentGenerator, curateService, fileSystem, parentSessionId, sandboxService, searchKnowledgeService, sessionManager } = options;
|
|
15
|
+
const { commandType, contentGenerator, curateService, fileSystem, parentSessionId, projectRoot, sandboxService, searchKnowledgeService, sessionManager } = options;
|
|
15
16
|
const isReadOnly = commandType === 'query';
|
|
16
17
|
return {
|
|
17
18
|
async agentQuery(prompt, options) {
|
|
@@ -156,6 +157,13 @@ export function createToolsSDK(options) {
|
|
|
156
157
|
if (isReadOnly) {
|
|
157
158
|
throw new Error('writeFile() is disabled in read-only (query) mode');
|
|
158
159
|
}
|
|
160
|
+
// Write guard: block writes to shared source context trees
|
|
161
|
+
if (projectRoot) {
|
|
162
|
+
const writeError = validateWriteTarget(filePath, projectRoot);
|
|
163
|
+
if (writeError) {
|
|
164
|
+
throw new Error(writeError);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
159
167
|
return fileSystem.writeFile(filePath, content, {
|
|
160
168
|
createDirs: options?.createDirs ?? false,
|
|
161
169
|
});
|