@sanqianx/project-knowledge 4.7.0 → 5.0.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/.agents/plugins/marketplace.json +17 -17
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +836 -799
- package/README.md +58 -41
- package/_modules/knowledge-engine/README.md +91 -0
- package/_modules/knowledge-engine/package.json +38 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
- package/_modules/knowledge-engine/src/bin.js +52 -0
- package/_modules/knowledge-engine/src/bridge-client.js +24 -0
- package/_modules/knowledge-engine/src/coordinator.js +263 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
- package/_modules/knowledge-engine/src/memories.js +85 -0
- package/_modules/knowledge-engine/src/retrieval.js +116 -0
- package/_modules/knowledge-engine/src/runtime/README.md +7 -0
- package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
- package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
- package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
- package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
- package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
- package/_modules/knowledge-engine/src/server.js +154 -0
- package/_modules/knowledge-engine/src/store.js +236 -0
- package/_modules/knowledge-engine/ui/app.css +172 -0
- package/_modules/knowledge-engine/ui/app.js +297 -0
- package/_modules/knowledge-engine/ui/index.html +95 -0
- package/_site/README.md +30 -30
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +62 -62
- package/_site/_test/claude-workbench-test.js +170 -170
- package/_site/_test/codex-conversation-projection-test.js +2 -1
- package/_site/_test/commit-conversation-binding-test.js +95 -95
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/conversation-api-test.js +109 -109
- package/_site/_test/conversation-store-test.js +136 -136
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/make-git-repos.js +12 -1
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/fresh-hook-reimport-test.js +53 -0
- package/_site/_test/fresh-product-data-test.js +42 -0
- package/_site/_test/full-integration-e2e-test.js +165 -165
- package/_site/_test/git-validation-test.js +2 -2
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/import-preflight-api-test.js +234 -233
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +171 -171
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +106 -104
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/module-artifact-integrity-test.js +27 -0
- package/_site/_test/module-boundary-test.js +30 -0
- package/_site/_test/module-bridge-eventbridge-test.js +7 -0
- package/_site/_test/module-model-configuration-test.js +44 -0
- package/_site/_test/module-process-lifecycle-test.js +24 -0
- package/_site/_test/module-stream-proxy-test.js +34 -0
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/packaged-ui-smoke-test.js +11 -9
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/product-diagnostics-ui-test.js +75 -0
- package/_site/_test/product-import-ui-test.js +92 -0
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/release-version-sync-test.js +1 -1
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +159 -156
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/startup-analysis-disabled-test.js +10 -10
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/tracking-start-test.js +109 -108
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/workspace-ui-contract-test.js +28 -43
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-workspace.js +101 -101
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +1 -0
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/claude-cli-runner.js +1510 -1510
- package/_site/lib/commit-conversation-binder.js +234 -234
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +406 -406
- package/_site/lib/contracts.js +284 -279
- package/_site/lib/conversation-query-service.js +144 -144
- package/_site/lib/conversation-store.js +385 -385
- package/_site/lib/data-dir.js +6 -74
- package/_site/lib/engine-connection.js +67 -0
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +21 -21
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1172 -1172
- package/_site/lib/hook-manager.js +203 -202
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-installer.js +908 -907
- package/_site/lib/kb-framework.js +176 -176
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-tool-runtime.js +359 -359
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/module-bridge.js +191 -36
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/product-data.js +24 -0
- package/_site/lib/project-lifecycle-service.js +535 -497
- package/_site/lib/project-store.js +268 -262
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-endpoint.js +155 -155
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1579 -1520
- package/_site/lib/settings-store.js +133 -113
- package/_site/lib/storage-layout.js +162 -162
- package/_site/lib/structured-logger.js +574 -574
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +157 -156
- package/_site/scripts/install-module-candidates.js +69 -0
- package/_site/scripts/pack-module-candidates.js +72 -0
- package/_site/scripts/sync-release-version.js +2 -2
- package/_site/scripts/vendor-modules.js +47 -0
- package/_site/scripts/verify-module-runtime.js +45 -0
- package/_site/scripts/verify-product-runtime.js +34 -0
- package/bin/project-knowledge-mcp.js +194 -194
- package/docs/README.zh-CN.md +42 -29
- package/docs/fresh-v5-start.md +28 -0
- package/docs/project-registry-schema.md +22 -22
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +220 -0
- package/package.json +14 -9
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/project-readme.md +34 -34
- package/ui/favicon.svg +38 -38
- package/ui/index.html +52 -149
- package/ui/product.css +9 -0
- package/ui/product.js +412 -0
- package/vendor-manifest.json +45 -0
- package/_site/_test/data-dir-migration-test.js +0 -77
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/knowledge-migration-test.js +0 -75
- package/_site/_test/legacy-forward-compat-test.js +0 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
- package/_site/_test/p0-data-migration-characterization-test.js +0 -37
- package/_site/_test/p0-e2e-gate-test.js +0 -347
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- package/_site/_test/project-layout-v2-migration-test.js +0 -127
- package/_site/_test/server-runtime-migration-safety-test.js +0 -27
- package/_site/_test/task15-20-ui-flow-test.js +0 -148
- package/_site/_test/ui-i18n-toggle-test.js +0 -114
- package/_site/_test/ui-smoke-test.js +0 -73
- package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
- package/_site/lib/data-state-classifier.js +0 -40
- package/_site/lib/legacy-data-manifest.js +0 -26
- package/_site/lib/migration-service.js +0 -442
- package/ui/app.css +0 -58
- package/ui/app.js +0 -607
- package/ui/i18n.js +0 -146
|
@@ -22,9 +22,12 @@
|
|
|
22
22
|
* KB_EVENTBRIDGE_COMMAND explicit node command line for the event-bridge console
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
const { spawn } = require('child_process');
|
|
25
|
+
const { spawn, spawnSync } = require('child_process');
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const fs = require('fs');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
const { once } = require('events');
|
|
30
|
+
const { DomainError } = require('./contracts');
|
|
28
31
|
|
|
29
32
|
const PROXY_TIMEOUT_MS = 10000;
|
|
30
33
|
// Registration drives a full embedding pass in vector-hub (remote API); a
|
|
@@ -47,6 +50,10 @@ const MODULE_ENTRIES = {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
function resolveModuleEntry(name) {
|
|
53
|
+
if (name === 'knowledge-engine') {
|
|
54
|
+
const runtime = path.resolve(__dirname, '..', '..', '_modules', 'knowledge-engine', 'src', 'bin.js');
|
|
55
|
+
return fs.existsSync(runtime) ? runtime : null;
|
|
56
|
+
}
|
|
50
57
|
try { return require.resolve(MODULE_ENTRIES[name]); } catch { return null; }
|
|
51
58
|
}
|
|
52
59
|
|
|
@@ -70,14 +77,19 @@ function defaultEventBridgeUrl() {
|
|
|
70
77
|
return String(process.env.KB_EVENTBRIDGE_URL || 'http://127.0.0.1:8790').replace(/\/+$/, '');
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
function defaultEngineUrl() { return String(process.env.KB_KNOWLEDGE_ENGINE_URL || 'http://127.0.0.1:5790').replace(/\/+$/, ''); }
|
|
81
|
+
|
|
73
82
|
class ModuleBridge {
|
|
74
83
|
constructor(options = {}) {
|
|
75
84
|
this.logger = options.logger || { info: () => {}, warn: () => {}, error: () => {} };
|
|
76
85
|
this.terminalUrl = options.terminalUrl || defaultTerminalUrl();
|
|
77
86
|
this.vectorHubUrl = options.vectorHubUrl || defaultVectorHubUrl();
|
|
78
87
|
this.eventBridgeUrl = options.eventBridgeUrl || defaultEventBridgeUrl();
|
|
88
|
+
this.engineUrl = options.engineUrl || defaultEngineUrl();
|
|
89
|
+
this.runtimeToken = options.runtimeToken || crypto.randomBytes(32).toString('hex');
|
|
79
90
|
this.registryStore = options.registryStore || null;
|
|
80
91
|
this.projectStore = options.projectStore || null;
|
|
92
|
+
this.settingsStore = options.settingsStore || null;
|
|
81
93
|
this.dataDir = options.dataDir || null;
|
|
82
94
|
this.supervised = [];
|
|
83
95
|
}
|
|
@@ -110,7 +122,7 @@ class ModuleBridge {
|
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
/** Same-origin
|
|
125
|
+
/** Same-origin proxy. SSE has a headers deadline, never a whole-turn deadline. */
|
|
114
126
|
async proxy(req, res, target) {
|
|
115
127
|
const chunks = [];
|
|
116
128
|
for await (const chunk of req) chunks.push(chunk);
|
|
@@ -120,23 +132,45 @@ class ModuleBridge {
|
|
|
120
132
|
const isPicker = /\/(system\/)?pick-folder/.test(target);
|
|
121
133
|
const timeoutMs = isPicker ? PICKER_TIMEOUT_MS : PROXY_TIMEOUT_MS;
|
|
122
134
|
let upstream;
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
+
const disconnected = () => { if (!res.writableEnded) controller.abort(); };
|
|
138
|
+
res.once?.('close', disconnected);
|
|
123
139
|
try {
|
|
124
|
-
upstream = await
|
|
140
|
+
upstream = await fetch(target, {
|
|
125
141
|
method: req.method,
|
|
126
|
-
|
|
127
|
-
headers: body ? { 'Content-Type': req.headers['content-type'] || 'application/json' } : {},
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
headers: { ...(body ? { 'Content-Type': req.headers['content-type'] || 'application/json' } : {}), ...(req.headers['last-event-id'] ? { 'Last-Event-ID': req.headers['last-event-id'] } : {}) },
|
|
128
144
|
body: ['GET', 'HEAD'].includes(req.method) ? undefined : body,
|
|
129
145
|
});
|
|
130
146
|
} catch (error) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
res.off?.('close', disconnected);
|
|
149
|
+
if (res.destroyed) return;
|
|
131
150
|
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
132
151
|
res.end(JSON.stringify({ ok: false, error: 'MODULE_UNREACHABLE', target }));
|
|
133
152
|
return;
|
|
134
153
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8';
|
|
155
|
+
if (contentType.includes('text/event-stream')) {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
res.writeHead(upstream.status, { 'Content-Type': contentType, 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'X-Accel-Buffering': 'no' });
|
|
158
|
+
res.flushHeaders?.();
|
|
159
|
+
try {
|
|
160
|
+
for await (const chunk of upstream.body) {
|
|
161
|
+
if (res.destroyed) break;
|
|
162
|
+
if (!res.write(Buffer.from(chunk))) await once(res, 'drain', { signal: controller.signal });
|
|
163
|
+
}
|
|
164
|
+
if (!res.destroyed) res.end();
|
|
165
|
+
} catch { if (!res.destroyed) res.destroy(); }
|
|
166
|
+
finally { controller.abort(); res.off?.('close', disconnected); }
|
|
167
|
+
} else {
|
|
168
|
+
try {
|
|
169
|
+
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
170
|
+
res.writeHead(upstream.status, { 'Content-Type': contentType });
|
|
171
|
+
res.end(buf);
|
|
172
|
+
} finally { clearTimeout(timer); res.off?.('close', disconnected); }
|
|
173
|
+
}
|
|
140
174
|
}
|
|
141
175
|
|
|
142
176
|
proxyTerminal(req, res, pathname, search) {
|
|
@@ -151,6 +185,23 @@ class ModuleBridge {
|
|
|
151
185
|
return this.proxy(req, res, this._eventBridgeApi(pathname) + (search || ''));
|
|
152
186
|
}
|
|
153
187
|
|
|
188
|
+
proxyEngine(req, res, pathname, search) {
|
|
189
|
+
if (this.engineCompatible === false) throw new DomainError('ENGINE_CONNECTION_PENDING', '知识引擎连接尚未确认,请查看系统状态;不会读取旧服务中的知识。', { status: 503 });
|
|
190
|
+
return this.proxy(req, res, `${this.engineUrl}/api/${pathname.replace(/^\/api\/engine\/?/, '')}${search || ''}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
proxyModuleUi(req, res, module, rest, search) {
|
|
194
|
+
const base = { terminal: this.terminalUrl, vectorhub: this.vectorHubUrl, eventbridge: this.eventBridgeUrl, engine: this.engineUrl }[module];
|
|
195
|
+
let decoded;
|
|
196
|
+
try { decoded = decodeURIComponent(rest); } catch { decoded = null; }
|
|
197
|
+
// Reject encoded separators/dot segments before URL normalization. Static
|
|
198
|
+
// mounts must never become a second route to a module's private API.
|
|
199
|
+
if (!base || req.method !== 'GET' || decoded === null || /[\\\x00-\x1f]/.test(decoded) || decoded.split('/').some(part => ['.', '..', 'api', 'internal'].includes(part.toLowerCase())) || /%[0-9a-f]{2}/i.test(decoded)) { res.statusCode = 404; res.end('Not found'); return; }
|
|
200
|
+
const target = new URL(`${base}/${rest}${search || ''}`);
|
|
201
|
+
if (target.origin !== new URL(base).origin) { res.statusCode = 404; res.end('Not found'); return; }
|
|
202
|
+
return this.proxy(req, res, target.href);
|
|
203
|
+
}
|
|
204
|
+
|
|
154
205
|
async _listTerminalProjects() {
|
|
155
206
|
try {
|
|
156
207
|
const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`);
|
|
@@ -179,12 +230,23 @@ class ModuleBridge {
|
|
|
179
230
|
} catch { return []; }
|
|
180
231
|
}
|
|
181
232
|
|
|
233
|
+
async _listEngineProjects() {
|
|
234
|
+
if (this.engineCompatible === false) return [];
|
|
235
|
+
try {
|
|
236
|
+
const response = await this._fetch(`${this.engineUrl}/api/projects`);
|
|
237
|
+
if (!response.ok) return [];
|
|
238
|
+
const payload = await response.json();
|
|
239
|
+
return Array.isArray(payload.projects) ? payload.projects : [];
|
|
240
|
+
} catch { return []; }
|
|
241
|
+
}
|
|
242
|
+
|
|
182
243
|
/** Aggregated projection for the shell sidebar: pkb registry is the spine. */
|
|
183
244
|
async aggregatedProjects() {
|
|
184
|
-
const [terminalProjects, vectorHubProjects, eventBridgeProjects] = await Promise.all([
|
|
245
|
+
const [terminalProjects, vectorHubProjects, eventBridgeProjects, engineProjects] = await Promise.all([
|
|
185
246
|
this._listTerminalProjects(),
|
|
186
247
|
this._listVectorHubProjects(),
|
|
187
248
|
this._listEventBridgeProjects(),
|
|
249
|
+
this._listEngineProjects(),
|
|
188
250
|
]);
|
|
189
251
|
const ids = this.registryStore ? this.registryStore.listIds() : [];
|
|
190
252
|
const byWorkspace = new Map(terminalProjects.map(p => [String(p.rootPath || '').toLowerCase(), p]));
|
|
@@ -209,10 +271,13 @@ class ModuleBridge {
|
|
|
209
271
|
name: config.displayName || projectId,
|
|
210
272
|
workspacePath,
|
|
211
273
|
knowledgePath,
|
|
274
|
+
brainId: config.brainId || 'brain-default',
|
|
275
|
+
domainId: config.domainId || null,
|
|
212
276
|
modules: {
|
|
213
277
|
terminal: terminal ? { registered: true, projectId: terminal.id, name: terminal.name } : { registered: false, status: wiring.terminal && wiring.terminal.status },
|
|
214
278
|
vectorHub: vectorHub ? { registered: true, projectName: vectorHub.name, docCount: vectorHub.docCount, chunkCount: vectorHub.chunkCount, watching: !!vectorHub.watching } : { registered: false, status: wiring.vectorHub && wiring.vectorHub.status },
|
|
215
279
|
eventBridge: eventBridge ? { registered: true, projectId: eventBridge.id, turns: eventBridge.turns, auto: !!eventBridge.auto } : { registered: false, status: wiring.eventBridge && wiring.eventBridge.status },
|
|
280
|
+
engine: engineProjects.some(p => p.projectId === projectId) ? { registered: true, projectId } : { registered: false, status: wiring.engine && wiring.engine.status },
|
|
216
281
|
},
|
|
217
282
|
};
|
|
218
283
|
});
|
|
@@ -221,26 +286,68 @@ class ModuleBridge {
|
|
|
221
286
|
terminalAvailable: terminalProjects.length > 0 || await this._probe(this.terminalUrl),
|
|
222
287
|
vectorHubAvailable: vectorHubProjects.length > 0 || await this._probe(this.vectorHubUrl),
|
|
223
288
|
eventBridgeAvailable: eventBridgeProjects.length > 0 || await this._probe(this.eventBridgeUrl),
|
|
289
|
+
engineAvailable: engineProjects.length > 0 || await this._probe(this.engineUrl),
|
|
224
290
|
};
|
|
225
291
|
}
|
|
226
292
|
|
|
227
293
|
async _probe(base) {
|
|
228
|
-
|
|
294
|
+
const route = base === this.terminalUrl ? '/api/claude-workbench/v1/health' : '/api/health';
|
|
295
|
+
try { const response = await this._fetch(`${base}${route}`, { timeoutMs: 2000 }); return response.ok; } catch { return false; }
|
|
229
296
|
}
|
|
230
297
|
|
|
231
298
|
async modulesHealth() {
|
|
232
|
-
const [terminal, vectorHub, eventBridge] = await Promise.all([
|
|
299
|
+
const [terminal, vectorHub, eventBridge, engine] = await Promise.all([
|
|
233
300
|
this._probe(this.terminalUrl),
|
|
234
301
|
this._probe(this.vectorHubUrl),
|
|
235
302
|
this._probe(this.eventBridgeUrl),
|
|
303
|
+
this._probe(this.engineUrl),
|
|
236
304
|
]);
|
|
237
305
|
return { ok: true, modules: {
|
|
238
306
|
terminal: { url: this.terminalUrl, up: terminal },
|
|
239
307
|
vectorHub: { url: this.vectorHubUrl, up: vectorHub },
|
|
240
308
|
eventBridge: { url: this.eventBridgeUrl, up: eventBridge },
|
|
309
|
+
engine: { url: this.engineUrl, up: engine },
|
|
241
310
|
} };
|
|
242
311
|
}
|
|
243
312
|
|
|
313
|
+
async syncEngineConfiguration() {
|
|
314
|
+
if (this.syncingConfig) return this.configStatus || { scopes: false, model: false };
|
|
315
|
+
this.syncingConfig = true;
|
|
316
|
+
const status = { scopes: false, model: false, embedding: false };
|
|
317
|
+
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.runtimeToken}` };
|
|
318
|
+
try {
|
|
319
|
+
if (this.settingsStore) {
|
|
320
|
+
const settings = this.settingsStore.read();
|
|
321
|
+
if (settings.knowledge.rootPath) {
|
|
322
|
+
const brain = settings.brain;
|
|
323
|
+
const root = path.join(settings.knowledge.rootPath, '_shared');
|
|
324
|
+
const scopes = [{ id: brain.id, kind: 'brain', brainId: brain.id, name: brain.name, knowledgePath: path.join(root, brain.id) }, ...brain.domains.map(d => ({ id: d.id, kind: 'domain', brainId: brain.id, domainId: d.id, name: d.name, knowledgePath: path.join(root, d.id) }))];
|
|
325
|
+
status.scopes = (await this._fetch(`${this.engineUrl}/api/scopes`, { method: 'POST', headers, body: JSON.stringify({ scopes }) })).ok;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch { /* A scope failure must not prevent revoking a stale model key. */ }
|
|
329
|
+
try {
|
|
330
|
+
let profile = null;
|
|
331
|
+
try {
|
|
332
|
+
const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/maintenance-config`, { method: 'POST', headers, body: '{}' });
|
|
333
|
+
if (response.ok) profile = await response.json();
|
|
334
|
+
} catch { /* Missing configuration must clear a previously resolved key. */ }
|
|
335
|
+
const response = await this._fetch(`${this.engineUrl}/api/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ projectId: 'default', profile }) });
|
|
336
|
+
status.model = response.ok && !!profile;
|
|
337
|
+
} catch { /* Status projection only: never log resolved profiles or raw errors. */ }
|
|
338
|
+
try {
|
|
339
|
+
let settings = null;
|
|
340
|
+
try {
|
|
341
|
+
const resolved = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
|
|
342
|
+
if (resolved.ok) settings = await resolved.json();
|
|
343
|
+
} catch { /* Revocation wins over a previously cached runtime key. */ }
|
|
344
|
+
const applied = await this._fetch(`${this.vectorHubUrl}/api/internal/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ settings }) });
|
|
345
|
+
status.embedding = applied.ok && !!settings;
|
|
346
|
+
} catch { /* Public status contains no resolved configuration. */ }
|
|
347
|
+
this.syncingConfig = false; this.configStatus = status;
|
|
348
|
+
return status;
|
|
349
|
+
}
|
|
350
|
+
|
|
244
351
|
/**
|
|
245
352
|
* Register one pkb project into both modules and persist the wiring table.
|
|
246
353
|
* Best-effort per module: a module that is down is recorded as 'pending'
|
|
@@ -248,11 +355,12 @@ class ModuleBridge {
|
|
|
248
355
|
* KB_MODULES_REGISTER=0 disables fan-out entirely (test isolation).
|
|
249
356
|
*/
|
|
250
357
|
async registerProjectLinks({ projectId, name, workspacePath, knowledgePath }) {
|
|
251
|
-
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
|
|
358
|
+
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false }, engine: { ok: false } };
|
|
252
359
|
if (String(process.env.KB_MODULES_REGISTER || '') === '0') {
|
|
253
360
|
result.terminal = { ok: false, status: 'disabled' };
|
|
254
361
|
result.vectorHub = { ok: false, status: 'disabled' };
|
|
255
362
|
result.eventBridge = { ok: false, status: 'disabled' };
|
|
363
|
+
result.engine = { ok: false, status: 'disabled' };
|
|
256
364
|
return result;
|
|
257
365
|
}
|
|
258
366
|
// 幂等登记:终端已有同 rootPath 的项目时复用,绝不重复创建
|
|
@@ -273,12 +381,14 @@ class ModuleBridge {
|
|
|
273
381
|
result.terminal = { ok: false, status: 'pending' };
|
|
274
382
|
}
|
|
275
383
|
}
|
|
276
|
-
|
|
384
|
+
const existingVector = (await this._listVectorHubProjects()).find(p => p.name === projectId && String(p.sourceDir || '').toLowerCase() === String(knowledgePath || '').toLowerCase());
|
|
385
|
+
if (existingVector) result.vectorHub = { ok: true, projectName: projectId, status: 'registered', reused: true };
|
|
386
|
+
else try {
|
|
277
387
|
const response = await this._fetch(`${this.vectorHubUrl}/api/import`, {
|
|
278
388
|
method: 'POST',
|
|
279
389
|
headers: { 'Content-Type': 'application/json' },
|
|
280
390
|
timeoutMs: REGISTER_TIMEOUT_MS,
|
|
281
|
-
body: JSON.stringify({ sourceDir: knowledgePath, projectName: projectId, watch:
|
|
391
|
+
body: JSON.stringify({ sourceDir: knowledgePath, projectName: projectId, watch: false }),
|
|
282
392
|
});
|
|
283
393
|
result.vectorHub = { ok: response.ok, projectName: projectId, status: response.ok ? 'registered' : 'error' };
|
|
284
394
|
} catch {
|
|
@@ -307,10 +417,26 @@ class ModuleBridge {
|
|
|
307
417
|
result.eventBridge = { ok: false, status: 'pending' };
|
|
308
418
|
}
|
|
309
419
|
}
|
|
420
|
+
const config = this.projectStore ? this.projectStore.readConfig(projectId) : {};
|
|
421
|
+
const state = this.projectStore ? this.projectStore.readState(projectId) : {};
|
|
422
|
+
try {
|
|
423
|
+
const baselines = { ...(state.conversationBaselineCursors || {}), global: state.conversationBaselineCursor ?? 0 };
|
|
424
|
+
const journalId = result.eventBridge.projectId || state.modules?.eventBridge?.projectId;
|
|
425
|
+
if (journalId && baselines[journalId] === undefined) {
|
|
426
|
+
const watermark = result.eventBridge.reused && await this._fetch(`${this.eventBridgeUrl}/api/journal/high-watermark?journalId=${encodeURIComponent(journalId)}`);
|
|
427
|
+
const current = result.eventBridge.reused ? watermark.ok && await watermark.json() : { sequence: 0 };
|
|
428
|
+
if (!current || !Number.isSafeInteger(current.sequence)) throw new Error('JOURNAL_BASELINE_UNAVAILABLE');
|
|
429
|
+
baselines[journalId] = current.sequence;
|
|
430
|
+
if (this.projectStore) await this.projectStore.updateState(projectId, draft => { draft.conversationBaselineCursors = baselines; });
|
|
431
|
+
}
|
|
432
|
+
const response = await this._fetch(`${this.engineUrl}/api/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId, name: name || projectId, repoPath: workspacePath, knowledgePath, knowledgeLanguage: config.knowledgeLanguage || 'zh-CN', brainId: config.brainId || 'brain-default', domainId: config.domainId || null, repoIdentity: config.repoIdentity, journalId: journalId || null, vectorProjectId: projectId, trackingStartCommit: state.trackingStartCommit || state.lastAnalyzedCommit || null, conversationBaselineCursor: state.conversationBaselineCursor ?? 0, conversationBaselineCursors: baselines, profileId: config.aiProfileId || null }) });
|
|
433
|
+
result.engine = { ok: response.ok, projectId, status: response.ok ? 'registered' : 'error' };
|
|
434
|
+
} catch { result.engine = { ok: false, status: 'pending' }; }
|
|
310
435
|
await this._writeWiring(projectId, wiring => {
|
|
311
436
|
wiring.terminal = { ...wiring.terminal, ...result.terminal, name: name || projectId, rootPath: workspacePath, updatedAt: new Date().toISOString() };
|
|
312
437
|
wiring.vectorHub = { ...wiring.vectorHub, ...result.vectorHub, sourceDir: knowledgePath, updatedAt: new Date().toISOString() };
|
|
313
438
|
wiring.eventBridge = { ...wiring.eventBridge, ...result.eventBridge, path: workspacePath, store: knowledgePath ? path.join(knowledgePath, 'dev-conversations') : '', updatedAt: new Date().toISOString() };
|
|
439
|
+
wiring.engine = { ...wiring.engine, ...result.engine, updatedAt: new Date().toISOString() };
|
|
314
440
|
});
|
|
315
441
|
this.logger.info('modules.register', 'Module registration finished.', { context: { projectId, terminal: result.terminal.status, vectorHub: result.vectorHub.status, eventBridge: result.eventBridge.status } });
|
|
316
442
|
return result;
|
|
@@ -320,9 +446,19 @@ class ModuleBridge {
|
|
|
320
446
|
* only drops its derived index (source untouched); terminal removes the project
|
|
321
447
|
* with keepSessions=1 so session records survive; the event-bridge console only
|
|
322
448
|
* unregisters the project — journals and sealed commit documents stay on disk. */
|
|
323
|
-
async removeProjectLinks({ projectId }) {
|
|
449
|
+
async removeProjectLinks({ projectId, requireEngine = true }) {
|
|
324
450
|
const wiring = (this.projectStore ? this.projectStore.readState(projectId).modules : null) || {};
|
|
325
|
-
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
|
|
451
|
+
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false }, engine: { ok: false } };
|
|
452
|
+
// Stop the writer first, before removing hooks/index registrations.
|
|
453
|
+
try {
|
|
454
|
+
if (!requireEngine) result.engine = { ok: true, status: 'not-owner' };
|
|
455
|
+
else {
|
|
456
|
+
const response = await this._fetch(`${this.engineUrl}/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE' });
|
|
457
|
+
if (response.status === 409) throw new DomainError('PROJECT_BUSY', '此项目正在执行知识维护,请等待写入结束后再解绑。未开始的排队任务会随解绑取消。', { status: 409, retryable: true });
|
|
458
|
+
result.engine = { ok: response.ok || response.status === 404, status: response.ok || response.status === 404 ? 'removed' : 'pending' };
|
|
459
|
+
}
|
|
460
|
+
} catch (error) { if (error.code === 'PROJECT_BUSY') throw error; result.engine = { ok: false, status: 'pending' }; }
|
|
461
|
+
if (!result.engine.ok) throw new DomainError('PROJECT_BUSY', '知识引擎暂时不可用,无法安全停止写入;请恢复后再解绑。', { status: 409, retryable: true });
|
|
326
462
|
let terminalId = wiring.terminal && wiring.terminal.projectId;
|
|
327
463
|
if (!terminalId) {
|
|
328
464
|
const workspacePath = wiring.terminal && wiring.terminal.rootPath;
|
|
@@ -363,6 +499,7 @@ class ModuleBridge {
|
|
|
363
499
|
if (result.terminal.ok) wiring.terminal = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
364
500
|
if (result.vectorHub.ok) wiring.vectorHub = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
365
501
|
if (result.eventBridge.ok) wiring.eventBridge = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
502
|
+
if (result.engine.ok) wiring.engine = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
366
503
|
});
|
|
367
504
|
this.logger.info('modules.remove', 'Module de-registration finished.', { context: { projectId, terminal: result.terminal, vectorHub: result.vectorHub, eventBridge: result.eventBridge } });
|
|
368
505
|
return result;
|
|
@@ -396,7 +533,7 @@ class ModuleBridge {
|
|
|
396
533
|
_supervisedSpecs() {
|
|
397
534
|
const specs = [];
|
|
398
535
|
if (process.env.KB_TERMINAL_COMMAND) {
|
|
399
|
-
specs.push({ name: 'terminal', command: process.env.KB_TERMINAL_COMMAND, url: this.terminalUrl, env: {} });
|
|
536
|
+
specs.push({ name: 'terminal', command: process.env.KB_TERMINAL_COMMAND, url: this.terminalUrl, env: { KB_WORKBENCH_RUNTIME_TOKEN: this.runtimeToken, KB_KNOWLEDGE_ENGINE_URL: this.engineUrl, KB_EVENT_BRIDGE_URL: this.eventBridgeUrl } });
|
|
400
537
|
} else {
|
|
401
538
|
const terminalEntry = resolveModuleEntry('terminal');
|
|
402
539
|
if (terminalEntry) {
|
|
@@ -404,7 +541,7 @@ class ModuleBridge {
|
|
|
404
541
|
name: 'terminal',
|
|
405
542
|
command: `node ${JSON.stringify(terminalEntry)}`,
|
|
406
543
|
url: this.terminalUrl,
|
|
407
|
-
env: { AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl) },
|
|
544
|
+
env: { AGENT_TERMINAL_DATA_DIR: path.join(this.dataDir || require('./data-dir').getDataDir(), 'workbench'), AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl), KB_WORKBENCH_RUNTIME_TOKEN: this.runtimeToken, KB_KNOWLEDGE_ENGINE_URL: this.engineUrl, KB_EVENT_BRIDGE_URL: this.eventBridgeUrl },
|
|
408
545
|
});
|
|
409
546
|
} else {
|
|
410
547
|
this.logger.warn('modules.supervisor', 'claude-ai-workbench runtime not installed; expecting an external service.', {});
|
|
@@ -413,7 +550,7 @@ class ModuleBridge {
|
|
|
413
550
|
// vector-hub declares engines >= 22; spawning it on older runtimes would
|
|
414
551
|
// crash-loop the supervisor for nothing.
|
|
415
552
|
if (process.env.KB_VECTORHUB_COMMAND) {
|
|
416
|
-
specs.push({ name: 'vector-hub', command: process.env.KB_VECTORHUB_COMMAND, url: this.vectorHubUrl, env: {} });
|
|
553
|
+
specs.push({ name: 'vector-hub', command: process.env.KB_VECTORHUB_COMMAND, url: this.vectorHubUrl, env: { KB_VECTOR_MANAGED: '1', KB_VECTOR_RUNTIME_TOKEN: this.runtimeToken } });
|
|
417
554
|
} else if (parseInt(process.versions.node, 10) < 22) {
|
|
418
555
|
this.logger.warn('modules.supervisor', 'vector-hub needs Node >= 22; not spawning the vendored runtime.', { context: { node: process.versions.node } });
|
|
419
556
|
} else {
|
|
@@ -425,9 +562,9 @@ class ModuleBridge {
|
|
|
425
562
|
const root = path.join(dataDir, 'vectorhub');
|
|
426
563
|
specs.push({
|
|
427
564
|
name: 'vector-hub',
|
|
428
|
-
command: `node ${JSON.stringify(vectorHubEntry)} serve --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
565
|
+
command: `node ${JSON.stringify(vectorHubEntry)} serve --host ${urlHost(this.vectorHubUrl)} --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
429
566
|
url: this.vectorHubUrl,
|
|
430
|
-
env: { VECTOR_HUB_ROOT: root },
|
|
567
|
+
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG: path.join(root, 'config.json'), VECTOR_HUB_LEGACY_CONFIG: path.join(root, 'no-legacy-config.json'), KB_VECTOR_MANAGED: '1', KB_VECTOR_RUNTIME_TOKEN: this.runtimeToken },
|
|
431
568
|
});
|
|
432
569
|
}
|
|
433
570
|
}
|
|
@@ -438,41 +575,48 @@ class ModuleBridge {
|
|
|
438
575
|
// folder-picker warm-up is an interactive-only feature the shell never
|
|
439
576
|
// routes here (import uses the terminal/vector-hub pickers) — skip it.
|
|
440
577
|
if (process.env.KB_EVENTBRIDGE_COMMAND) {
|
|
441
|
-
specs.push({ name: 'event-bridge', command: process.env.KB_EVENTBRIDGE_COMMAND, url: this.eventBridgeUrl, env: {} });
|
|
578
|
+
specs.push({ name: 'event-bridge', command: process.env.KB_EVENTBRIDGE_COMMAND, url: this.eventBridgeUrl, env: { KB_EVENT_BRIDGE_RUNTIME_TOKEN: this.runtimeToken } });
|
|
442
579
|
} else {
|
|
443
580
|
const eventBridgeEntry = resolveModuleEntry('event-bridge');
|
|
444
581
|
if (!eventBridgeEntry) {
|
|
445
582
|
this.logger.warn('modules.supervisor', '@sanqianx/ai-coding-event-bridge-console runtime not installed; expecting an external service.', {});
|
|
446
583
|
} else {
|
|
447
|
-
const homeArgs = process.env.AI_CODING_EVENT_BRIDGE_HOME
|
|
448
|
-
? ` --home ${JSON.stringify(process.env.AI_CODING_EVENT_BRIDGE_HOME)}`
|
|
449
|
-
: '';
|
|
584
|
+
const homeArgs = ` --home ${JSON.stringify(process.env.AI_CODING_EVENT_BRIDGE_HOME || path.join(this.dataDir || require('./data-dir').getDataDir(), 'events'))}`;
|
|
450
585
|
specs.push({
|
|
451
586
|
name: 'event-bridge',
|
|
452
587
|
command: `node ${JSON.stringify(eventBridgeEntry)} serve --host ${urlHost(this.eventBridgeUrl)} --port ${urlPort(this.eventBridgeUrl, 8790)}${homeArgs}`,
|
|
453
588
|
url: this.eventBridgeUrl,
|
|
454
|
-
env: { BRIDGE_CONSOLE_PICK_FOLDER: '0' },
|
|
589
|
+
env: { BRIDGE_CONSOLE_PICK_FOLDER: '0', KB_EVENT_BRIDGE_RUNTIME_TOKEN: this.runtimeToken },
|
|
455
590
|
});
|
|
456
591
|
}
|
|
457
592
|
}
|
|
593
|
+
const engineEntry = resolveModuleEntry('knowledge-engine');
|
|
594
|
+
if (process.env.KB_KNOWLEDGE_ENGINE_COMMAND || engineEntry) specs.push({ name: 'knowledge-engine', command: process.env.KB_KNOWLEDGE_ENGINE_COMMAND || `node ${JSON.stringify(engineEntry)} serve --port ${urlPort(this.engineUrl, 5790)} --host ${urlHost(this.engineUrl)}`, url: this.engineUrl, env: { KNOWLEDGE_ENGINE_DATA: path.join(this.dataDir || require('./data-dir').getDataDir(), 'knowledge-engine'), KB_ENGINE_MANAGED: '1', KNOWLEDGE_ENGINE_CONSUME: '0', KB_ENGINE_RUNTIME_TOKEN: this.runtimeToken, KB_EVENT_BRIDGE_URL: this.eventBridgeUrl, KB_VECTOR_HUB_URL: this.vectorHubUrl, NODE_PATH: path.resolve(__dirname, '..', '..', 'node_modules') } });
|
|
458
595
|
return specs;
|
|
459
596
|
}
|
|
460
597
|
|
|
461
598
|
_spawnSupervised(spec) {
|
|
599
|
+
if (this.stopped) return;
|
|
462
600
|
// Strip the quoting: spawn takes argv, not a shell line — a literal
|
|
463
601
|
// `"path"` argument makes node load a file whose name contains quotes.
|
|
464
602
|
const parts = (spec.command.match(/"[^"]+"|\S+/g) || [])
|
|
465
|
-
.map(part => (part.startsWith('"')
|
|
466
|
-
const
|
|
603
|
+
.map(part => { if (!part.startsWith('"') || !part.endsWith('"')) return part; try { return JSON.parse(part); } catch { return part.slice(1, -1); } });
|
|
604
|
+
const nodeProcess = parts[0] === 'node' || parts[0] === process.execPath;
|
|
605
|
+
const child = spawn(nodeProcess ? process.execPath : parts[0], parts.slice(1), {
|
|
467
606
|
// Module banners (ports, data roots) are diagnostics — inherit stdout
|
|
468
607
|
// so they land on the console in --fg mode and in launcher.log when the
|
|
469
608
|
// shell itself runs detached.
|
|
470
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
609
|
+
stdio: nodeProcess ? ['ignore', 'inherit', 'inherit', 'ipc'] : ['ignore', 'inherit', 'inherit'],
|
|
471
610
|
windowsHide: true,
|
|
472
611
|
env: { ...process.env, ...(spec.env || {}) },
|
|
473
612
|
});
|
|
474
|
-
let attempts = 0;
|
|
613
|
+
let attempts = spec._attempts || 0;
|
|
614
|
+
child.on('error', error => {
|
|
615
|
+
this.supervised = this.supervised.filter(existing => existing !== child);
|
|
616
|
+
this.logger.warn('modules.supervisor', 'Module process could not start.', { context: { name: spec.name, code: error.code || 'SPAWN_FAILED' } });
|
|
617
|
+
});
|
|
475
618
|
child.on('exit', code => {
|
|
619
|
+
this.supervised = this.supervised.filter(existing => existing !== child);
|
|
476
620
|
attempts += 1;
|
|
477
621
|
if (attempts > 5 || this.stopped) {
|
|
478
622
|
this.logger.warn('modules.supervisor', 'Module service stopped.', { context: { name: spec.name, code, attempts } });
|
|
@@ -480,17 +624,28 @@ class ModuleBridge {
|
|
|
480
624
|
}
|
|
481
625
|
const delay = Math.min(1000 * 2 ** attempts, 15000);
|
|
482
626
|
this.logger.warn('modules.supervisor', 'Module service exited; restarting.', { context: { name: spec.name, code, retryInMs: delay } });
|
|
483
|
-
setTimeout(() => this._spawnSupervised({ ...spec, _attempts: attempts }), delay);
|
|
627
|
+
const timer = setTimeout(() => { this.restartTimers.delete(timer); this._spawnSupervised({ ...spec, _attempts: attempts }); }, delay);
|
|
628
|
+
this.restartTimers = this.restartTimers || new Set(); this.restartTimers.add(timer);
|
|
484
629
|
});
|
|
485
630
|
this.supervised.push(child);
|
|
486
631
|
this.logger.info('modules.supervisor', 'Module service spawned.', { context: { name: spec.name, pid: child.pid } });
|
|
487
632
|
}
|
|
488
633
|
|
|
489
|
-
stopSupervised() {
|
|
634
|
+
async stopSupervised() {
|
|
490
635
|
this.stopped = true;
|
|
491
|
-
for (const
|
|
636
|
+
for (const timer of this.restartTimers || []) clearTimeout(timer);
|
|
637
|
+
await Promise.all(this.supervised.map(child => new Promise(resolve => {
|
|
638
|
+
if (child.exitCode !== null || !child.pid) return resolve();
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
if (process.platform === 'win32') spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
641
|
+
else try { child.kill('SIGKILL'); } catch {}
|
|
642
|
+
resolve();
|
|
643
|
+
}, 5000);
|
|
644
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
645
|
+
try { if (child.connected) child.send({ type: 'kb:shutdown' }, error => { if (error) try { child.kill(); } catch {} }); else child.kill(); } catch { try { child.kill(); } catch {} }
|
|
646
|
+
})));
|
|
492
647
|
this.supervised = [];
|
|
493
648
|
}
|
|
494
649
|
}
|
|
495
650
|
|
|
496
|
-
module.exports = { ModuleBridge, defaultTerminalUrl, defaultVectorHubUrl, defaultEventBridgeUrl };
|
|
651
|
+
module.exports = { ModuleBridge, defaultTerminalUrl, defaultVectorHubUrl, defaultEventBridgeUrl, defaultEngineUrl };
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { SCHEMAS, DomainError, validateProjectId } = require('./contracts');
|
|
4
|
-
const { StorageLayout } = require('./storage-layout');
|
|
5
|
-
const { ProjectRegistryStore } = require('./project-registry-store');
|
|
6
|
-
const { ProjectStore } = require('./project-store');
|
|
7
|
-
const { runGit } = require('./project-lifecycle-service');
|
|
8
|
-
const { CommitReconciler, reconcileProjectCommits } = require('./commit-reconciler');
|
|
9
|
-
const { validateSha } = require('./scanner');
|
|
10
|
-
const { renderCommitPrompt } = require('./commit-prompt');
|
|
11
|
-
|
|
12
|
-
function stores(deps = {}) {
|
|
13
|
-
const layout = deps.layout || new StorageLayout(deps);
|
|
14
|
-
return {
|
|
15
|
-
layout,
|
|
16
|
-
registryStore: deps.registryStore || new ProjectRegistryStore({ layout }),
|
|
17
|
-
projectStore: deps.projectStore || new ProjectStore({ layout }),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function handlePostCommitEvent(event = {}, deps = {}) {
|
|
22
|
-
if (event.schema !== SCHEMAS.hookEvent) throw new DomainError('SCHEMA_UNSUPPORTED', 'Hook event must use hook-event/v2.', { status: 409 });
|
|
23
|
-
const projectId = validateProjectId(event.projectId);
|
|
24
|
-
const commitSha = validateSha(event.head, 'Hook event head');
|
|
25
|
-
if (!event.repoRoot) throw new DomainError('INVALID_ARGUMENT', 'Hook event repoRoot is required.');
|
|
26
|
-
const resolved = stores(deps);
|
|
27
|
-
if (!resolved.registryStore.readDisplaySnapshot(projectId)) throw new DomainError('PROJECT_NOT_FOUND', 'Hook projectId is not registered.', { status: 404 });
|
|
28
|
-
const config = resolved.projectStore.readConfig(projectId);
|
|
29
|
-
const runtimeRoot = path.resolve(runGit(path.resolve(event.repoRoot), ['rev-parse', '--show-toplevel']).stdout);
|
|
30
|
-
const commonDir = path.resolve(runGit(runtimeRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir']).stdout);
|
|
31
|
-
if (config.repoIdentity && config.repoIdentity.commonDir
|
|
32
|
-
&& !resolved.layout.pathsEqual(config.repoIdentity.commonDir, commonDir)) {
|
|
33
|
-
if (fs.existsSync(config.repoPath)) {
|
|
34
|
-
throw new DomainError('PROJECT_NOT_FOUND', 'Hook Git identity does not match projectId.', { status: 404 });
|
|
35
|
-
}
|
|
36
|
-
const state = resolved.projectStore.readState(projectId);
|
|
37
|
-
const baseline = state.lastAnalyzedCommit || state.trackingStartCommit;
|
|
38
|
-
if (baseline) {
|
|
39
|
-
const reachable = runGit(runtimeRoot, ['merge-base', '--is-ancestor', baseline, commitSha], { allowFailure: true });
|
|
40
|
-
if (!reachable.ok) throw new DomainError('PROJECT_NOT_FOUND', 'Moved Hook repository does not contain the project baseline.', { status: 404 });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (!resolved.layout.pathsEqual(config.repoPath, runtimeRoot)) {
|
|
44
|
-
// Only the versioned Hook path reaches this update, after projectId and Git identity checks above.
|
|
45
|
-
await resolved.projectStore.updateConfig(projectId, { repoPath: runtimeRoot }, { allowRepoPath: true });
|
|
46
|
-
}
|
|
47
|
-
if (event.boundary && event.boundary.status === 'captured' && event.boundary.boundary) {
|
|
48
|
-
const boundary = event.boundary.boundary;
|
|
49
|
-
if (boundary.projectId !== projectId || boundary.commitSha !== commitSha) {
|
|
50
|
-
throw new DomainError('INVALID_ARGUMENT', 'Hook boundary identity does not match the Hook event.');
|
|
51
|
-
}
|
|
52
|
-
const commitExists = runGit(runtimeRoot, ['cat-file', '-e', `${boundary.commitSha}^{commit}`], { allowFailure: true });
|
|
53
|
-
if (!commitExists.ok) throw new DomainError('INVALID_ARGUMENT', 'Hook boundary commit does not exist in the repository.');
|
|
54
|
-
// Ordering (T13): the boundary was appended before the Hook notified us;
|
|
55
|
-
// drain the journal THROUGH the boundary sequence before any snapshot is
|
|
56
|
-
// bound, so every same-workspace event <= boundaryEndCursor is already in
|
|
57
|
-
// the ConversationStore (or deterministically handled) at freeze time.
|
|
58
|
-
if (deps.bridgeConsumerService && typeof deps.bridgeConsumerService.drainThrough === 'function') {
|
|
59
|
-
await deps.bridgeConsumerService.drainThrough(Number(boundary.journalSequence), 'commit-boundary');
|
|
60
|
-
}
|
|
61
|
-
if (deps.conversationStore && typeof deps.conversationStore.writeBoundary === 'function') {
|
|
62
|
-
deps.conversationStore.writeBoundary(projectId, boundary);
|
|
63
|
-
}
|
|
64
|
-
} else if (event.boundary && event.boundary.status === 'unavailable' && deps.logger && typeof deps.logger.warn === 'function') {
|
|
65
|
-
await deps.logger.warn('conversation.boundary_gap', 'Conversation boundary capture gap was reported by the Git Hook.', {
|
|
66
|
-
projectId,
|
|
67
|
-
operationId: deps.operationId || event.operationId || '',
|
|
68
|
-
commitSha: event.head || '',
|
|
69
|
-
phase: 'boundary',
|
|
70
|
-
context: {
|
|
71
|
-
gapId: event.boundary.gap && event.boundary.gap.gapId || '',
|
|
72
|
-
reason: event.boundary.gap && event.boundary.gap.reason || 'unavailable',
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const reconciler = deps.reconciler instanceof CommitReconciler ? deps.reconciler : new CommitReconciler({ ...deps, ...resolved });
|
|
77
|
-
return reconciler.processCommitEvent({ projectId, commitSha, branch: event.branch || '', operationId: deps.operationId || event.operationId || '' });
|
|
78
|
-
}
|
|
79
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { SCHEMAS, DomainError, validateProjectId } = require('./contracts');
|
|
4
|
+
const { StorageLayout } = require('./storage-layout');
|
|
5
|
+
const { ProjectRegistryStore } = require('./project-registry-store');
|
|
6
|
+
const { ProjectStore } = require('./project-store');
|
|
7
|
+
const { runGit } = require('./project-lifecycle-service');
|
|
8
|
+
const { CommitReconciler, reconcileProjectCommits } = require('./commit-reconciler');
|
|
9
|
+
const { validateSha } = require('./scanner');
|
|
10
|
+
const { renderCommitPrompt } = require('./commit-prompt');
|
|
11
|
+
|
|
12
|
+
function stores(deps = {}) {
|
|
13
|
+
const layout = deps.layout || new StorageLayout(deps);
|
|
14
|
+
return {
|
|
15
|
+
layout,
|
|
16
|
+
registryStore: deps.registryStore || new ProjectRegistryStore({ layout }),
|
|
17
|
+
projectStore: deps.projectStore || new ProjectStore({ layout }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function handlePostCommitEvent(event = {}, deps = {}) {
|
|
22
|
+
if (event.schema !== SCHEMAS.hookEvent) throw new DomainError('SCHEMA_UNSUPPORTED', 'Hook event must use hook-event/v2.', { status: 409 });
|
|
23
|
+
const projectId = validateProjectId(event.projectId);
|
|
24
|
+
const commitSha = validateSha(event.head, 'Hook event head');
|
|
25
|
+
if (!event.repoRoot) throw new DomainError('INVALID_ARGUMENT', 'Hook event repoRoot is required.');
|
|
26
|
+
const resolved = stores(deps);
|
|
27
|
+
if (!resolved.registryStore.readDisplaySnapshot(projectId)) throw new DomainError('PROJECT_NOT_FOUND', 'Hook projectId is not registered.', { status: 404 });
|
|
28
|
+
const config = resolved.projectStore.readConfig(projectId);
|
|
29
|
+
const runtimeRoot = path.resolve(runGit(path.resolve(event.repoRoot), ['rev-parse', '--show-toplevel']).stdout);
|
|
30
|
+
const commonDir = path.resolve(runGit(runtimeRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir']).stdout);
|
|
31
|
+
if (config.repoIdentity && config.repoIdentity.commonDir
|
|
32
|
+
&& !resolved.layout.pathsEqual(config.repoIdentity.commonDir, commonDir)) {
|
|
33
|
+
if (fs.existsSync(config.repoPath)) {
|
|
34
|
+
throw new DomainError('PROJECT_NOT_FOUND', 'Hook Git identity does not match projectId.', { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
const state = resolved.projectStore.readState(projectId);
|
|
37
|
+
const baseline = state.lastAnalyzedCommit || state.trackingStartCommit;
|
|
38
|
+
if (baseline) {
|
|
39
|
+
const reachable = runGit(runtimeRoot, ['merge-base', '--is-ancestor', baseline, commitSha], { allowFailure: true });
|
|
40
|
+
if (!reachable.ok) throw new DomainError('PROJECT_NOT_FOUND', 'Moved Hook repository does not contain the project baseline.', { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!resolved.layout.pathsEqual(config.repoPath, runtimeRoot)) {
|
|
44
|
+
// Only the versioned Hook path reaches this update, after projectId and Git identity checks above.
|
|
45
|
+
await resolved.projectStore.updateConfig(projectId, { repoPath: runtimeRoot }, { allowRepoPath: true });
|
|
46
|
+
}
|
|
47
|
+
if (event.boundary && event.boundary.status === 'captured' && event.boundary.boundary) {
|
|
48
|
+
const boundary = event.boundary.boundary;
|
|
49
|
+
if (boundary.projectId !== projectId || boundary.commitSha !== commitSha) {
|
|
50
|
+
throw new DomainError('INVALID_ARGUMENT', 'Hook boundary identity does not match the Hook event.');
|
|
51
|
+
}
|
|
52
|
+
const commitExists = runGit(runtimeRoot, ['cat-file', '-e', `${boundary.commitSha}^{commit}`], { allowFailure: true });
|
|
53
|
+
if (!commitExists.ok) throw new DomainError('INVALID_ARGUMENT', 'Hook boundary commit does not exist in the repository.');
|
|
54
|
+
// Ordering (T13): the boundary was appended before the Hook notified us;
|
|
55
|
+
// drain the journal THROUGH the boundary sequence before any snapshot is
|
|
56
|
+
// bound, so every same-workspace event <= boundaryEndCursor is already in
|
|
57
|
+
// the ConversationStore (or deterministically handled) at freeze time.
|
|
58
|
+
if (deps.bridgeConsumerService && typeof deps.bridgeConsumerService.drainThrough === 'function') {
|
|
59
|
+
await deps.bridgeConsumerService.drainThrough(Number(boundary.journalSequence), 'commit-boundary');
|
|
60
|
+
}
|
|
61
|
+
if (deps.conversationStore && typeof deps.conversationStore.writeBoundary === 'function') {
|
|
62
|
+
deps.conversationStore.writeBoundary(projectId, boundary);
|
|
63
|
+
}
|
|
64
|
+
} else if (event.boundary && event.boundary.status === 'unavailable' && deps.logger && typeof deps.logger.warn === 'function') {
|
|
65
|
+
await deps.logger.warn('conversation.boundary_gap', 'Conversation boundary capture gap was reported by the Git Hook.', {
|
|
66
|
+
projectId,
|
|
67
|
+
operationId: deps.operationId || event.operationId || '',
|
|
68
|
+
commitSha: event.head || '',
|
|
69
|
+
phase: 'boundary',
|
|
70
|
+
context: {
|
|
71
|
+
gapId: event.boundary.gap && event.boundary.gap.gapId || '',
|
|
72
|
+
reason: event.boundary.gap && event.boundary.gap.reason || 'unavailable',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const reconciler = deps.reconciler instanceof CommitReconciler ? deps.reconciler : new CommitReconciler({ ...deps, ...resolved });
|
|
77
|
+
return reconciler.processCommitEvent({ projectId, commitSha, branch: event.branch || '', operationId: deps.operationId || event.operationId || '' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
80
|
function cleanupOrphanedRuns(_projects, deps = {}) {
|
|
81
|
-
if (!deps.projectStore && !deps.registryStore && !deps.layout) {
|
|
82
|
-
return { activeClaims: 0, recoveredFromFrozenClaims: 0, legacyStoreSkipped: true };
|
|
83
|
-
}
|
|
84
|
-
const resolved = stores(deps);
|
|
85
|
-
let activeClaims = 0;
|
|
86
|
-
for (const projectId of resolved.registryStore.listIds()) {
|
|
87
|
-
if (resolved.projectStore.readState(projectId).analysis.activeClaim) activeClaims += 1;
|
|
88
|
-
}
|
|
81
|
+
if (!deps.projectStore && !deps.registryStore && !deps.layout) {
|
|
82
|
+
return { activeClaims: 0, recoveredFromFrozenClaims: 0, legacyStoreSkipped: true };
|
|
83
|
+
}
|
|
84
|
+
const resolved = stores(deps);
|
|
85
|
+
let activeClaims = 0;
|
|
86
|
+
for (const projectId of resolved.registryStore.listIds()) {
|
|
87
|
+
if (resolved.projectStore.readState(projectId).analysis.activeClaim) activeClaims += 1;
|
|
88
|
+
}
|
|
89
89
|
return { activeClaims, recoveredFromFrozenClaims: activeClaims };
|
|
90
90
|
}
|
|
91
91
|
|