nexus-prime 7.9.0 → 7.9.2
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/dist/agents/adapters/mcp/async-gate.d.ts +5 -0
- package/dist/agents/adapters/mcp/async-gate.js +12 -2
- package/dist/agents/adapters/mcp/dispatch.js +35 -1
- package/dist/agents/adapters/mcp/handlers/runtime.js +33 -1
- package/dist/agents/adapters/mcp/runHandler.d.ts +2 -1
- package/dist/agents/adapters/mcp/runHandler.js +13 -4
- package/dist/agents/adapters/mcp.d.ts +2 -0
- package/dist/agents/adapters/mcp.js +59 -14
- package/dist/architects/bootstrap.js +19 -0
- package/dist/architects/db/schema.d.ts +1 -1
- package/dist/architects/db/schema.js +40 -1
- package/dist/architects/types.d.ts +1 -1
- package/dist/daemon/client.d.ts +1 -0
- package/dist/daemon/client.js +2 -1
- package/dist/daemon/server.d.ts +2 -0
- package/dist/daemon/server.js +39 -29
- package/dist/dashboard/app/main.js +26 -4
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.js +56 -7
- package/dist/dashboard/stream/sse-broker.js +2 -0
- package/dist/engines/event-bus.d.ts +7 -1
- package/dist/engines/knowledge-fabric.d.ts +3 -1
- package/dist/engines/knowledge-fabric.js +70 -40
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/synapse/mandate/pipeline.js +5 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ export type AsyncJobStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
|
15
15
|
export interface AsyncJob {
|
|
16
16
|
runId: string;
|
|
17
17
|
tool: string;
|
|
18
|
+
args?: Record<string, unknown>;
|
|
18
19
|
status: AsyncJobStatus;
|
|
19
20
|
stage: string;
|
|
20
21
|
progress: number;
|
|
@@ -29,10 +30,13 @@ export interface AsyncJob {
|
|
|
29
30
|
};
|
|
30
31
|
error?: string;
|
|
31
32
|
etaMs?: number;
|
|
33
|
+
lifecycleReportedAt?: number;
|
|
32
34
|
}
|
|
33
35
|
export interface AsyncGateOpts {
|
|
34
36
|
/** Tool name — for job metadata */
|
|
35
37
|
tool: string;
|
|
38
|
+
/** Original tool args, used when a poll observes final lifecycle state. */
|
|
39
|
+
args?: Record<string, unknown>;
|
|
36
40
|
/** If handler resolves within this window, return inline. Default: 2000ms */
|
|
37
41
|
maxSyncMs?: number;
|
|
38
42
|
/** Human-readable hint for estimated duration */
|
|
@@ -67,6 +71,7 @@ declare class AsyncGate {
|
|
|
67
71
|
}>;
|
|
68
72
|
};
|
|
69
73
|
updateStage(runId: string, stage: string, progress: number): void;
|
|
74
|
+
markLifecycleReported(runId: string): void;
|
|
70
75
|
/** Return jobs that have been running longer than their stale threshold. */
|
|
71
76
|
getStaleJobs(now: number, defaultStaleMs: number, etaMultiplier: number): AsyncJob[];
|
|
72
77
|
/** Mark a running job as failed (watchdog reap). */
|
|
@@ -25,6 +25,7 @@ class AsyncGate {
|
|
|
25
25
|
const job = {
|
|
26
26
|
runId,
|
|
27
27
|
tool: opts.tool,
|
|
28
|
+
args: opts.args,
|
|
28
29
|
status: 'pending',
|
|
29
30
|
stage: 'queued',
|
|
30
31
|
progress: 0,
|
|
@@ -55,8 +56,12 @@ class AsyncGate {
|
|
|
55
56
|
job.error = err instanceof Error ? err.message : String(err);
|
|
56
57
|
job.completedAt = Date.now();
|
|
57
58
|
nexusEventBus.emit('async_gate.failed', { runId, tool: opts.tool, error: job.error });
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: `❌ Run ${runId} failed\nTool: ${opts.tool}\nError: ${job.error ?? 'unknown'}`,
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
60
65
|
}
|
|
61
66
|
})();
|
|
62
67
|
// Suppress unhandled rejection: deadline may win while work is still in flight.
|
|
@@ -118,6 +123,11 @@ class AsyncGate {
|
|
|
118
123
|
job.progress = Math.min(99, progress);
|
|
119
124
|
}
|
|
120
125
|
}
|
|
126
|
+
markLifecycleReported(runId) {
|
|
127
|
+
const job = this.jobs.get(runId);
|
|
128
|
+
if (job)
|
|
129
|
+
job.lifecycleReportedAt = Date.now();
|
|
130
|
+
}
|
|
121
131
|
/** Return jobs that have been running longer than their stale threshold. */
|
|
122
132
|
getStaleJobs(now, defaultStaleMs, etaMultiplier) {
|
|
123
133
|
const stale = [];
|
|
@@ -26,6 +26,23 @@ const SLOW_TOOLS = new Set([
|
|
|
26
26
|
'nexus_ghost_pass',
|
|
27
27
|
'nexus_spawn_workers',
|
|
28
28
|
'nexus_optimize_tokens',
|
|
29
|
+
'nexus_search',
|
|
30
|
+
'nexus_assemble_context',
|
|
31
|
+
'nexus_graph_query',
|
|
32
|
+
'nexus_session_search',
|
|
33
|
+
'nexus_memory_export',
|
|
34
|
+
'nexus_memory_backup',
|
|
35
|
+
'nexus_memory_import',
|
|
36
|
+
'nexus_memory_hygiene',
|
|
37
|
+
'nexus_memory_audit',
|
|
38
|
+
'nexus_pattern_search',
|
|
39
|
+
'nexus_knowledge_provenance',
|
|
40
|
+
'nexus_rag_ingest_collection',
|
|
41
|
+
'nexus_workflow_run',
|
|
42
|
+
'nexus_automation_run',
|
|
43
|
+
'nexus_doctor',
|
|
44
|
+
'nexus_autofix',
|
|
45
|
+
'nexus_hypertune_max',
|
|
29
46
|
]);
|
|
30
47
|
/** Estimated wall-clock duration for each slow tool (ms). */
|
|
31
48
|
const TOOL_ETA_MS = {
|
|
@@ -34,6 +51,23 @@ const TOOL_ETA_MS = {
|
|
|
34
51
|
nexus_ghost_pass: 45_000,
|
|
35
52
|
nexus_spawn_workers: 30_000,
|
|
36
53
|
nexus_optimize_tokens: 10_000,
|
|
54
|
+
nexus_search: 20_000,
|
|
55
|
+
nexus_assemble_context: 20_000,
|
|
56
|
+
nexus_graph_query: 30_000,
|
|
57
|
+
nexus_session_search: 20_000,
|
|
58
|
+
nexus_memory_export: 20_000,
|
|
59
|
+
nexus_memory_backup: 30_000,
|
|
60
|
+
nexus_memory_import: 30_000,
|
|
61
|
+
nexus_memory_hygiene: 30_000,
|
|
62
|
+
nexus_memory_audit: 30_000,
|
|
63
|
+
nexus_pattern_search: 20_000,
|
|
64
|
+
nexus_knowledge_provenance: 20_000,
|
|
65
|
+
nexus_rag_ingest_collection: 45_000,
|
|
66
|
+
nexus_workflow_run: 45_000,
|
|
67
|
+
nexus_automation_run: 45_000,
|
|
68
|
+
nexus_doctor: 20_000,
|
|
69
|
+
nexus_autofix: 45_000,
|
|
70
|
+
nexus_hypertune_max: 45_000,
|
|
37
71
|
};
|
|
38
72
|
/**
|
|
39
73
|
* Route a tool call to the appropriate handler group.
|
|
@@ -90,7 +124,7 @@ export async function dispatchMcpToolCall(hctx, request, args, ctx) {
|
|
|
90
124
|
const gated = await withAsyncGate(async () => {
|
|
91
125
|
const r = await runHandlers();
|
|
92
126
|
return r ?? { content: [{ type: 'text', text: `Tool ${toolName} returned no result` }] };
|
|
93
|
-
}, { tool: toolName, maxSyncMs: 2000, etaMs: TOOL_ETA_MS[toolName] });
|
|
127
|
+
}, { tool: toolName, args, maxSyncMs: 2000, etaMs: TOOL_ETA_MS[toolName] });
|
|
94
128
|
if ('queued' in gated && gated.queued) {
|
|
95
129
|
// Persist orchestration run record for durable stage tracking
|
|
96
130
|
if (toolName === 'nexus_orchestrate') {
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* federation_status, run_status.
|
|
5
5
|
* Extracted from mcp.ts (Phase 3 split).
|
|
6
6
|
*/
|
|
7
|
-
import { formatBullets, formatJsonDetails, } from '../helpers.js';
|
|
7
|
+
import { formatBullets, formatJsonDetails, buildAutoMemorySummary, } from '../helpers.js';
|
|
8
8
|
import { nexusEventBus } from '../../../../engines/event-bus.js';
|
|
9
9
|
import { getSharedLicenseManager, snapshotPCU, formatPCUStatus } from '../../../../licensing/index.js';
|
|
10
10
|
import { TokenAnalyticsEngine } from '../../../../engines/token-analytics.js';
|
|
11
|
+
import * as path from 'path';
|
|
11
12
|
import { requireRuntime } from '../util/require-runtime.js';
|
|
12
13
|
import { getAsyncGate } from '../async-gate.js';
|
|
13
14
|
import { getRun as getOrchRun } from '../../../../engines/orchestrator/store.js';
|
|
@@ -231,6 +232,37 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
|
|
|
231
232
|
// Check async gate first (fast in-memory jobs from withAsyncGate)
|
|
232
233
|
const asyncJob = getAsyncGate().getJob(runId);
|
|
233
234
|
if (asyncJob) {
|
|
235
|
+
if (!asyncJob.lifecycleReportedAt
|
|
236
|
+
&& (asyncJob.status === 'completed' || asyncJob.status === 'failed')) {
|
|
237
|
+
if (asyncJob.status === 'completed') {
|
|
238
|
+
hctx.telemetry.observeSuccessfulToolCall(asyncJob.tool, asyncJob.args ?? {});
|
|
239
|
+
}
|
|
240
|
+
const shouldPersistSuccess = [
|
|
241
|
+
'nexus_orchestrate',
|
|
242
|
+
'nexus_plan_execution',
|
|
243
|
+
'nexus_session_dna',
|
|
244
|
+
'nexus_ghost_pass',
|
|
245
|
+
'nexus_mindkit_check',
|
|
246
|
+
].includes(asyncJob.tool);
|
|
247
|
+
try {
|
|
248
|
+
if (asyncJob.tool !== 'nexus_store_memory' && (asyncJob.status === 'failed' || shouldPersistSuccess)) {
|
|
249
|
+
const workspace = hctx.getWorkspace(asyncJob.args ?? {});
|
|
250
|
+
const repoName = workspace.repoName || path.basename(workspace.repoRoot || '');
|
|
251
|
+
const summary = asyncJob.status === 'completed'
|
|
252
|
+
? `MCP ${asyncJob.tool} completed asynchronously. ${buildAutoMemorySummary(asyncJob.tool, asyncJob.args ?? {})}`
|
|
253
|
+
: `MCP ${asyncJob.tool} failed asynchronously: ${asyncJob.error ?? 'unknown failure'}. ${buildAutoMemorySummary(asyncJob.tool, asyncJob.args ?? {})}`;
|
|
254
|
+
hctx.nexusRef.storeMemory(summary.slice(0, 1200), asyncJob.status === 'completed' ? 0.55 : 0.72, [
|
|
255
|
+
'#mcp-result',
|
|
256
|
+
'#auto',
|
|
257
|
+
`#tool:${asyncJob.tool}`,
|
|
258
|
+
...(repoName ? [`repo:${repoName}`, `#repo:${repoName}`] : []),
|
|
259
|
+
...(asyncJob.status === 'completed' ? ['#workspace'] : ['#failure-mode', '#runtime-result', '#inbox']),
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch { /* best-effort lifecycle memory */ }
|
|
264
|
+
getAsyncGate().markLifecycleReported(runId);
|
|
265
|
+
}
|
|
234
266
|
if (fmt === 'nxl')
|
|
235
267
|
return nxlResult(asyncJob);
|
|
236
268
|
return getAsyncGate().formatStatus(runId);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Transient retries — SQLite-busy / cold runtime → one retry with backoff.
|
|
8
8
|
*
|
|
9
9
|
* Per-client timeouts (AI Lead refinement):
|
|
10
|
-
* Codex 90 s · Claude Code 60 s · Cursor 30 s · default
|
|
10
|
+
* Codex 90 s · Claude Code 60 s · Cursor 30 s · MCP clients 60 s · default 60 s
|
|
11
11
|
*
|
|
12
12
|
* Types and `envelopeToMcpResponse` are canonical in ./envelope.ts;
|
|
13
13
|
* re-exported from here for backward compatibility.
|
|
@@ -16,6 +16,7 @@ import type { RunHandlerOptions } from './envelope.js';
|
|
|
16
16
|
import type { HandlerEnvelope } from './envelope.js';
|
|
17
17
|
export type { HandlerErrorCode, HandlerEnvelope, RunHandlerOptions } from './envelope.js';
|
|
18
18
|
export { envelopeToMcpResponse } from './envelope.js';
|
|
19
|
+
export declare function resolveHandlerTimeoutForTest(opts?: RunHandlerOptions): number;
|
|
19
20
|
/**
|
|
20
21
|
* Run a handler function with timeout, retry, and structured envelope.
|
|
21
22
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Transient retries — SQLite-busy / cold runtime → one retry with backoff.
|
|
8
8
|
*
|
|
9
9
|
* Per-client timeouts (AI Lead refinement):
|
|
10
|
-
* Codex 90 s · Claude Code 60 s · Cursor 30 s · default
|
|
10
|
+
* Codex 90 s · Claude Code 60 s · Cursor 30 s · MCP clients 60 s · default 60 s
|
|
11
11
|
*
|
|
12
12
|
* Types and `envelopeToMcpResponse` are canonical in ./envelope.ts;
|
|
13
13
|
* re-exported from here for backward compatibility.
|
|
@@ -20,16 +20,25 @@ const CLIENT_TIMEOUTS = {
|
|
|
20
20
|
'codex': 90_000,
|
|
21
21
|
'claude-code': 60_000,
|
|
22
22
|
'cursor': 30_000,
|
|
23
|
+
'openclaw': 90_000,
|
|
24
|
+
'opencode': 90_000,
|
|
25
|
+
'windsurf': 90_000,
|
|
26
|
+
'antigravity': 90_000,
|
|
27
|
+
'mcp': 60_000,
|
|
23
28
|
};
|
|
24
|
-
const DEFAULT_TIMEOUT =
|
|
29
|
+
const DEFAULT_TIMEOUT = 60_000;
|
|
25
30
|
function resolveTimeout(opts) {
|
|
26
31
|
if (opts.timeoutMs !== undefined)
|
|
27
32
|
return opts.timeoutMs;
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const callerName = String(opts.callerName ?? '').trim().toLowerCase();
|
|
34
|
+
if (callerName && callerName in CLIENT_TIMEOUTS) {
|
|
35
|
+
return CLIENT_TIMEOUTS[callerName];
|
|
30
36
|
}
|
|
31
37
|
return DEFAULT_TIMEOUT;
|
|
32
38
|
}
|
|
39
|
+
export function resolveHandlerTimeoutForTest(opts = {}) {
|
|
40
|
+
return resolveTimeout(opts);
|
|
41
|
+
}
|
|
33
42
|
// ─── Error classification ─────────────────────────────────────────────────────
|
|
34
43
|
function classifyError(err) {
|
|
35
44
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -818,6 +818,7 @@ export class MCPAdapter {
|
|
|
818
818
|
const result = envelopeToMcpResponse(envelope);
|
|
819
819
|
const durationMs = Date.now() - startTimeMs;
|
|
820
820
|
const resultPreview = envelope.ok ? buildResultPreview(result) : null;
|
|
821
|
+
const asyncReceipt = envelope.ok ? this.parseAsyncReceipt(result) : null;
|
|
821
822
|
nexusEventBus.emit('mcp.call.complete', {
|
|
822
823
|
callId,
|
|
823
824
|
serverName: 'nexus-prime',
|
|
@@ -830,27 +831,71 @@ export class MCPAdapter {
|
|
|
830
831
|
? { result: resultPreview }
|
|
831
832
|
: { error: envelope.error.message }),
|
|
832
833
|
});
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
if (this.nexusRef && !isReadOnlyMcpTool(toolName)) {
|
|
834
|
+
if (envelope.ok && !asyncReceipt?.queued) {
|
|
835
|
+
this.telemetry.observeSuccessfulToolCall(toolName, args);
|
|
836
|
+
}
|
|
837
|
+
// Surface meaningful tool calls on the SSE feed; persist only high-signal outcomes.
|
|
838
|
+
if (this.nexusRef) {
|
|
839
839
|
const repoRoot = this.nexusRef.getWorkspaceContext?.()?.repoRoot ?? '';
|
|
840
840
|
const repoName = repoRoot ? path.basename(repoRoot) : '';
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
841
|
+
if (!isReadOnlyMcpTool(toolName)) {
|
|
842
|
+
try {
|
|
843
|
+
nexusEventBus.emit('memory.snapshot', {
|
|
844
|
+
kind: 'tool-invocation',
|
|
845
|
+
toolName,
|
|
846
|
+
repo: repoName,
|
|
847
|
+
summary: buildAutoMemorySummary(toolName, args),
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
catch { /* best-effort */ }
|
|
848
851
|
}
|
|
849
|
-
|
|
852
|
+
this.maybeStoreToolOutcomeMemory(toolName, args, envelope, durationMs, repoName, asyncReceipt);
|
|
850
853
|
}
|
|
851
854
|
const decorated = this.decorateLifecycleResponse(toolName, result);
|
|
852
855
|
return this.injectMemoryContext(toolName, args, decorated);
|
|
853
856
|
}
|
|
857
|
+
parseAsyncReceipt(result) {
|
|
858
|
+
const text = result.content?.[0]?.text;
|
|
859
|
+
if (!text)
|
|
860
|
+
return null;
|
|
861
|
+
try {
|
|
862
|
+
const parsed = JSON.parse(String(text).split('\n\n')[0] ?? '{}');
|
|
863
|
+
return parsed && typeof parsed === 'object' && parsed.queued === true ? parsed : null;
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
maybeStoreToolOutcomeMemory(toolName, args, envelope, durationMs, repoName, asyncReceipt) {
|
|
870
|
+
if (!this.nexusRef || toolName === 'nexus_store_memory')
|
|
871
|
+
return;
|
|
872
|
+
if (asyncReceipt?.queued)
|
|
873
|
+
return;
|
|
874
|
+
const shouldPersistSuccess = [
|
|
875
|
+
'nexus_orchestrate',
|
|
876
|
+
'nexus_plan_execution',
|
|
877
|
+
'nexus_session_dna',
|
|
878
|
+
'nexus_ghost_pass',
|
|
879
|
+
'nexus_mindkit_check',
|
|
880
|
+
].includes(toolName);
|
|
881
|
+
if (envelope.ok && !shouldPersistSuccess)
|
|
882
|
+
return;
|
|
883
|
+
const error = envelope.error;
|
|
884
|
+
const summary = envelope.ok
|
|
885
|
+
? `MCP ${toolName} completed in ${durationMs}ms. ${buildAutoMemorySummary(toolName, args)}`
|
|
886
|
+
: `MCP ${toolName} failed after ${durationMs}ms: ${error?.code ?? 'error'} ${error?.message ?? 'unknown failure'}. ${buildAutoMemorySummary(toolName, args)}`;
|
|
887
|
+
const tags = [
|
|
888
|
+
'#mcp-result',
|
|
889
|
+
'#auto',
|
|
890
|
+
`#tool:${toolName}`,
|
|
891
|
+
...(repoName ? [`repo:${repoName}`, `#repo:${repoName}`] : []),
|
|
892
|
+
...(envelope.ok ? ['#workspace'] : ['#failure-mode', '#runtime-result', '#inbox']),
|
|
893
|
+
];
|
|
894
|
+
try {
|
|
895
|
+
this.nexusRef.storeMemory(summary.slice(0, 1200), envelope.ok ? 0.55 : 0.72, tags);
|
|
896
|
+
}
|
|
897
|
+
catch { /* best-effort */ }
|
|
898
|
+
}
|
|
854
899
|
buildHandlerCtx(args = {}) {
|
|
855
900
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
856
901
|
const self = this;
|
|
@@ -245,12 +245,30 @@ export function initArchitects(options) {
|
|
|
245
245
|
nexusEventBus.emit('architects.event.failed', { event: 'striketeam.deployed', strikeTeamId, error: String(err) });
|
|
246
246
|
}
|
|
247
247
|
};
|
|
248
|
+
const onStriketeamFinished = ({ strikeTeamId, intent, outcome }) => {
|
|
249
|
+
try {
|
|
250
|
+
if (intent === 'mutate' && outcome !== 'cancelled' && outcome !== 'failed')
|
|
251
|
+
return;
|
|
252
|
+
const worklistId = implicitWorklistByTeam.get(strikeTeamId);
|
|
253
|
+
if (!worklistId)
|
|
254
|
+
return;
|
|
255
|
+
db.prepare(`
|
|
256
|
+
UPDATE architects_work_items SET status='cancelled'
|
|
257
|
+
WHERE worklist_id=? AND status='todo'
|
|
258
|
+
`).run(worklistId);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
console.error('[Architects] onStriketeamFinished failed', strikeTeamId, err);
|
|
262
|
+
nexusEventBus.emit('architects.event.failed', { event: 'striketeam.finished', strikeTeamId, error: String(err) });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
248
265
|
const onStanddown = () => setConvergencePaused(true);
|
|
249
266
|
const onResumed = () => setConvergencePaused(false);
|
|
250
267
|
const unsubOperative = nexusEventBus.on('synapse.operative.hired', onOperativeHired);
|
|
251
268
|
const unsubMission = nexusEventBus.on('synapse.mission.assigned', onMissionAssigned);
|
|
252
269
|
const unsubSortie = nexusEventBus.on('synapse.sortie.completed', onSortieCompleted);
|
|
253
270
|
const unsubTeam = nexusEventBus.on('synapse.striketeam.deployed', onStriketeamDeployed);
|
|
271
|
+
const unsubFinished = nexusEventBus.on('synapse.striketeam.finished', onStriketeamFinished);
|
|
254
272
|
const unsubStanddown = nexusEventBus.on('synapse.compaction.standdown', onStanddown);
|
|
255
273
|
const unsubResumed = nexusEventBus.on('synapse.compaction.resumed', onResumed);
|
|
256
274
|
nexusEventBus.emit('architects.ready', { version: '5.0.0' });
|
|
@@ -321,6 +339,7 @@ export function initArchitects(options) {
|
|
|
321
339
|
unsubMission();
|
|
322
340
|
unsubSortie();
|
|
323
341
|
unsubTeam();
|
|
342
|
+
unsubFinished();
|
|
324
343
|
unsubStanddown();
|
|
325
344
|
unsubResumed();
|
|
326
345
|
db.close();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ArchitectsDb } from '../types.js';
|
|
2
|
-
export declare const ARCHITECTS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS architects_blueprints (\n id TEXT PRIMARY KEY,\n strike_team_id TEXT,\n title TEXT NOT NULL,\n workflow_id TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('draft','active','converging','done','archived')),\n variables TEXT NOT NULL DEFAULT '{}',\n worklist_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_worklists (\n id TEXT PRIMARY KEY,\n blueprint_id TEXT NOT NULL,\n title TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_work_items (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n title TEXT NOT NULL,\n parent_work_item_id TEXT,\n correlation_id TEXT,\n ancestry_path TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked')),\n depends_on TEXT NOT NULL DEFAULT '[]',\n assigned_operative_id TEXT,\n construction_lock_id TEXT,\n branch TEXT,\n merged_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_construction_locks (\n id TEXT PRIMARY KEY,\n work_item_id TEXT NOT NULL,\n operative_id TEXT NOT NULL,\n acquired_at TEXT NOT NULL DEFAULT (datetime('now')),\n released_at TEXT,\n lease_duration_ms INTEGER NOT NULL DEFAULT 300000,\n lease_renewed_at INTEGER,\n lease_expires_at INTEGER NOT NULL DEFAULT 0,\n hook_ref TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_relay_messages (\n id TEXT PRIMARY KEY,\n from_operative_id TEXT NOT NULL,\n to_operative_id TEXT,\n strike_team_id TEXT,\n subject TEXT NOT NULL,\n body TEXT NOT NULL,\n sent_at TEXT NOT NULL DEFAULT (datetime('now')),\n read_at TEXT,\n priority TEXT NOT NULL DEFAULT 'normal'\n CHECK(priority IN ('normal','urgent'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_convergence_runs (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n strategy TEXT NOT NULL DEFAULT 'bisecting'\n CHECK(strategy IN ('sequential','bisecting')),\n work_item_ids TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running','merged','failed','bisecting','deferred')),\n failed_item_id TEXT,\n started_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_dispatch_queue (\n id TEXT PRIMARY KEY,\n operative_id TEXT NOT NULL,\n work_item_id TEXT NOT NULL,\n scheduled_at TEXT NOT NULL DEFAULT (datetime('now')),\n dispatched_at TEXT,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','dispatched','failed'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);\nCREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);\nCREATE INDEX IF NOT EXISTS idx_arch_locks_item ON architects_construction_locks(work_item_id, released_at);\nCREATE INDEX IF NOT EXISTS idx_arch_blueprints_team ON architects_blueprints(strike_team_id, worklist_id);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_op ON architects_relay_messages(to_operative_id, read_at);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_team ON architects_relay_messages(strike_team_id, read_at);\n";
|
|
2
|
+
export declare const ARCHITECTS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS architects_blueprints (\n id TEXT PRIMARY KEY,\n strike_team_id TEXT,\n title TEXT NOT NULL,\n workflow_id TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('draft','active','converging','done','archived')),\n variables TEXT NOT NULL DEFAULT '{}',\n worklist_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_worklists (\n id TEXT PRIMARY KEY,\n blueprint_id TEXT NOT NULL,\n title TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_work_items (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n title TEXT NOT NULL,\n parent_work_item_id TEXT,\n correlation_id TEXT,\n ancestry_path TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),\n depends_on TEXT NOT NULL DEFAULT '[]',\n assigned_operative_id TEXT,\n construction_lock_id TEXT,\n branch TEXT,\n merged_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_construction_locks (\n id TEXT PRIMARY KEY,\n work_item_id TEXT NOT NULL,\n operative_id TEXT NOT NULL,\n acquired_at TEXT NOT NULL DEFAULT (datetime('now')),\n released_at TEXT,\n lease_duration_ms INTEGER NOT NULL DEFAULT 300000,\n lease_renewed_at INTEGER,\n lease_expires_at INTEGER NOT NULL DEFAULT 0,\n hook_ref TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_relay_messages (\n id TEXT PRIMARY KEY,\n from_operative_id TEXT NOT NULL,\n to_operative_id TEXT,\n strike_team_id TEXT,\n subject TEXT NOT NULL,\n body TEXT NOT NULL,\n sent_at TEXT NOT NULL DEFAULT (datetime('now')),\n read_at TEXT,\n priority TEXT NOT NULL DEFAULT 'normal'\n CHECK(priority IN ('normal','urgent'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_convergence_runs (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n strategy TEXT NOT NULL DEFAULT 'bisecting'\n CHECK(strategy IN ('sequential','bisecting')),\n work_item_ids TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running','merged','failed','bisecting','deferred')),\n failed_item_id TEXT,\n started_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_dispatch_queue (\n id TEXT PRIMARY KEY,\n operative_id TEXT NOT NULL,\n work_item_id TEXT NOT NULL,\n scheduled_at TEXT NOT NULL DEFAULT (datetime('now')),\n dispatched_at TEXT,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','dispatched','failed'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);\nCREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);\nCREATE INDEX IF NOT EXISTS idx_arch_locks_item ON architects_construction_locks(work_item_id, released_at);\nCREATE INDEX IF NOT EXISTS idx_arch_blueprints_team ON architects_blueprints(strike_team_id, worklist_id);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_op ON architects_relay_messages(to_operative_id, read_at);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_team ON architects_relay_messages(strike_team_id, read_at);\n";
|
|
3
3
|
export declare function initArchitectsSchema(db: ArchitectsDb): void;
|
|
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS architects_work_items (
|
|
|
27
27
|
correlation_id TEXT,
|
|
28
28
|
ancestry_path TEXT NOT NULL DEFAULT '[]',
|
|
29
29
|
status TEXT NOT NULL DEFAULT 'todo'
|
|
30
|
-
CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked')),
|
|
30
|
+
CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),
|
|
31
31
|
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
32
32
|
assigned_operative_id TEXT,
|
|
33
33
|
construction_lock_id TEXT,
|
|
@@ -100,10 +100,49 @@ function addColumnIfMissing(db, table, column, ddl) {
|
|
|
100
100
|
return;
|
|
101
101
|
db.prepare(ddl).run();
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Migrate architects_work_items to allow 'cancelled' status.
|
|
105
|
+
*
|
|
106
|
+
* SQLite cannot ALTER a CHECK constraint, so we recreate the table when the
|
|
107
|
+
* existing CHECK does not include 'cancelled'. Safe to call on new databases
|
|
108
|
+
* (the table won't exist yet — `CREATE TABLE IF NOT EXISTS` in ARCHITECTS_SCHEMA
|
|
109
|
+
* already includes 'cancelled', so nothing needs doing).
|
|
110
|
+
*/
|
|
111
|
+
function migrateWorkItemCancelledStatus(db) {
|
|
112
|
+
const existing = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='architects_work_items'`).get();
|
|
113
|
+
if (!existing?.sql)
|
|
114
|
+
return; // new DB — schema string already has 'cancelled'
|
|
115
|
+
if (existing.sql.includes("'cancelled'"))
|
|
116
|
+
return; // already migrated
|
|
117
|
+
db.exec(`
|
|
118
|
+
CREATE TABLE IF NOT EXISTS architects_work_items_v2 (
|
|
119
|
+
id TEXT PRIMARY KEY,
|
|
120
|
+
worklist_id TEXT NOT NULL,
|
|
121
|
+
title TEXT NOT NULL,
|
|
122
|
+
parent_work_item_id TEXT,
|
|
123
|
+
correlation_id TEXT,
|
|
124
|
+
ancestry_path TEXT NOT NULL DEFAULT '[]',
|
|
125
|
+
status TEXT NOT NULL DEFAULT 'todo'
|
|
126
|
+
CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),
|
|
127
|
+
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
128
|
+
assigned_operative_id TEXT,
|
|
129
|
+
construction_lock_id TEXT,
|
|
130
|
+
branch TEXT,
|
|
131
|
+
merged_at TEXT,
|
|
132
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
133
|
+
);
|
|
134
|
+
INSERT OR IGNORE INTO architects_work_items_v2 SELECT * FROM architects_work_items;
|
|
135
|
+
DROP TABLE architects_work_items;
|
|
136
|
+
ALTER TABLE architects_work_items_v2 RENAME TO architects_work_items;
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
103
141
|
export function initArchitectsSchema(db) {
|
|
104
142
|
// File-based forward-only migrations (PRAGMA user_version scheme).
|
|
105
143
|
runMigrations(db, 'architects');
|
|
106
144
|
db.exec(ARCHITECTS_SCHEMA);
|
|
145
|
+
migrateWorkItemCancelledStatus(db);
|
|
107
146
|
addColumnIfMissing(db, 'architects_work_items', 'parent_work_item_id', 'ALTER TABLE architects_work_items ADD COLUMN parent_work_item_id TEXT');
|
|
108
147
|
addColumnIfMissing(db, 'architects_work_items', 'correlation_id', 'ALTER TABLE architects_work_items ADD COLUMN correlation_id TEXT');
|
|
109
148
|
addColumnIfMissing(db, 'architects_work_items', 'ancestry_path', "ALTER TABLE architects_work_items ADD COLUMN ancestry_path TEXT NOT NULL DEFAULT '[]'");
|
|
@@ -5,7 +5,7 @@ import type { PersistentWorkLedger } from '../engines/work-ledger.js';
|
|
|
5
5
|
import type { NexusNetRelay } from '../engines/nexusnet-relay.js';
|
|
6
6
|
import type { EngineHealthStatus } from '../engines/runtime-health.js';
|
|
7
7
|
export type ArchitectsDb = BetterSqlite3.Database;
|
|
8
|
-
export type WorkItemStatus = 'todo' | 'claimed' | 'in_progress' | 'done' | 'failed' | 'blocked';
|
|
8
|
+
export type WorkItemStatus = 'todo' | 'claimed' | 'in_progress' | 'done' | 'failed' | 'blocked' | 'cancelled';
|
|
9
9
|
export type BlueprintStatus = 'draft' | 'active' | 'converging' | 'done' | 'archived';
|
|
10
10
|
export type ClaimScope = 'workitem' | 'goal' | 'mission' | 'crew' | 'project';
|
|
11
11
|
export interface Blueprint {
|
package/dist/daemon/client.d.ts
CHANGED
package/dist/daemon/client.js
CHANGED
|
@@ -16,6 +16,7 @@ function readCliPkgVersion() {
|
|
|
16
16
|
return 'unknown';
|
|
17
17
|
}
|
|
18
18
|
import { acquireDaemonLock, getDaemonLockPath, isProcessAlive, readDaemonLock, removeDaemonLock, } from './lock.js';
|
|
19
|
+
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 45_000;
|
|
19
20
|
function sleep(ms) {
|
|
20
21
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
22
|
}
|
|
@@ -111,7 +112,7 @@ function spawnDaemonProcess(workspace, entrypoint) {
|
|
|
111
112
|
child.unref();
|
|
112
113
|
}
|
|
113
114
|
export async function ensureDaemonReady(workspace, options = {}) {
|
|
114
|
-
const timeoutMs = options.timeoutMs ??
|
|
115
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
115
116
|
const existing = acquireDaemonLock(workspace);
|
|
116
117
|
if (existing.status === 'running') {
|
|
117
118
|
const currentVersion = readCliPkgVersion();
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class NexusDaemonServer {
|
|
|
13
13
|
private lockRecord;
|
|
14
14
|
private heartbeatTimer;
|
|
15
15
|
private stopping;
|
|
16
|
+
private runtimeReady;
|
|
16
17
|
constructor(workspace: WorkspaceContext);
|
|
17
18
|
private installProcessErrorHandlers;
|
|
18
19
|
/**
|
|
@@ -21,6 +22,7 @@ export declare class NexusDaemonServer {
|
|
|
21
22
|
* holding stale references.
|
|
22
23
|
*/
|
|
23
24
|
getLockRecord(): DaemonLockRecord | null;
|
|
25
|
+
private startHeartbeat;
|
|
24
26
|
start(): Promise<{
|
|
25
27
|
started: boolean;
|
|
26
28
|
record: DaemonLockRecord;
|
package/dist/daemon/server.js
CHANGED
|
@@ -101,6 +101,7 @@ export class NexusDaemonServer {
|
|
|
101
101
|
lockRecord = null;
|
|
102
102
|
heartbeatTimer = null;
|
|
103
103
|
stopping = false;
|
|
104
|
+
runtimeReady = false;
|
|
104
105
|
constructor(workspace) {
|
|
105
106
|
this.workspace = workspace;
|
|
106
107
|
this.httpServer = http.createServer((req, res) => {
|
|
@@ -156,6 +157,25 @@ export class NexusDaemonServer {
|
|
|
156
157
|
getLockRecord() {
|
|
157
158
|
return this.lockRecord;
|
|
158
159
|
}
|
|
160
|
+
startHeartbeat() {
|
|
161
|
+
if (this.heartbeatTimer) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.heartbeatTimer = setInterval(() => {
|
|
165
|
+
if (this.lockPath && this.lockRecord) {
|
|
166
|
+
try {
|
|
167
|
+
this.lockRecord = writeDaemonLock(this.lockPath, {
|
|
168
|
+
...this.lockRecord,
|
|
169
|
+
heartbeatAt: Date.now(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Non-fatal; worst case the next starter will detect us as stale.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
177
|
+
this.heartbeatTimer.unref?.();
|
|
178
|
+
}
|
|
159
179
|
async start() {
|
|
160
180
|
// Clear the stopping flag so a second start() after a supervisor-triggered
|
|
161
181
|
// stop() doesn't silently no-op via the SIGINT/SIGTERM handler guard.
|
|
@@ -171,6 +191,17 @@ export class NexusDaemonServer {
|
|
|
171
191
|
record: lock.record,
|
|
172
192
|
};
|
|
173
193
|
}
|
|
194
|
+
const port = await listen(this.httpServer, this.host);
|
|
195
|
+
this.lockRecord = writeDaemonLock(this.lockPath, {
|
|
196
|
+
...lock.record,
|
|
197
|
+
port,
|
|
198
|
+
token: this.authToken,
|
|
199
|
+
heartbeatAt: Date.now(),
|
|
200
|
+
pkgVersion: DAEMON_PKG_VERSION,
|
|
201
|
+
});
|
|
202
|
+
this.startHeartbeat();
|
|
203
|
+
this.installSignalHandlers();
|
|
204
|
+
this.installProcessErrorHandlers();
|
|
174
205
|
await runStartupHygiene({
|
|
175
206
|
repoRoot: this.workspace.repoRoot,
|
|
176
207
|
workspaceStateRoot: this.workspace.stateRoot,
|
|
@@ -183,6 +214,7 @@ export class NexusDaemonServer {
|
|
|
183
214
|
if (!preflight.ok) {
|
|
184
215
|
console.error(`[nexus-daemon] startup-error: ${preflight.code} — ${preflight.hint}`);
|
|
185
216
|
console.error(`[nexus-daemon] startup-error.json written to ${this.workspace.stateRoot}`);
|
|
217
|
+
await this.stop('startup-error');
|
|
186
218
|
return { started: false, record: lock.record };
|
|
187
219
|
}
|
|
188
220
|
// Integrity check: detect + auto-repair corrupt sqlite files before
|
|
@@ -196,6 +228,7 @@ export class NexusDaemonServer {
|
|
|
196
228
|
},
|
|
197
229
|
});
|
|
198
230
|
await this.nexus.start();
|
|
231
|
+
this.runtimeReady = true;
|
|
199
232
|
// Optional repo-search prewarm. It can be CPU-heavy in large workspaces, so
|
|
200
233
|
// keep daemon startup and dashboard responsiveness as the default behavior.
|
|
201
234
|
if (process.env.NEXUS_PREWARM_REPO_SEARCH === '1' && this.workspace.gitRoot) {
|
|
@@ -206,33 +239,6 @@ export class NexusDaemonServer {
|
|
|
206
239
|
// Warm the invoker availability cache so /api/runtimes returns fast and
|
|
207
240
|
// the dashboard runtime badges render immediately on first load.
|
|
208
241
|
setImmediate(() => { invokerRegistry.warmCache(); });
|
|
209
|
-
const port = await listen(this.httpServer, this.host);
|
|
210
|
-
this.lockRecord = writeDaemonLock(this.lockPath, {
|
|
211
|
-
...lock.record,
|
|
212
|
-
port,
|
|
213
|
-
token: this.authToken,
|
|
214
|
-
heartbeatAt: Date.now(),
|
|
215
|
-
pkgVersion: DAEMON_PKG_VERSION,
|
|
216
|
-
});
|
|
217
|
-
// Heartbeat: keep lockfile fresh so stale-lock detection (30 s TTL) works
|
|
218
|
-
// correctly after a SIGKILL. The interval is intentionally unref'd so it
|
|
219
|
-
// does not prevent clean shutdown.
|
|
220
|
-
this.heartbeatTimer = setInterval(() => {
|
|
221
|
-
if (this.lockPath && this.lockRecord) {
|
|
222
|
-
try {
|
|
223
|
-
this.lockRecord = writeDaemonLock(this.lockPath, {
|
|
224
|
-
...this.lockRecord,
|
|
225
|
-
heartbeatAt: Date.now(),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
// Non-fatal; worst case the next starter will detect us as stale.
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
233
|
-
this.heartbeatTimer.unref?.();
|
|
234
|
-
this.installSignalHandlers();
|
|
235
|
-
this.installProcessErrorHandlers();
|
|
236
242
|
return {
|
|
237
243
|
started: true,
|
|
238
244
|
record: this.lockRecord,
|
|
@@ -374,8 +380,11 @@ export class NexusDaemonServer {
|
|
|
374
380
|
return;
|
|
375
381
|
}
|
|
376
382
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
377
|
-
|
|
378
|
-
|
|
383
|
+
const ready = this.runtimeReady && this.nexus !== null;
|
|
384
|
+
sendJson(res, ready ? 200 : 503, {
|
|
385
|
+
ok: ready,
|
|
386
|
+
ready,
|
|
387
|
+
error: ready ? undefined : 'runtime-initializing',
|
|
379
388
|
pid: process.pid,
|
|
380
389
|
port: this.lockRecord?.port ?? 0,
|
|
381
390
|
stateKey: this.workspace.stateKey,
|
|
@@ -429,6 +438,7 @@ export class NexusDaemonServer {
|
|
|
429
438
|
return;
|
|
430
439
|
}
|
|
431
440
|
this.stopping = true;
|
|
441
|
+
this.runtimeReady = false;
|
|
432
442
|
nexusEventBus.emit('nexus.shutdown', { signal });
|
|
433
443
|
this.cleanupTimer.unref?.();
|
|
434
444
|
try {
|
|
@@ -27,6 +27,24 @@ import { mount as mountRuntimeBadges } from './widgets/runtime-badge.js';
|
|
|
27
27
|
|
|
28
28
|
const $ = id => document.getElementById(id);
|
|
29
29
|
const esc = s => s==null?'':String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
30
|
+
let memoryReloadTimer = null;
|
|
31
|
+
|
|
32
|
+
function _memoryApiUrl() {
|
|
33
|
+
return S.workspace?.repoName ? '/api/memory?repo='+encodeURIComponent(S.workspace.repoName) : '/api/memory';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _refreshMemorySoon() {
|
|
37
|
+
bustCache(_memoryApiUrl());
|
|
38
|
+
bustCache('/api/memory');
|
|
39
|
+
bustCache('/api/memory/health');
|
|
40
|
+
bustCache('/api/dashboard/surface/memory');
|
|
41
|
+
if (S.tab !== 'memory') return;
|
|
42
|
+
clearTimeout(memoryReloadTimer);
|
|
43
|
+
memoryReloadTimer = setTimeout(() => {
|
|
44
|
+
memoryReloadTimer = null;
|
|
45
|
+
Memory.load();
|
|
46
|
+
}, 150);
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
/* ─────────────────── Router ─────────────────── */
|
|
32
50
|
navRegister('board', Board.load);
|
|
@@ -53,15 +71,19 @@ setOnEvent(evt => {
|
|
|
53
71
|
Board.render();
|
|
54
72
|
_renderTicker();
|
|
55
73
|
}
|
|
56
|
-
// Refresh tool health tile on
|
|
57
|
-
if (
|
|
58
|
-
|
|
74
|
+
// Refresh tool health tile on tool lifecycle events.
|
|
75
|
+
if (['tool.invocation', 'mcp.handler.complete', 'mcp.handler.failed'].includes(String(evt.type||''))) {
|
|
76
|
+
bustCache('/api/runtime/tool-health');
|
|
77
|
+
bustCache('/api/dashboard/surface/operate');
|
|
78
|
+
if (tab === 'board') {
|
|
79
|
+
Board.loadToolHealth();
|
|
80
|
+
}
|
|
59
81
|
}
|
|
60
82
|
if (tab === 'workforce' && ['synapse','operative','mission'].includes(evt.category)) {
|
|
61
83
|
Workforce.render();
|
|
62
84
|
}
|
|
63
85
|
if (tab === 'memory' && evt.category === 'memory') {
|
|
64
|
-
|
|
86
|
+
_refreshMemorySoon();
|
|
65
87
|
}
|
|
66
88
|
if (tab === 'governance' && evt.category === 'darwin') {
|
|
67
89
|
Governance.load();
|
|
@@ -25,6 +25,7 @@ export declare class DashboardServer {
|
|
|
25
25
|
private repoTreeGenerator;
|
|
26
26
|
private gitUser;
|
|
27
27
|
private sseBroker;
|
|
28
|
+
private unsubscribeWorkspaceChanged;
|
|
28
29
|
constructor(options?: DashboardServerOptions);
|
|
29
30
|
start(): void;
|
|
30
31
|
stop(): void;
|
|
@@ -64,6 +65,8 @@ export declare class DashboardServer {
|
|
|
64
65
|
private readJsonIfExists;
|
|
65
66
|
private fileExists;
|
|
66
67
|
private collectUsageSnapshot;
|
|
68
|
+
private normalizeDashboardRepoToken;
|
|
69
|
+
private dashboardMemoryMatchesRepoName;
|
|
67
70
|
private listDashboardMemories;
|
|
68
71
|
private listCrossProjectMemories;
|
|
69
72
|
private listCrossProjectProjects;
|
package/dist/dashboard/server.js
CHANGED
|
@@ -107,6 +107,7 @@ export class DashboardServer {
|
|
|
107
107
|
repoTreeGenerator;
|
|
108
108
|
gitUser;
|
|
109
109
|
sseBroker = new SseBroker();
|
|
110
|
+
unsubscribeWorkspaceChanged = null;
|
|
110
111
|
constructor(options = {}) {
|
|
111
112
|
this.runtimeProvider = options.runtimeProvider;
|
|
112
113
|
this.orchestratorProvider = options.orchestratorProvider;
|
|
@@ -124,7 +125,16 @@ export class DashboardServer {
|
|
|
124
125
|
this.repoTreeGenerator = new RepoTreeGenerator(this.repoRoot);
|
|
125
126
|
this.gitUser = process.env.GIT_AUTHOR_NAME || process.env.GIT_COMMITTER_NAME || '';
|
|
126
127
|
this.server = http.createServer((req, res) => {
|
|
127
|
-
|
|
128
|
+
this.requestHandler(req, res).catch((err) => {
|
|
129
|
+
console.error('[Dashboard] requestHandler unhandled error:', err instanceof Error ? err.message : String(err));
|
|
130
|
+
if (!res.headersSent && res.writable) {
|
|
131
|
+
try {
|
|
132
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
133
|
+
res.end('Internal Server Error');
|
|
134
|
+
}
|
|
135
|
+
catch { /* socket already dead */ }
|
|
136
|
+
}
|
|
137
|
+
});
|
|
128
138
|
});
|
|
129
139
|
this.server.on('error', (error) => {
|
|
130
140
|
if (this.dashboardMode === 'bound') {
|
|
@@ -135,7 +145,7 @@ export class DashboardServer {
|
|
|
135
145
|
// is promoted (e.g. from a nexus_session_bootstrap file hint). Without this
|
|
136
146
|
// the dashboard keeps serving the stale "/" → "workspace" fallback for
|
|
137
147
|
// up to 30s after the real repo is discovered.
|
|
138
|
-
nexusEventBus.on('workspace.changed', (payload) => {
|
|
148
|
+
this.unsubscribeWorkspaceChanged = nexusEventBus.on('workspace.changed', (payload) => {
|
|
139
149
|
try {
|
|
140
150
|
this.repoRoot = payload.repoRoot || this.repoRoot;
|
|
141
151
|
this.repoTreeGenerator = new RepoTreeGenerator(this.repoRoot);
|
|
@@ -178,6 +188,10 @@ export class DashboardServer {
|
|
|
178
188
|
controller.abort();
|
|
179
189
|
}
|
|
180
190
|
this.activeProbeControllers.clear();
|
|
191
|
+
if (this.unsubscribeWorkspaceChanged) {
|
|
192
|
+
this.unsubscribeWorkspaceChanged();
|
|
193
|
+
this.unsubscribeWorkspaceChanged = null;
|
|
194
|
+
}
|
|
181
195
|
if (this.dashboardMode === 'bound') {
|
|
182
196
|
this.sseBroker.stop();
|
|
183
197
|
this.server.close();
|
|
@@ -762,6 +776,35 @@ export class DashboardServer {
|
|
|
762
776
|
latestRun: null,
|
|
763
777
|
};
|
|
764
778
|
}
|
|
779
|
+
normalizeDashboardRepoToken(value) {
|
|
780
|
+
return String(value ?? '').trim().replace(/^#/, '').toLowerCase();
|
|
781
|
+
}
|
|
782
|
+
dashboardMemoryMatchesRepoName(memory, repoName) {
|
|
783
|
+
const expected = this.normalizeDashboardRepoToken(repoName);
|
|
784
|
+
if (!expected)
|
|
785
|
+
return true;
|
|
786
|
+
const tags = Array.isArray(memory.tags) ? memory.tags : [];
|
|
787
|
+
const provenance = memory.provenance ?? {};
|
|
788
|
+
const candidates = [
|
|
789
|
+
...tags,
|
|
790
|
+
...(Array.isArray(provenance.tags) ? provenance.tags : []),
|
|
791
|
+
...(Array.isArray(provenance.containerTags) ? provenance.containerTags : []),
|
|
792
|
+
provenance.repoName,
|
|
793
|
+
provenance.repoId,
|
|
794
|
+
provenance.workspaceId,
|
|
795
|
+
provenance.projectId,
|
|
796
|
+
];
|
|
797
|
+
return candidates.some((candidate) => {
|
|
798
|
+
const token = this.normalizeDashboardRepoToken(candidate);
|
|
799
|
+
if (!token)
|
|
800
|
+
return false;
|
|
801
|
+
if (token === expected)
|
|
802
|
+
return true;
|
|
803
|
+
if (token === `repo:${expected}`)
|
|
804
|
+
return true;
|
|
805
|
+
return token.startsWith('repo:') && token.slice(5) === expected;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
765
808
|
listDashboardMemories(options = {}) {
|
|
766
809
|
const limit = Math.max(1, Number(options.limit || 40));
|
|
767
810
|
const raw = this.getMemory()?.listSnapshots(Math.min(limit * 3, 200), {
|
|
@@ -769,21 +812,20 @@ export class DashboardServer {
|
|
|
769
812
|
tag: options.tag,
|
|
770
813
|
linkedType: options.linkedType,
|
|
771
814
|
recencyMs: options.recencyMs,
|
|
815
|
+
state: options.includeHidden || options.lane === 'inbox' ? undefined : 'active',
|
|
772
816
|
lane: options.lane,
|
|
773
817
|
repoId: options.repoId,
|
|
774
818
|
workspaceId: options.workspaceId,
|
|
775
819
|
projectId: options.projectId,
|
|
776
820
|
includeHidden: options.includeHidden,
|
|
777
821
|
}) ?? [];
|
|
778
|
-
|
|
822
|
+
const visible = raw
|
|
779
823
|
.filter((memory) => {
|
|
780
824
|
const tags = Array.isArray(memory.tags) ? memory.tags : [];
|
|
781
825
|
if (!options.includeHidden && tags.includes('#system-hidden'))
|
|
782
826
|
return false;
|
|
783
827
|
if (!options.includeHidden && tags.includes('#auto') && Number(memory.priority ?? 0) < 0.4)
|
|
784
828
|
return false;
|
|
785
|
-
if (options.repoName && !tags.some((t) => t === `repo:${options.repoName}`))
|
|
786
|
-
return false;
|
|
787
829
|
if (tags.includes('#quarantine') && options.lane !== 'inbox')
|
|
788
830
|
return false;
|
|
789
831
|
if (!options.showPhantom && (tags.includes('#phantom-learning') || tags.includes('#swarm')))
|
|
@@ -793,8 +835,11 @@ export class DashboardServer {
|
|
|
793
835
|
if (!options.includeHidden && (tags.includes('#hidden') || tags.includes('#repo-profile') || tags.includes('#system-hidden')))
|
|
794
836
|
return false;
|
|
795
837
|
return true;
|
|
796
|
-
})
|
|
797
|
-
|
|
838
|
+
});
|
|
839
|
+
const scoped = options.repoName
|
|
840
|
+
? visible.filter((memory) => this.dashboardMemoryMatchesRepoName(memory, options.repoName))
|
|
841
|
+
: visible;
|
|
842
|
+
return (options.repoName && scoped.length === 0 ? visible : scoped).slice(0, limit);
|
|
798
843
|
}
|
|
799
844
|
listCrossProjectMemories(options) {
|
|
800
845
|
const memory = this.getMemory();
|
|
@@ -1465,6 +1510,10 @@ export class DashboardServer {
|
|
|
1465
1510
|
body: Buffer.concat(chunks).toString('utf8'),
|
|
1466
1511
|
});
|
|
1467
1512
|
});
|
|
1513
|
+
res.on('error', (error) => {
|
|
1514
|
+
cleanup();
|
|
1515
|
+
reject(error);
|
|
1516
|
+
});
|
|
1468
1517
|
});
|
|
1469
1518
|
req.on('timeout', () => {
|
|
1470
1519
|
req.destroy(new Error('probe-timeout'));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.snapshot' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'orchestration.decomposed' | 'orchestration.completed' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'workspace.changed' | 'nexus.shutdown' | 'nexus.daemon.error' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'orchestrator.warn' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'tool.invocation' | 'nexus.deprecation.used' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.coordination.publish.failed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'architects.event.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete' | 'async_gate.completed' | 'async_gate.failed' | 'install.step' | 'license.tierChanged' | 'license.upgradeNudge' | 'orchestrator.run.start' | 'orchestrator.run.complete';
|
|
1
|
+
export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.snapshot' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'orchestration.decomposed' | 'orchestration.completed' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'workspace.changed' | 'nexus.shutdown' | 'nexus.daemon.error' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'orchestrator.warn' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'tool.invocation' | 'nexus.deprecation.used' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.striketeam.finished' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.coordination.publish.failed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'architects.event.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete' | 'async_gate.completed' | 'async_gate.failed' | 'install.step' | 'license.tierChanged' | 'license.upgradeNudge' | 'orchestrator.run.start' | 'orchestrator.run.complete';
|
|
2
2
|
export interface NexusEventPayloads {
|
|
3
3
|
'system.boot': {
|
|
4
4
|
version: string;
|
|
@@ -463,6 +463,12 @@ export interface NexusEventPayloads {
|
|
|
463
463
|
worklistId?: string | null;
|
|
464
464
|
correlationId?: string | null;
|
|
465
465
|
};
|
|
466
|
+
'synapse.striketeam.finished': {
|
|
467
|
+
strikeTeamId: string;
|
|
468
|
+
intent: 'read' | 'orchestrate' | 'mutate';
|
|
469
|
+
outcome: 'completed' | 'cancelled' | 'failed';
|
|
470
|
+
worklistId?: string | null;
|
|
471
|
+
};
|
|
466
472
|
'synapse.mission.assigned': {
|
|
467
473
|
operativeId: string;
|
|
468
474
|
missionId: string;
|
|
@@ -149,6 +149,7 @@ export declare class KnowledgeFabricEngine {
|
|
|
149
149
|
private readonly patternRegistry;
|
|
150
150
|
private readonly memory?;
|
|
151
151
|
private readonly bundleCache;
|
|
152
|
+
private readonly snapshotCache;
|
|
152
153
|
constructor(options?: {
|
|
153
154
|
repoRoot?: string;
|
|
154
155
|
stateRoot?: string;
|
|
@@ -200,7 +201,8 @@ export declare class KnowledgeFabricEngine {
|
|
|
200
201
|
trace: ModelTierTrace[];
|
|
201
202
|
};
|
|
202
203
|
private persistBundle;
|
|
203
|
-
private
|
|
204
|
+
private persistBundleAsync;
|
|
205
|
+
private pruneRuntimeSnapshotsAsync;
|
|
204
206
|
private buildSourceMix;
|
|
205
207
|
private allocateTokenBudget;
|
|
206
208
|
private buildRecommendations;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import { promises as fsp } from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import { PatternRegistry } from './pattern-registry.js';
|
|
4
5
|
import { RuntimeRegistry, resolveNexusStateDir, } from './runtime-registry.js';
|
|
@@ -14,6 +15,7 @@ export class KnowledgeFabricEngine {
|
|
|
14
15
|
patternRegistry;
|
|
15
16
|
memory;
|
|
16
17
|
bundleCache = new Map();
|
|
18
|
+
snapshotCache = new Map();
|
|
17
19
|
constructor(options = {}) {
|
|
18
20
|
this.repoRoot = options.repoRoot ?? process.cwd();
|
|
19
21
|
this.stateDir = path.join(options.stateRoot ?? resolveNexusStateDir(), 'knowledge-fabric');
|
|
@@ -22,6 +24,7 @@ export class KnowledgeFabricEngine {
|
|
|
22
24
|
this.ragCollections = options.ragCollections ?? new RagCollectionStore(options.stateRoot);
|
|
23
25
|
this.patternRegistry = options.patternRegistry ?? new PatternRegistry(options.stateRoot);
|
|
24
26
|
this.memory = options.memory;
|
|
27
|
+
// eslint-disable-next-line no-restricted-syntax -- constructor sync mkdir is startup-only, not hot path
|
|
25
28
|
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
26
29
|
}
|
|
27
30
|
async compose(input) {
|
|
@@ -55,7 +58,7 @@ export class KnowledgeFabricEngine {
|
|
|
55
58
|
}
|
|
56
59
|
async composeStage(input, stage, cacheKey) {
|
|
57
60
|
const runtimeSnapshot = input.runtimeSnapshot ?? this.runtimeRegistry.read(input.runtimeId);
|
|
58
|
-
const fileRefs = input.candidateFiles.map((entry) => toFileRef(entry)).filter((entry) => Boolean(entry));
|
|
61
|
+
const fileRefs = (await Promise.all(input.candidateFiles.map((entry) => toFileRef(entry)))).filter((entry) => Boolean(entry));
|
|
59
62
|
const readingPlan = fileRefs.length > 0 ? await this.tokenEngine.plan(input.task, fileRefs, input.fileBoosts) : undefined;
|
|
60
63
|
const selectedFiles = readingPlan
|
|
61
64
|
? readingPlan.files.filter((file) => file.action !== 'skip').map((file) => file.file.path)
|
|
@@ -226,14 +229,13 @@ export class KnowledgeFabricEngine {
|
|
|
226
229
|
const existingTextLabels = new Set((fullCollection?.sources ?? [])
|
|
227
230
|
.filter((source) => source.kind === 'text')
|
|
228
231
|
.map((source) => source.label));
|
|
229
|
-
const
|
|
232
|
+
const seedCandidates = dedupePaths([
|
|
230
233
|
path.join(this.repoRoot, 'README.md'),
|
|
231
234
|
path.join(this.repoRoot, 'package.json'),
|
|
232
235
|
...input.candidateFiles,
|
|
233
|
-
])
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.slice(0, 6);
|
|
236
|
+
]);
|
|
237
|
+
const seedReadable = await Promise.all(seedCandidates.map((target) => isReadableBootstrapSeed(target)));
|
|
238
|
+
const seedPaths = seedCandidates.filter((_, i) => seedReadable[i]).slice(0, 6);
|
|
237
239
|
const ingestInputs = seedPaths
|
|
238
240
|
.filter((target) => !existingLocations.has(path.resolve(target)))
|
|
239
241
|
.map((target) => ({
|
|
@@ -241,7 +243,7 @@ export class KnowledgeFabricEngine {
|
|
|
241
243
|
label: path.relative(this.repoRoot, target) || path.basename(target),
|
|
242
244
|
tags: ['bootstrap-seeded', 'project-context'],
|
|
243
245
|
}));
|
|
244
|
-
const workspaceProfile = buildWorkspaceProfileSeed(this.repoRoot);
|
|
246
|
+
const workspaceProfile = await buildWorkspaceProfileSeed(this.repoRoot);
|
|
245
247
|
if (workspaceProfile && !existingTextLabels.has('workspace-profile')) {
|
|
246
248
|
ingestInputs.unshift({
|
|
247
249
|
text: workspaceProfile,
|
|
@@ -265,11 +267,20 @@ export class KnowledgeFabricEngine {
|
|
|
265
267
|
this.patternRegistry.recordOutcome(patternId, success);
|
|
266
268
|
}
|
|
267
269
|
getSessionSnapshot(runtimeId) {
|
|
270
|
+
// Serve from in-memory cache written by persistBundle — avoids disk read on hot path.
|
|
271
|
+
// Falls back to disk only if the snapshot was persisted by a previous process instance.
|
|
272
|
+
const cached = this.snapshotCache.get(runtimeId);
|
|
273
|
+
if (cached)
|
|
274
|
+
return cached;
|
|
268
275
|
const target = path.join(this.stateDir, runtimeId, 'latest.json');
|
|
276
|
+
// eslint-disable-next-line no-restricted-syntax -- cold-start fallback, runs at most once per runtimeId per process
|
|
269
277
|
if (!fs.existsSync(target))
|
|
270
278
|
return undefined;
|
|
271
279
|
try {
|
|
272
|
-
|
|
280
|
+
// eslint-disable-next-line no-restricted-syntax -- cold-start fallback, populates snapshotCache so subsequent calls are free
|
|
281
|
+
const snapshot = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
282
|
+
this.snapshotCache.set(runtimeId, snapshot);
|
|
283
|
+
return snapshot;
|
|
273
284
|
}
|
|
274
285
|
catch {
|
|
275
286
|
return undefined;
|
|
@@ -289,8 +300,6 @@ export class KnowledgeFabricEngine {
|
|
|
289
300
|
};
|
|
290
301
|
}
|
|
291
302
|
persistBundle(bundle) {
|
|
292
|
-
const runtimeDir = path.join(this.stateDir, bundle.runtimeId);
|
|
293
|
-
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
294
303
|
const snapshot = {
|
|
295
304
|
runtimeId: bundle.runtimeId,
|
|
296
305
|
sessionId: bundle.sessionId,
|
|
@@ -314,21 +323,43 @@ export class KnowledgeFabricEngine {
|
|
|
314
323
|
provenance: bundle.provenance,
|
|
315
324
|
summary: bundle.summary,
|
|
316
325
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
// Update in-memory cache immediately so getSessionSnapshot has zero disk latency.
|
|
327
|
+
this.snapshotCache.set(bundle.runtimeId, snapshot);
|
|
328
|
+
// Persist to disk asynchronously — callers never await this return value.
|
|
329
|
+
void this.persistBundleAsync(bundle.runtimeId, bundle.sessionId, snapshot);
|
|
330
|
+
}
|
|
331
|
+
async persistBundleAsync(runtimeId, sessionId, snapshot) {
|
|
332
|
+
const runtimeDir = path.join(this.stateDir, runtimeId);
|
|
333
|
+
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
334
|
+
const serialized = JSON.stringify(snapshot, null, 2);
|
|
335
|
+
await Promise.all([
|
|
336
|
+
fsp.writeFile(path.join(runtimeDir, 'latest.json'), serialized, 'utf8'),
|
|
337
|
+
fsp.writeFile(path.join(runtimeDir, `${sessionId}.json`), serialized, 'utf8'),
|
|
338
|
+
]);
|
|
339
|
+
await this.pruneRuntimeSnapshotsAsync(runtimeDir);
|
|
340
|
+
}
|
|
341
|
+
async pruneRuntimeSnapshotsAsync(runtimeDir) {
|
|
342
|
+
let entries;
|
|
343
|
+
try {
|
|
344
|
+
entries = await fsp.readdir(runtimeDir);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return;
|
|
331
348
|
}
|
|
349
|
+
const candidates = entries.filter((entry) => entry.endsWith('.json') && entry !== 'latest.json');
|
|
350
|
+
if (candidates.length <= MAX_RUNTIME_SESSION_SNAPSHOTS)
|
|
351
|
+
return;
|
|
352
|
+
const withMtimes = await Promise.all(candidates.map(async (entry) => {
|
|
353
|
+
try {
|
|
354
|
+
const stat = await fsp.stat(path.join(runtimeDir, entry));
|
|
355
|
+
return { entry, mtimeMs: stat.mtimeMs };
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return { entry, mtimeMs: 0 };
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
const stale = withMtimes.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_RUNTIME_SESSION_SNAPSHOTS);
|
|
362
|
+
await Promise.all(stale.map(({ entry }) => fsp.unlink(path.join(runtimeDir, entry)).catch(() => { })));
|
|
332
363
|
}
|
|
333
364
|
buildSourceMix(input) {
|
|
334
365
|
// Memory quality factor: scales memory weight by avg confidence × trust, penalizes high entropy
|
|
@@ -576,25 +607,26 @@ function normalizeBudget(base, totalBudget) {
|
|
|
576
607
|
}
|
|
577
608
|
return normalized;
|
|
578
609
|
}
|
|
579
|
-
function buildWorkspaceProfileSeed(repoRoot) {
|
|
610
|
+
async function buildWorkspaceProfileSeed(repoRoot) {
|
|
580
611
|
const repoName = path.basename(repoRoot) || 'workspace';
|
|
581
|
-
const packageName = readPackageName(repoRoot);
|
|
582
|
-
const topLevel = safeListDir(repoRoot)
|
|
612
|
+
const packageName = await readPackageName(repoRoot);
|
|
613
|
+
const topLevel = (await safeListDir(repoRoot))
|
|
583
614
|
.slice(0, 10)
|
|
584
615
|
.map((entry) => entry.name)
|
|
585
616
|
.join(', ');
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
617
|
+
const focusAreaCandidates = ['src/synapse', 'src/architects', 'src/dashboard', 'src/phantom', 'src/engines', 'test', 'docs'];
|
|
618
|
+
const focusAreaExists = await Promise.all(focusAreaCandidates.map((relativePath) => fsp.access(path.join(repoRoot, relativePath)).then(() => true, () => false)));
|
|
619
|
+
const focusAreas = focusAreaCandidates.filter((_, i) => focusAreaExists[i]).join(', ');
|
|
589
620
|
return [
|
|
590
621
|
`Workspace profile: ${repoName}${packageName && packageName !== repoName ? ` (package ${packageName})` : ''}.`,
|
|
591
622
|
topLevel ? `Top-level entries: ${topLevel}.` : null,
|
|
592
623
|
focusAreas ? `Key areas: ${focusAreas}.` : null,
|
|
593
624
|
].filter(Boolean).join(' ');
|
|
594
625
|
}
|
|
595
|
-
function safeListDir(target) {
|
|
626
|
+
async function safeListDir(target) {
|
|
596
627
|
try {
|
|
597
|
-
|
|
628
|
+
const entries = await fsp.readdir(target, { withFileTypes: true });
|
|
629
|
+
return entries
|
|
598
630
|
.filter((entry) => !entry.name.startsWith('.'))
|
|
599
631
|
.map((entry) => ({ name: entry.name }));
|
|
600
632
|
}
|
|
@@ -602,21 +634,19 @@ function safeListDir(target) {
|
|
|
602
634
|
return [];
|
|
603
635
|
}
|
|
604
636
|
}
|
|
605
|
-
function readPackageName(repoRoot) {
|
|
637
|
+
async function readPackageName(repoRoot) {
|
|
606
638
|
const packageJsonPath = path.join(repoRoot, 'package.json');
|
|
607
|
-
if (!fs.existsSync(packageJsonPath))
|
|
608
|
-
return null;
|
|
609
639
|
try {
|
|
610
|
-
const parsed = JSON.parse(
|
|
640
|
+
const parsed = JSON.parse(await fsp.readFile(packageJsonPath, 'utf8'));
|
|
611
641
|
return typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : null;
|
|
612
642
|
}
|
|
613
643
|
catch {
|
|
614
644
|
return null;
|
|
615
645
|
}
|
|
616
646
|
}
|
|
617
|
-
function toFileRef(filePath) {
|
|
647
|
+
async function toFileRef(filePath) {
|
|
618
648
|
try {
|
|
619
|
-
const stat =
|
|
649
|
+
const stat = await fsp.stat(filePath);
|
|
620
650
|
return {
|
|
621
651
|
path: filePath,
|
|
622
652
|
sizeBytes: stat.size,
|
|
@@ -636,9 +666,9 @@ function dedupeStrings(values) {
|
|
|
636
666
|
function estimateTokens(value) {
|
|
637
667
|
return Math.max(1, Math.ceil(String(value || '').length / 4));
|
|
638
668
|
}
|
|
639
|
-
function isReadableBootstrapSeed(target) {
|
|
669
|
+
async function isReadableBootstrapSeed(target) {
|
|
640
670
|
try {
|
|
641
|
-
const stat =
|
|
671
|
+
const stat = await fsp.stat(target);
|
|
642
672
|
if (!stat.isFile() || stat.size > 96_000) {
|
|
643
673
|
return false;
|
|
644
674
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -281,3 +281,10 @@ export declare class NexusPrime {
|
|
|
281
281
|
private getDefaultCapabilities;
|
|
282
282
|
}
|
|
283
283
|
export declare const createNexusPrime: (config?: Partial<NexusConfig>) => NexusPrime;
|
|
284
|
+
export { GhostPass, PhantomOrchestrator, PhantomWorker } from './phantom/index.js';
|
|
285
|
+
export type { GhostReport, WorkerTask, WorkerResult, MergeDecision } from './phantom/index.js';
|
|
286
|
+
export { MemoryEngine, createMemoryEngine } from './engines/memory.js';
|
|
287
|
+
export { SessionDNAManager } from './engines/session-dna.js';
|
|
288
|
+
export type { SessionDNA } from './engines/session-dna.js';
|
|
289
|
+
export { GithubBridge, getSharedGithubBridge } from './engines/github-bridge.js';
|
|
290
|
+
export type { GithubPromotionInput, GithubPromotionResult } from './engines/github-bridge.js';
|
package/dist/index.js
CHANGED
|
@@ -1193,3 +1193,8 @@ function detectCurrentClient() {
|
|
|
1193
1193
|
}
|
|
1194
1194
|
return null;
|
|
1195
1195
|
}
|
|
1196
|
+
// Public API surface for sibling packages (e.g., @nexus-prime/finishit)
|
|
1197
|
+
export { GhostPass, PhantomOrchestrator, PhantomWorker } from './phantom/index.js';
|
|
1198
|
+
export { MemoryEngine, createMemoryEngine } from './engines/memory.js';
|
|
1199
|
+
export { SessionDNAManager } from './engines/session-dna.js';
|
|
1200
|
+
export { GithubBridge, getSharedGithubBridge } from './engines/github-bridge.js';
|
|
@@ -166,5 +166,10 @@ export async function executeMandatePipeline(db, mandateText, providers, opts =
|
|
|
166
166
|
});
|
|
167
167
|
return getStrikeTeam(db, teamId);
|
|
168
168
|
})();
|
|
169
|
+
nexusEventBus.emit('synapse.striketeam.finished', {
|
|
170
|
+
strikeTeamId: team.id,
|
|
171
|
+
intent: signals.complexity,
|
|
172
|
+
outcome: 'completed',
|
|
173
|
+
});
|
|
169
174
|
return team;
|
|
170
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.2",
|
|
4
4
|
"description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|