@sanqianx/project-knowledge 4.7.0 → 4.8.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 +817 -799
- package/README.md +48 -36
- package/_modules/knowledge-engine/README.md +88 -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 +255 -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/migration.js +88 -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 +155 -0
- package/_modules/knowledge-engine/src/store.js +239 -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/data-dir-migration-test.js +57 -57
- 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/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/legacy-forward-compat-test.js +257 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
- 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 +43 -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/p0-data-migration-characterization-test.js +37 -37
- package/_site/_test/p0-e2e-gate-test.js +347 -347
- 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-layout-v2-migration-test.js +127 -127
- 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/server-runtime-migration-safety-test.js +27 -27
- 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 +108 -108
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
- 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 +74 -74
- package/_site/lib/data-state-classifier.js +40 -40
- package/_site/lib/engine-cutover.js +97 -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 +907 -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/legacy-data-manifest.js +26 -26
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/migration-service.js +442 -442
- package/_site/lib/module-bridge.js +198 -33
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/project-lifecycle-service.js +519 -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 +1590 -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 +34 -25
- 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 +13 -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 +51 -149
- package/ui/product.css +9 -0
- package/ui/product.js +386 -0
- package/vendor-manifest.json +46 -0
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- 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/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,22 @@ class ModuleBridge {
|
|
|
151
185
|
return this.proxy(req, res, this._eventBridgeApi(pathname) + (search || ''));
|
|
152
186
|
}
|
|
153
187
|
|
|
188
|
+
proxyEngine(req, res, pathname, search) {
|
|
189
|
+
return this.proxy(req, res, `${this.engineUrl}/api/${pathname.replace(/^\/api\/engine\/?/, '')}${search || ''}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
proxyModuleUi(req, res, module, rest, search) {
|
|
193
|
+
const base = { terminal: this.terminalUrl, vectorhub: this.vectorHubUrl, eventbridge: this.eventBridgeUrl, engine: this.engineUrl }[module];
|
|
194
|
+
let decoded;
|
|
195
|
+
try { decoded = decodeURIComponent(rest); } catch { decoded = null; }
|
|
196
|
+
// Reject encoded separators/dot segments before URL normalization. Static
|
|
197
|
+
// mounts must never become a second route to a module's private API.
|
|
198
|
+
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; }
|
|
199
|
+
const target = new URL(`${base}/${rest}${search || ''}`);
|
|
200
|
+
if (target.origin !== new URL(base).origin) { res.statusCode = 404; res.end('Not found'); return; }
|
|
201
|
+
return this.proxy(req, res, target.href);
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
async _listTerminalProjects() {
|
|
155
205
|
try {
|
|
156
206
|
const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`);
|
|
@@ -179,12 +229,22 @@ class ModuleBridge {
|
|
|
179
229
|
} catch { return []; }
|
|
180
230
|
}
|
|
181
231
|
|
|
232
|
+
async _listEngineProjects() {
|
|
233
|
+
try {
|
|
234
|
+
const response = await this._fetch(`${this.engineUrl}/api/projects`);
|
|
235
|
+
if (!response.ok) return [];
|
|
236
|
+
const payload = await response.json();
|
|
237
|
+
return Array.isArray(payload.projects) ? payload.projects : [];
|
|
238
|
+
} catch { return []; }
|
|
239
|
+
}
|
|
240
|
+
|
|
182
241
|
/** Aggregated projection for the shell sidebar: pkb registry is the spine. */
|
|
183
242
|
async aggregatedProjects() {
|
|
184
|
-
const [terminalProjects, vectorHubProjects, eventBridgeProjects] = await Promise.all([
|
|
243
|
+
const [terminalProjects, vectorHubProjects, eventBridgeProjects, engineProjects] = await Promise.all([
|
|
185
244
|
this._listTerminalProjects(),
|
|
186
245
|
this._listVectorHubProjects(),
|
|
187
246
|
this._listEventBridgeProjects(),
|
|
247
|
+
this._listEngineProjects(),
|
|
188
248
|
]);
|
|
189
249
|
const ids = this.registryStore ? this.registryStore.listIds() : [];
|
|
190
250
|
const byWorkspace = new Map(terminalProjects.map(p => [String(p.rootPath || '').toLowerCase(), p]));
|
|
@@ -209,10 +269,13 @@ class ModuleBridge {
|
|
|
209
269
|
name: config.displayName || projectId,
|
|
210
270
|
workspacePath,
|
|
211
271
|
knowledgePath,
|
|
272
|
+
brainId: config.brainId || 'brain-default',
|
|
273
|
+
domainId: config.domainId || null,
|
|
212
274
|
modules: {
|
|
213
275
|
terminal: terminal ? { registered: true, projectId: terminal.id, name: terminal.name } : { registered: false, status: wiring.terminal && wiring.terminal.status },
|
|
214
276
|
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
277
|
eventBridge: eventBridge ? { registered: true, projectId: eventBridge.id, turns: eventBridge.turns, auto: !!eventBridge.auto } : { registered: false, status: wiring.eventBridge && wiring.eventBridge.status },
|
|
278
|
+
engine: engineProjects.some(p => p.projectId === projectId) ? { registered: true, projectId } : { registered: false, status: wiring.engine && wiring.engine.status },
|
|
216
279
|
},
|
|
217
280
|
};
|
|
218
281
|
});
|
|
@@ -221,26 +284,78 @@ class ModuleBridge {
|
|
|
221
284
|
terminalAvailable: terminalProjects.length > 0 || await this._probe(this.terminalUrl),
|
|
222
285
|
vectorHubAvailable: vectorHubProjects.length > 0 || await this._probe(this.vectorHubUrl),
|
|
223
286
|
eventBridgeAvailable: eventBridgeProjects.length > 0 || await this._probe(this.eventBridgeUrl),
|
|
287
|
+
engineAvailable: engineProjects.length > 0 || await this._probe(this.engineUrl),
|
|
224
288
|
};
|
|
225
289
|
}
|
|
226
290
|
|
|
227
291
|
async _probe(base) {
|
|
228
|
-
|
|
292
|
+
const route = base === this.terminalUrl ? '/api/claude-workbench/v1/health' : '/api/health';
|
|
293
|
+
try { const response = await this._fetch(`${base}${route}`, { timeoutMs: 2000 }); return response.ok; } catch { return false; }
|
|
229
294
|
}
|
|
230
295
|
|
|
231
296
|
async modulesHealth() {
|
|
232
|
-
const [terminal, vectorHub, eventBridge] = await Promise.all([
|
|
297
|
+
const [terminal, vectorHub, eventBridge, engine] = await Promise.all([
|
|
233
298
|
this._probe(this.terminalUrl),
|
|
234
299
|
this._probe(this.vectorHubUrl),
|
|
235
300
|
this._probe(this.eventBridgeUrl),
|
|
301
|
+
this._probe(this.engineUrl),
|
|
236
302
|
]);
|
|
237
303
|
return { ok: true, modules: {
|
|
238
304
|
terminal: { url: this.terminalUrl, up: terminal },
|
|
239
305
|
vectorHub: { url: this.vectorHubUrl, up: vectorHub },
|
|
240
306
|
eventBridge: { url: this.eventBridgeUrl, up: eventBridge },
|
|
307
|
+
engine: { url: this.engineUrl, up: engine },
|
|
241
308
|
} };
|
|
242
309
|
}
|
|
243
310
|
|
|
311
|
+
async syncEngineConfiguration() {
|
|
312
|
+
if (this.syncingConfig) return this.configStatus || { scopes: false, model: false };
|
|
313
|
+
this.syncingConfig = true;
|
|
314
|
+
const status = { scopes: false, model: false, embedding: false };
|
|
315
|
+
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.runtimeToken}` };
|
|
316
|
+
try {
|
|
317
|
+
if (this.settingsStore) {
|
|
318
|
+
const settings = this.settingsStore.read();
|
|
319
|
+
if (settings.knowledge.rootPath) {
|
|
320
|
+
const brain = settings.brain;
|
|
321
|
+
const root = path.join(settings.knowledge.rootPath, '_shared');
|
|
322
|
+
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) }))];
|
|
323
|
+
status.scopes = (await this._fetch(`${this.engineUrl}/api/scopes`, { method: 'POST', headers, body: JSON.stringify({ scopes }) })).ok;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch { /* A scope failure must not prevent revoking a stale model key. */ }
|
|
327
|
+
try {
|
|
328
|
+
let profile = null;
|
|
329
|
+
try {
|
|
330
|
+
const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/maintenance-config`, { method: 'POST', headers, body: '{}' });
|
|
331
|
+
if (response.ok) profile = await response.json();
|
|
332
|
+
} catch { /* Missing configuration must clear a previously resolved key. */ }
|
|
333
|
+
const response = await this._fetch(`${this.engineUrl}/api/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ projectId: 'default', profile }) });
|
|
334
|
+
status.model = response.ok && !!profile;
|
|
335
|
+
} catch { /* Status projection only: never log resolved profiles or raw errors. */ }
|
|
336
|
+
try {
|
|
337
|
+
const legacyResponse = await this._fetch(`${this.vectorHubUrl}/api/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
|
|
338
|
+
if (legacyResponse.ok) {
|
|
339
|
+
const legacy = await legacyResponse.json();
|
|
340
|
+
if (legacy.settings && /^embedding-[a-f0-9]{64}$/.test(legacy.migrationId || '')) {
|
|
341
|
+
const imported = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/import-embedding`, { method: 'POST', headers, body: JSON.stringify(legacy) });
|
|
342
|
+
if (imported.ok) await this._fetch(`${this.vectorHubUrl}/api/internal/retire-embedding`, { method: 'POST', headers, body: JSON.stringify({ migrationId: legacy.migrationId }) });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch { /* Failed migration retains the module's protected configuration. */ }
|
|
346
|
+
try {
|
|
347
|
+
let settings = null;
|
|
348
|
+
try {
|
|
349
|
+
const resolved = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
|
|
350
|
+
if (resolved.ok) settings = await resolved.json();
|
|
351
|
+
} catch { /* Revocation wins over a previously cached runtime key. */ }
|
|
352
|
+
const applied = await this._fetch(`${this.vectorHubUrl}/api/internal/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ settings }) });
|
|
353
|
+
status.embedding = applied.ok && !!settings;
|
|
354
|
+
} catch { /* Public status contains no resolved configuration. */ }
|
|
355
|
+
this.syncingConfig = false; this.configStatus = status;
|
|
356
|
+
return status;
|
|
357
|
+
}
|
|
358
|
+
|
|
244
359
|
/**
|
|
245
360
|
* Register one pkb project into both modules and persist the wiring table.
|
|
246
361
|
* Best-effort per module: a module that is down is recorded as 'pending'
|
|
@@ -248,11 +363,12 @@ class ModuleBridge {
|
|
|
248
363
|
* KB_MODULES_REGISTER=0 disables fan-out entirely (test isolation).
|
|
249
364
|
*/
|
|
250
365
|
async registerProjectLinks({ projectId, name, workspacePath, knowledgePath }) {
|
|
251
|
-
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
|
|
366
|
+
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false }, engine: { ok: false } };
|
|
252
367
|
if (String(process.env.KB_MODULES_REGISTER || '') === '0') {
|
|
253
368
|
result.terminal = { ok: false, status: 'disabled' };
|
|
254
369
|
result.vectorHub = { ok: false, status: 'disabled' };
|
|
255
370
|
result.eventBridge = { ok: false, status: 'disabled' };
|
|
371
|
+
result.engine = { ok: false, status: 'disabled' };
|
|
256
372
|
return result;
|
|
257
373
|
}
|
|
258
374
|
// 幂等登记:终端已有同 rootPath 的项目时复用,绝不重复创建
|
|
@@ -273,12 +389,14 @@ class ModuleBridge {
|
|
|
273
389
|
result.terminal = { ok: false, status: 'pending' };
|
|
274
390
|
}
|
|
275
391
|
}
|
|
276
|
-
|
|
392
|
+
const existingVector = (await this._listVectorHubProjects()).find(p => p.name === projectId && String(p.sourceDir || '').toLowerCase() === String(knowledgePath || '').toLowerCase());
|
|
393
|
+
if (existingVector) result.vectorHub = { ok: true, projectName: projectId, status: 'registered', reused: true };
|
|
394
|
+
else try {
|
|
277
395
|
const response = await this._fetch(`${this.vectorHubUrl}/api/import`, {
|
|
278
396
|
method: 'POST',
|
|
279
397
|
headers: { 'Content-Type': 'application/json' },
|
|
280
398
|
timeoutMs: REGISTER_TIMEOUT_MS,
|
|
281
|
-
body: JSON.stringify({ sourceDir: knowledgePath, projectName: projectId, watch:
|
|
399
|
+
body: JSON.stringify({ sourceDir: knowledgePath, projectName: projectId, watch: false }),
|
|
282
400
|
});
|
|
283
401
|
result.vectorHub = { ok: response.ok, projectName: projectId, status: response.ok ? 'registered' : 'error' };
|
|
284
402
|
} catch {
|
|
@@ -307,10 +425,26 @@ class ModuleBridge {
|
|
|
307
425
|
result.eventBridge = { ok: false, status: 'pending' };
|
|
308
426
|
}
|
|
309
427
|
}
|
|
428
|
+
const config = this.projectStore ? this.projectStore.readConfig(projectId) : {};
|
|
429
|
+
const state = this.projectStore ? this.projectStore.readState(projectId) : {};
|
|
430
|
+
try {
|
|
431
|
+
const baselines = { ...(state.conversationBaselineCursors || {}), global: state.conversationBaselineCursor ?? 0 };
|
|
432
|
+
const journalId = result.eventBridge.projectId || state.modules?.eventBridge?.projectId;
|
|
433
|
+
if (journalId && baselines[journalId] === undefined) {
|
|
434
|
+
const watermark = result.eventBridge.reused && await this._fetch(`${this.eventBridgeUrl}/api/journal/high-watermark?journalId=${encodeURIComponent(journalId)}`);
|
|
435
|
+
const current = result.eventBridge.reused ? watermark.ok && await watermark.json() : { sequence: 0 };
|
|
436
|
+
if (!current || !Number.isSafeInteger(current.sequence)) throw new Error('JOURNAL_BASELINE_UNAVAILABLE');
|
|
437
|
+
baselines[journalId] = current.sequence;
|
|
438
|
+
if (this.projectStore) await this.projectStore.updateState(projectId, draft => { draft.conversationBaselineCursors = baselines; });
|
|
439
|
+
}
|
|
440
|
+
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 }) });
|
|
441
|
+
result.engine = { ok: response.ok, projectId, status: response.ok ? 'registered' : 'error' };
|
|
442
|
+
} catch { result.engine = { ok: false, status: 'pending' }; }
|
|
310
443
|
await this._writeWiring(projectId, wiring => {
|
|
311
444
|
wiring.terminal = { ...wiring.terminal, ...result.terminal, name: name || projectId, rootPath: workspacePath, updatedAt: new Date().toISOString() };
|
|
312
445
|
wiring.vectorHub = { ...wiring.vectorHub, ...result.vectorHub, sourceDir: knowledgePath, updatedAt: new Date().toISOString() };
|
|
313
446
|
wiring.eventBridge = { ...wiring.eventBridge, ...result.eventBridge, path: workspacePath, store: knowledgePath ? path.join(knowledgePath, 'dev-conversations') : '', updatedAt: new Date().toISOString() };
|
|
447
|
+
wiring.engine = { ...wiring.engine, ...result.engine, updatedAt: new Date().toISOString() };
|
|
314
448
|
});
|
|
315
449
|
this.logger.info('modules.register', 'Module registration finished.', { context: { projectId, terminal: result.terminal.status, vectorHub: result.vectorHub.status, eventBridge: result.eventBridge.status } });
|
|
316
450
|
return result;
|
|
@@ -320,9 +454,19 @@ class ModuleBridge {
|
|
|
320
454
|
* only drops its derived index (source untouched); terminal removes the project
|
|
321
455
|
* with keepSessions=1 so session records survive; the event-bridge console only
|
|
322
456
|
* unregisters the project — journals and sealed commit documents stay on disk. */
|
|
323
|
-
async removeProjectLinks({ projectId }) {
|
|
457
|
+
async removeProjectLinks({ projectId, requireEngine = true }) {
|
|
324
458
|
const wiring = (this.projectStore ? this.projectStore.readState(projectId).modules : null) || {};
|
|
325
|
-
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
|
|
459
|
+
const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false }, engine: { ok: false } };
|
|
460
|
+
// Stop the writer first, before removing hooks/index registrations.
|
|
461
|
+
try {
|
|
462
|
+
if (!requireEngine) result.engine = { ok: true, status: 'not-owner' };
|
|
463
|
+
else {
|
|
464
|
+
const response = await this._fetch(`${this.engineUrl}/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE' });
|
|
465
|
+
if (response.status === 409) throw new DomainError('PROJECT_BUSY', '知识维护仍在运行,不能解绑。', { status: 409, retryable: true });
|
|
466
|
+
result.engine = { ok: response.ok || response.status === 404, status: response.ok || response.status === 404 ? 'removed' : 'pending' };
|
|
467
|
+
}
|
|
468
|
+
} catch (error) { if (error.code === 'PROJECT_BUSY') throw error; result.engine = { ok: false, status: 'pending' }; }
|
|
469
|
+
if (!result.engine.ok) throw new DomainError('PROJECT_BUSY', '知识引擎暂时不可用,无法安全停止写入;请恢复后再解绑。', { status: 409, retryable: true });
|
|
326
470
|
let terminalId = wiring.terminal && wiring.terminal.projectId;
|
|
327
471
|
if (!terminalId) {
|
|
328
472
|
const workspacePath = wiring.terminal && wiring.terminal.rootPath;
|
|
@@ -363,6 +507,7 @@ class ModuleBridge {
|
|
|
363
507
|
if (result.terminal.ok) wiring.terminal = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
364
508
|
if (result.vectorHub.ok) wiring.vectorHub = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
365
509
|
if (result.eventBridge.ok) wiring.eventBridge = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
510
|
+
if (result.engine.ok) wiring.engine = { status: 'removed', updatedAt: new Date().toISOString() };
|
|
366
511
|
});
|
|
367
512
|
this.logger.info('modules.remove', 'Module de-registration finished.', { context: { projectId, terminal: result.terminal, vectorHub: result.vectorHub, eventBridge: result.eventBridge } });
|
|
368
513
|
return result;
|
|
@@ -396,7 +541,7 @@ class ModuleBridge {
|
|
|
396
541
|
_supervisedSpecs() {
|
|
397
542
|
const specs = [];
|
|
398
543
|
if (process.env.KB_TERMINAL_COMMAND) {
|
|
399
|
-
specs.push({ name: 'terminal', command: process.env.KB_TERMINAL_COMMAND, url: this.terminalUrl, env: {} });
|
|
544
|
+
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
545
|
} else {
|
|
401
546
|
const terminalEntry = resolveModuleEntry('terminal');
|
|
402
547
|
if (terminalEntry) {
|
|
@@ -404,7 +549,7 @@ class ModuleBridge {
|
|
|
404
549
|
name: 'terminal',
|
|
405
550
|
command: `node ${JSON.stringify(terminalEntry)}`,
|
|
406
551
|
url: this.terminalUrl,
|
|
407
|
-
env: { AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl) },
|
|
552
|
+
env: { 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
553
|
});
|
|
409
554
|
} else {
|
|
410
555
|
this.logger.warn('modules.supervisor', 'claude-ai-workbench runtime not installed; expecting an external service.', {});
|
|
@@ -413,7 +558,7 @@ class ModuleBridge {
|
|
|
413
558
|
// vector-hub declares engines >= 22; spawning it on older runtimes would
|
|
414
559
|
// crash-loop the supervisor for nothing.
|
|
415
560
|
if (process.env.KB_VECTORHUB_COMMAND) {
|
|
416
|
-
specs.push({ name: 'vector-hub', command: process.env.KB_VECTORHUB_COMMAND, url: this.vectorHubUrl, env: {} });
|
|
561
|
+
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
562
|
} else if (parseInt(process.versions.node, 10) < 22) {
|
|
418
563
|
this.logger.warn('modules.supervisor', 'vector-hub needs Node >= 22; not spawning the vendored runtime.', { context: { node: process.versions.node } });
|
|
419
564
|
} else {
|
|
@@ -425,9 +570,9 @@ class ModuleBridge {
|
|
|
425
570
|
const root = path.join(dataDir, 'vectorhub');
|
|
426
571
|
specs.push({
|
|
427
572
|
name: 'vector-hub',
|
|
428
|
-
command: `node ${JSON.stringify(vectorHubEntry)} serve --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
573
|
+
command: `node ${JSON.stringify(vectorHubEntry)} serve --host ${urlHost(this.vectorHubUrl)} --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
429
574
|
url: this.vectorHubUrl,
|
|
430
|
-
env: { VECTOR_HUB_ROOT: root },
|
|
575
|
+
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG: process.env.VECTOR_HUB_CONFIG || path.join(root, 'config.json'), VECTOR_HUB_LEGACY_CONFIG: process.env.VECTOR_HUB_LEGACY_CONFIG || path.join(require('os').homedir(), '.vector-hub', 'config.json'), KB_VECTOR_MANAGED: '1', KB_VECTOR_RUNTIME_TOKEN: this.runtimeToken },
|
|
431
576
|
});
|
|
432
577
|
}
|
|
433
578
|
}
|
|
@@ -438,7 +583,7 @@ class ModuleBridge {
|
|
|
438
583
|
// folder-picker warm-up is an interactive-only feature the shell never
|
|
439
584
|
// routes here (import uses the terminal/vector-hub pickers) — skip it.
|
|
440
585
|
if (process.env.KB_EVENTBRIDGE_COMMAND) {
|
|
441
|
-
specs.push({ name: 'event-bridge', command: process.env.KB_EVENTBRIDGE_COMMAND, url: this.eventBridgeUrl, env: {} });
|
|
586
|
+
specs.push({ name: 'event-bridge', command: process.env.KB_EVENTBRIDGE_COMMAND, url: this.eventBridgeUrl, env: { KB_EVENT_BRIDGE_RUNTIME_TOKEN: this.runtimeToken } });
|
|
442
587
|
} else {
|
|
443
588
|
const eventBridgeEntry = resolveModuleEntry('event-bridge');
|
|
444
589
|
if (!eventBridgeEntry) {
|
|
@@ -451,28 +596,37 @@ class ModuleBridge {
|
|
|
451
596
|
name: 'event-bridge',
|
|
452
597
|
command: `node ${JSON.stringify(eventBridgeEntry)} serve --host ${urlHost(this.eventBridgeUrl)} --port ${urlPort(this.eventBridgeUrl, 8790)}${homeArgs}`,
|
|
453
598
|
url: this.eventBridgeUrl,
|
|
454
|
-
env: { BRIDGE_CONSOLE_PICK_FOLDER: '0' },
|
|
599
|
+
env: { BRIDGE_CONSOLE_PICK_FOLDER: '0', KB_EVENT_BRIDGE_RUNTIME_TOKEN: this.runtimeToken },
|
|
455
600
|
});
|
|
456
601
|
}
|
|
457
602
|
}
|
|
603
|
+
const engineEntry = resolveModuleEntry('knowledge-engine');
|
|
604
|
+
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
605
|
return specs;
|
|
459
606
|
}
|
|
460
607
|
|
|
461
608
|
_spawnSupervised(spec) {
|
|
609
|
+
if (this.stopped) return;
|
|
462
610
|
// Strip the quoting: spawn takes argv, not a shell line — a literal
|
|
463
611
|
// `"path"` argument makes node load a file whose name contains quotes.
|
|
464
612
|
const parts = (spec.command.match(/"[^"]+"|\S+/g) || [])
|
|
465
|
-
.map(part => (part.startsWith('"')
|
|
466
|
-
const
|
|
613
|
+
.map(part => { if (!part.startsWith('"') || !part.endsWith('"')) return part; try { return JSON.parse(part); } catch { return part.slice(1, -1); } });
|
|
614
|
+
const nodeProcess = parts[0] === 'node' || parts[0] === process.execPath;
|
|
615
|
+
const child = spawn(nodeProcess ? process.execPath : parts[0], parts.slice(1), {
|
|
467
616
|
// Module banners (ports, data roots) are diagnostics — inherit stdout
|
|
468
617
|
// so they land on the console in --fg mode and in launcher.log when the
|
|
469
618
|
// shell itself runs detached.
|
|
470
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
619
|
+
stdio: nodeProcess ? ['ignore', 'inherit', 'inherit', 'ipc'] : ['ignore', 'inherit', 'inherit'],
|
|
471
620
|
windowsHide: true,
|
|
472
621
|
env: { ...process.env, ...(spec.env || {}) },
|
|
473
622
|
});
|
|
474
|
-
let attempts = 0;
|
|
623
|
+
let attempts = spec._attempts || 0;
|
|
624
|
+
child.on('error', error => {
|
|
625
|
+
this.supervised = this.supervised.filter(existing => existing !== child);
|
|
626
|
+
this.logger.warn('modules.supervisor', 'Module process could not start.', { context: { name: spec.name, code: error.code || 'SPAWN_FAILED' } });
|
|
627
|
+
});
|
|
475
628
|
child.on('exit', code => {
|
|
629
|
+
this.supervised = this.supervised.filter(existing => existing !== child);
|
|
476
630
|
attempts += 1;
|
|
477
631
|
if (attempts > 5 || this.stopped) {
|
|
478
632
|
this.logger.warn('modules.supervisor', 'Module service stopped.', { context: { name: spec.name, code, attempts } });
|
|
@@ -480,17 +634,28 @@ class ModuleBridge {
|
|
|
480
634
|
}
|
|
481
635
|
const delay = Math.min(1000 * 2 ** attempts, 15000);
|
|
482
636
|
this.logger.warn('modules.supervisor', 'Module service exited; restarting.', { context: { name: spec.name, code, retryInMs: delay } });
|
|
483
|
-
setTimeout(() => this._spawnSupervised({ ...spec, _attempts: attempts }), delay);
|
|
637
|
+
const timer = setTimeout(() => { this.restartTimers.delete(timer); this._spawnSupervised({ ...spec, _attempts: attempts }); }, delay);
|
|
638
|
+
this.restartTimers = this.restartTimers || new Set(); this.restartTimers.add(timer);
|
|
484
639
|
});
|
|
485
640
|
this.supervised.push(child);
|
|
486
641
|
this.logger.info('modules.supervisor', 'Module service spawned.', { context: { name: spec.name, pid: child.pid } });
|
|
487
642
|
}
|
|
488
643
|
|
|
489
|
-
stopSupervised() {
|
|
644
|
+
async stopSupervised() {
|
|
490
645
|
this.stopped = true;
|
|
491
|
-
for (const
|
|
646
|
+
for (const timer of this.restartTimers || []) clearTimeout(timer);
|
|
647
|
+
await Promise.all(this.supervised.map(child => new Promise(resolve => {
|
|
648
|
+
if (child.exitCode !== null || !child.pid) return resolve();
|
|
649
|
+
const timer = setTimeout(() => {
|
|
650
|
+
if (process.platform === 'win32') spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
651
|
+
else try { child.kill('SIGKILL'); } catch {}
|
|
652
|
+
resolve();
|
|
653
|
+
}, 5000);
|
|
654
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
655
|
+
try { if (child.connected) child.send({ type: 'kb:shutdown' }, error => { if (error) try { child.kill(); } catch {} }); else child.kill(); } catch { try { child.kill(); } catch {} }
|
|
656
|
+
})));
|
|
492
657
|
this.supervised = [];
|
|
493
658
|
}
|
|
494
659
|
}
|
|
495
660
|
|
|
496
|
-
module.exports = { ModuleBridge, defaultTerminalUrl, defaultVectorHubUrl, defaultEventBridgeUrl };
|
|
661
|
+
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
|
|