nexus-prime 7.9.19 → 7.9.21
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/definitions.js +2 -3
- package/dist/agents/adapters/mcp/handlers/governance.js +1 -1
- package/dist/agents/adapters/mcp/handlers/runtime.js +1 -1
- package/dist/agents/adapters/mcp/types.d.ts +6 -0
- package/dist/agents/adapters/mcp.js +17 -9
- package/dist/cli/doctor-storage.js +1 -2
- package/dist/cli.js +23 -17
- package/dist/daemon/client.d.ts +1 -0
- package/dist/daemon/server.js +1 -0
- package/dist/dashboard/app/views/board.js +19 -5
- package/dist/dashboard/app/views/runtime.js +110 -14
- package/dist/dashboard/server.js +22 -1
- package/dist/engines/ngram-index.d.ts +4 -1
- package/dist/engines/ngram-index.js +16 -14
- package/dist/engines/workflow-runtime.js +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/synapse/operatives/crud.js +5 -1
- package/package.json +1 -1
|
@@ -976,11 +976,10 @@ export function buildMcpToolDefinitions() {
|
|
|
976
976
|
required: ['taskId', 'goal', 'findings'],
|
|
977
977
|
},
|
|
978
978
|
},
|
|
979
|
-
// When NEXUS_DISABLE_WORKFORCE=1 (
|
|
979
|
+
// When NEXUS_DISABLE_WORKFORCE=1 (explicit break-glass mode),
|
|
980
980
|
// strip the 24 nexus_synapse_* / nexus_architects_* tools from the
|
|
981
981
|
// catalog so the model doesn't see surface area for engines that
|
|
982
|
-
// aren't initialized at runtime.
|
|
983
|
-
// re-enable both engines + their tools.
|
|
982
|
+
// aren't initialized at runtime.
|
|
984
983
|
...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : synapseToolDefinitions),
|
|
985
984
|
...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : architectsToolDefinitions),
|
|
986
985
|
// ── Workforce (unified worker+job layer) ──────────────────────────
|
|
@@ -173,7 +173,7 @@ export async function handleGovernanceGroup(toolName, hctx, request, args, ctx)
|
|
|
173
173
|
const manifest = ensureBootstrap({
|
|
174
174
|
packageRoot: PROJECT_ROOT,
|
|
175
175
|
workspaceRoot: hctx.nexusRef.getWorkspaceContext().workspaceRoot,
|
|
176
|
-
phase: '
|
|
176
|
+
phase: 'install',
|
|
177
177
|
silent: true,
|
|
178
178
|
});
|
|
179
179
|
const memoryMaintenance = hctx.nexusRef.maintainMemory();
|
|
@@ -77,7 +77,7 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
|
|
|
77
77
|
if (!requestedTool) {
|
|
78
78
|
return { content: [{ type: 'text', text: 'tool_name is required.' }] };
|
|
79
79
|
}
|
|
80
|
-
const allToolDefs =
|
|
80
|
+
const allToolDefs = hctx.getDecoratedToolDefinitions(hctx.getToolProfile());
|
|
81
81
|
const found = allToolDefs.find(t => t.name === requestedTool);
|
|
82
82
|
if (!found) {
|
|
83
83
|
const available = allToolDefs.map(t => t.name).join(', ');
|
|
@@ -110,6 +110,12 @@ export interface McpHandlerCtx {
|
|
|
110
110
|
getDarwinLoop(args?: Record<string, unknown>): DarwinLoop;
|
|
111
111
|
/** Current MCP tool profile — 'autonomous' or 'full' */
|
|
112
112
|
getToolProfile(): McpToolProfile;
|
|
113
|
+
/** Get decorated tool definitions for lazy tool discovery */
|
|
114
|
+
getDecoratedToolDefinitions(profile?: McpToolProfile): Array<{
|
|
115
|
+
name: string;
|
|
116
|
+
description: string;
|
|
117
|
+
inputSchema: Record<string, unknown>;
|
|
118
|
+
}>;
|
|
113
119
|
/** True if `target` looks like a package (node_modules, built artifacts, etc.) */
|
|
114
120
|
isPackageLikeWorkspace(target: string): boolean;
|
|
115
121
|
/** Resolve a candidate path relative to the workspace root */
|
|
@@ -623,7 +623,7 @@ export class MCPAdapter {
|
|
|
623
623
|
: 'Full MCP profile active. Low-level and authoring tools are exposed.';
|
|
624
624
|
}
|
|
625
625
|
finalizeToolDefinitions(tools, profile = this.getToolProfile()) {
|
|
626
|
-
const taskContext = this.currentTask || this.
|
|
626
|
+
const taskContext = this.currentTask || this.runtime?.getUsageSnapshot()?.orchestration?.lastPrompt || '';
|
|
627
627
|
const userTier = getSharedLicenseManager().getStatus().tier;
|
|
628
628
|
const profileFiltered = profile === 'full'
|
|
629
629
|
? tools.slice()
|
|
@@ -715,14 +715,21 @@ export class MCPAdapter {
|
|
|
715
715
|
setupToolHandlers() {
|
|
716
716
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
717
717
|
const profile = this.getToolProfile();
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
try {
|
|
720
|
+
this.getRuntime().recordClientInstructionStatus({
|
|
721
|
+
clientId: this.name,
|
|
722
|
+
clientFamily: this.name === 'openclaw' ? 'antigravity' : this.name,
|
|
723
|
+
toolProfile: profile,
|
|
724
|
+
status: profile === 'autonomous' ? 'guided' : 'manual',
|
|
725
|
+
summary: this.describeClientInstructionStatus(profile),
|
|
726
|
+
updatedAt: Date.now(),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
// Tool enumeration must stay fast and side-effect best-effort.
|
|
731
|
+
}
|
|
732
|
+
}, 0);
|
|
726
733
|
return {
|
|
727
734
|
tools: this.listTools(profile),
|
|
728
735
|
};
|
|
@@ -957,6 +964,7 @@ export class MCPAdapter {
|
|
|
957
964
|
getRepoNgramIndex: (a = {}) => self.getRepoNgramIndex(a),
|
|
958
965
|
getDarwinLoop: (a = {}) => self.getDarwinLoop(a),
|
|
959
966
|
getToolProfile: () => self.getToolProfile(),
|
|
967
|
+
getDecoratedToolDefinitions: (profile) => self.getDecoratedToolDefinitions(profile),
|
|
960
968
|
isPackageLikeWorkspace: (target) => self.isPackageLikeWorkspace(target),
|
|
961
969
|
resolveToolPath: (p, a = {}) => self.resolveToolPath(p, a),
|
|
962
970
|
telemetry: self.telemetry,
|
|
@@ -12,10 +12,9 @@ import { resolveRunsBudget, resolveWorktreeBudget } from './cleanup.js';
|
|
|
12
12
|
import { dirBytes, formatBytes } from '../install/fs-purge.js';
|
|
13
13
|
import { enumerateNgramArchives, enumerateStatePaths, getNexusStateDir, getRuntimeTmpRoots, getWorktreeRoots, } from '../install/state-locator.js';
|
|
14
14
|
import { INSTALL_ARCH_GENERATION, loadManifest, } from '../install/manifest.js';
|
|
15
|
-
import { getNgramFootprintBytes } from '../engines/ngram-index.js';
|
|
15
|
+
import { getNgramFootprintBytes, NGRAM_DEFAULT_FOOTPRINT_BYTES } from '../engines/ngram-index.js';
|
|
16
16
|
import { getSharedLicenseManager } from '../licensing/license-manager.js';
|
|
17
17
|
const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
18
|
-
const NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024;
|
|
19
18
|
function readEnvBytesPositive(name, fallback) {
|
|
20
19
|
const raw = process.env[name];
|
|
21
20
|
if (!raw)
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { cliSetup, configureIDE, computeFileHash, readSetupMarker, writeSetupMar
|
|
|
30
30
|
import { isNewUser, promptLicenseKey, printReturningUserBanner } from './cli/interactive-setup.js';
|
|
31
31
|
import { runHookBootstrap, runHookMemory, runHookMindkit, runHookGhostPass, runHookSessionDna } from './cli/hook.js';
|
|
32
32
|
import { resolveWorkspaceContext } from './engines/workspace-resolver.js';
|
|
33
|
-
import { ensureDaemonReady, getDaemonStatus, stopDaemon } from './daemon/client.js';
|
|
33
|
+
import { ensureDaemonReady, getDaemonStatus, pingDaemonHealth, stopDaemon } from './daemon/client.js';
|
|
34
34
|
import { NexusDaemonServer } from './daemon/server.js';
|
|
35
35
|
import { DaemonSupervisor } from './daemon/supervisor.js';
|
|
36
36
|
import { startDaemonBackedMcpProxy } from './daemon/proxy.js';
|
|
@@ -720,8 +720,10 @@ program
|
|
|
720
720
|
try {
|
|
721
721
|
const record = await ensureDaemonManaged({ force: options.force });
|
|
722
722
|
const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
|
|
723
|
+
const health = await pingDaemonHealth(record).catch(() => null);
|
|
724
|
+
const dashboardUrl = health?.dashboardUrl ?? `http://localhost:${dashboardPort}`;
|
|
723
725
|
console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
|
|
724
|
-
console.log(`Dashboard:
|
|
726
|
+
console.log(`Dashboard: ${dashboardUrl}`);
|
|
725
727
|
}
|
|
726
728
|
catch (err) {
|
|
727
729
|
console.error(`Failed to start daemon: ${err.message}`);
|
|
@@ -820,15 +822,14 @@ program
|
|
|
820
822
|
if (process.env.NEXUS_DAEMON_FAST_START === undefined) {
|
|
821
823
|
process.env.NEXUS_DAEMON_FAST_START = '1';
|
|
822
824
|
}
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
process.env.NEXUS_DISABLE_WORKFORCE = '1';
|
|
825
|
+
// Keep daemon startup fast, but do not disable workforce. Synapse and
|
|
826
|
+
// Architects can lazily warm on first hire/tool call unless the operator
|
|
827
|
+
// explicitly sets the NEXUS_DISABLE_WORKFORCE escape hatch.
|
|
828
|
+
if (process.env.NEXUS_SYNAPSE_LAZY === undefined) {
|
|
829
|
+
process.env.NEXUS_SYNAPSE_LAZY = '1';
|
|
830
|
+
}
|
|
831
|
+
if (process.env.NEXUS_ARCHITECTS_LAZY === undefined) {
|
|
832
|
+
process.env.NEXUS_ARCHITECTS_LAZY = '1';
|
|
832
833
|
}
|
|
833
834
|
const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
|
|
834
835
|
const daemon = new NexusDaemonServer(workspaceContext);
|
|
@@ -976,11 +977,16 @@ program
|
|
|
976
977
|
});
|
|
977
978
|
}
|
|
978
979
|
console.error('Starting Nexus Prime MCP Server (standalone)...');
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
980
|
+
setTimeout(() => {
|
|
981
|
+
void runStartupHygiene({
|
|
982
|
+
repoRoot: workspaceContext.repoRoot,
|
|
983
|
+
workspaceStateRoot: workspaceContext.stateRoot,
|
|
984
|
+
mode: 'stale',
|
|
985
|
+
}).catch((error) => {
|
|
986
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
987
|
+
console.error(`[nexus-prime] Startup hygiene skipped: ${message}`);
|
|
988
|
+
});
|
|
989
|
+
}, 0);
|
|
984
990
|
nexus = createNexusPrime({
|
|
985
991
|
adapters: ['mcp'],
|
|
986
992
|
runtime: {
|
|
@@ -995,8 +1001,8 @@ program
|
|
|
995
1001
|
if (!adapterReady) {
|
|
996
1002
|
throw new Error('MCP adapter did not become ready before the startup deadline.');
|
|
997
1003
|
}
|
|
998
|
-
await startup;
|
|
999
1004
|
flushPrimedMcpStdioInput();
|
|
1005
|
+
await startup;
|
|
1000
1006
|
console.error('Nexus Prime MCP Server running on stdio (standalone)');
|
|
1001
1007
|
console.error('Memory persistence: active (~/.nexus-prime/memory.db)');
|
|
1002
1008
|
const shutdown = async (signal) => {
|
package/dist/daemon/client.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -390,6 +390,7 @@ export class NexusDaemonServer {
|
|
|
390
390
|
stateKey: this.workspace.stateKey,
|
|
391
391
|
workspaceRoot: this.workspace.workspaceRoot,
|
|
392
392
|
repoRoot: this.workspace.repoRoot,
|
|
393
|
+
dashboardUrl: this.nexus?.getDashboardAddress?.() ?? null,
|
|
393
394
|
startedAt: this.lockRecord?.startedAt ?? Date.now(),
|
|
394
395
|
});
|
|
395
396
|
return;
|
|
@@ -258,10 +258,15 @@ function renderPyramidWidget() {
|
|
|
258
258
|
if (!container) return;
|
|
259
259
|
const h = S.memHealth;
|
|
260
260
|
if (!h) { container.innerHTML = ''; return; }
|
|
261
|
+
const tierCounts = h.tierCounts || {};
|
|
262
|
+
const count = (...values) => {
|
|
263
|
+
const positive = values.find(v => Number(v) > 0);
|
|
264
|
+
return positive != null ? Number(positive) : 0;
|
|
265
|
+
};
|
|
261
266
|
const counts = {
|
|
262
|
-
prefrontal: h.working
|
|
263
|
-
hippocampus: h.episodic
|
|
264
|
-
cortex: h.semantic
|
|
267
|
+
prefrontal: count(h.working, h.prefrontal, tierCounts.prefrontal),
|
|
268
|
+
hippocampus: count(h.episodic, h.hippocampus, tierCounts.hippocampus),
|
|
269
|
+
cortex: count(h.semantic, h.cortex, tierCounts.cortex),
|
|
265
270
|
};
|
|
266
271
|
renderPyramid(container, counts, _activeTier, tier => {
|
|
267
272
|
_activeTier = tier;
|
|
@@ -793,6 +798,14 @@ function renderOrchestrationPipeline() {
|
|
|
793
798
|
}
|
|
794
799
|
host.style.display = 'block';
|
|
795
800
|
const same = dec && cmp && dec.runId === cmp.runId;
|
|
801
|
+
const completionState = (completion) => {
|
|
802
|
+
const state = completion?.state || '';
|
|
803
|
+
const result = String(completion?.result || '');
|
|
804
|
+
if (state === 'failed' && /without a repository patch|no applicable diff|no selected mutation bindings|advisory/i.test(result)) {
|
|
805
|
+
return 'inspected';
|
|
806
|
+
}
|
|
807
|
+
return state;
|
|
808
|
+
};
|
|
796
809
|
const stateColor = (state) => state === 'merged' || state === 'inspected' ? 'var(--accent-good, #4ade80)'
|
|
797
810
|
: state === 'rolled_back' ? 'var(--accent-warn, #fbbf24)'
|
|
798
811
|
: state === 'failed' ? 'var(--accent-bad, #ff5f57)'
|
|
@@ -814,9 +827,10 @@ function renderOrchestrationPipeline() {
|
|
|
814
827
|
${chipList('skills', dec.skills)}
|
|
815
828
|
${chipList('files', dec.files)}
|
|
816
829
|
</div>` : '';
|
|
830
|
+
const cmpState = completionState(cmp);
|
|
817
831
|
const cmpBlock = cmp ? `
|
|
818
|
-
<div style="border-left:3px solid ${stateColor(
|
|
819
|
-
<div style="font-size:13px;font-weight:600;margin-bottom:4px">Completion · run ${esc((cmp.runId || '').slice(-8))} · <span style="color:${stateColor(
|
|
832
|
+
<div style="border-left:3px solid ${stateColor(cmpState)};padding:8px 12px">
|
|
833
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:4px">Completion · run ${esc((cmp.runId || '').slice(-8))} · <span style="color:${stateColor(cmpState)}">${esc(cmpState || '')}</span></div>
|
|
820
834
|
<div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(cmp.result || '')}</div>
|
|
821
835
|
<div>${chip('verified', `${cmp.verifiedWorkers ?? 0}/${cmp.totalWorkers ?? 0}`)}${chip('saved', `${fmtNum(cmp.savedTokens ?? 0)} t`)}${chip('compression', `${cmp.compressionPct ?? 0}%`)}${chip('duration', `${Math.round((cmp.durationMs ?? 0) / 100) / 10}s`)}</div>
|
|
822
836
|
</div>` : '';
|
|
@@ -14,6 +14,7 @@ const esc = s => s == null ? '' : String(s)
|
|
|
14
14
|
const MAX_EVENTS = 200;
|
|
15
15
|
const MAX_RESOLVED_MCP = 24;
|
|
16
16
|
const LONG_CALL_MS = 2000;
|
|
17
|
+
const STALE_CALL_MS = 5 * 60 * 1000;
|
|
17
18
|
const _events = [];
|
|
18
19
|
let _mounted = false;
|
|
19
20
|
let _filter = 'all';
|
|
@@ -23,6 +24,8 @@ let _toolCalls = 0;
|
|
|
23
24
|
let _activeTools = new Map();
|
|
24
25
|
// resolved MCP calls (newest-first, capped at MAX_RESOLVED_MCP)
|
|
25
26
|
let _resolvedMcp = [];
|
|
27
|
+
// tool name -> latest completion/shutdown timestamp, used to ignore older replayed starts
|
|
28
|
+
let _settledToolTimes = new Map();
|
|
26
29
|
// active-bar pulse timer
|
|
27
30
|
let _pulseTimer = null;
|
|
28
31
|
// toast queue
|
|
@@ -62,6 +65,89 @@ function humanMs(ms) {
|
|
|
62
65
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function toolNameFromPayload(payload) {
|
|
69
|
+
return String(payload.toolName ?? payload.tool ?? payload.name ?? '').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function eventMillis(evt, payload) {
|
|
73
|
+
const raw = evt?.time ?? payload?.time ?? payload?.ts ?? null;
|
|
74
|
+
const numeric = Number(raw);
|
|
75
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric;
|
|
76
|
+
const parsed = Date.parse(String(raw ?? ''));
|
|
77
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function capResolvedMcp() {
|
|
81
|
+
if (_resolvedMcp.length > MAX_RESOLVED_MCP) _resolvedMcp.length = MAX_RESOLVED_MCP;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rememberSettledTool(tool, eventTime) {
|
|
85
|
+
if (!tool) return;
|
|
86
|
+
const previous = _settledToolTimes.get(tool) ?? 0;
|
|
87
|
+
if (eventTime >= previous) _settledToolTimes.set(tool, eventTime);
|
|
88
|
+
if (_settledToolTimes.size > MAX_RESOLVED_MCP * 2) {
|
|
89
|
+
const oldest = [..._settledToolTimes.entries()].sort((a, b) => a[1] - b[1])[0]?.[0];
|
|
90
|
+
if (oldest) _settledToolTimes.delete(oldest);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wasSettledAfter(tool, eventTime) {
|
|
95
|
+
const settledAt = tool ? _settledToolTimes.get(tool) : null;
|
|
96
|
+
return Number.isFinite(settledAt) && Number.isFinite(eventTime) && eventTime <= settledAt;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function activeToolKey(trackKey, tool) {
|
|
100
|
+
if (trackKey && _activeTools.has(trackKey)) return trackKey;
|
|
101
|
+
if (tool && _activeTools.has(tool)) return tool;
|
|
102
|
+
if (tool) {
|
|
103
|
+
for (const [key, entry] of _activeTools.entries()) {
|
|
104
|
+
if (entry.tool === tool) return key;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!trackKey && !tool && _activeTools.size === 1) {
|
|
108
|
+
return _activeTools.keys().next().value;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function settleActiveTool(trackKey, tool, payload = {}, opts = {}) {
|
|
114
|
+
const key = activeToolKey(trackKey, tool);
|
|
115
|
+
if (!key) return false;
|
|
116
|
+
const entry = _activeTools.get(key);
|
|
117
|
+
if (!entry) return false;
|
|
118
|
+
const elapsed = Number(opts.durationMs ?? payload.durationMs ?? 0) || (Date.now() - entry.startedAt);
|
|
119
|
+
const status = String(payload.status ?? '').toLowerCase();
|
|
120
|
+
_resolvedMcp.unshift({
|
|
121
|
+
tool: entry.tool || tool || key,
|
|
122
|
+
durationMs: elapsed,
|
|
123
|
+
error: payload.error ?? (status && status !== 'ok' ? status : null),
|
|
124
|
+
stale: opts.stale === true,
|
|
125
|
+
});
|
|
126
|
+
rememberSettledTool(entry.tool || tool || key, Number(opts.eventTime ?? Date.now()));
|
|
127
|
+
capResolvedMcp();
|
|
128
|
+
_activeTools.delete(key);
|
|
129
|
+
if (entry.callId) _activeTools.delete(entry.callId);
|
|
130
|
+
if (entry.tool) _activeTools.delete(entry.tool);
|
|
131
|
+
if (trackKey) _activeTools.delete(trackKey);
|
|
132
|
+
if (tool) _activeTools.delete(tool);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function expireStaleActiveTools() {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
for (const [key, entry] of [..._activeTools.entries()]) {
|
|
139
|
+
if (now - entry.startedAt > STALE_CALL_MS) {
|
|
140
|
+
settleActiveTool(key, entry.tool, {}, { stale: true, durationMs: now - entry.startedAt });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function clearActiveToolsAsStale() {
|
|
146
|
+
for (const [key, entry] of [..._activeTools.entries()]) {
|
|
147
|
+
settleActiveTool(key, entry.tool, {}, { stale: true, durationMs: Date.now() - entry.startedAt });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
65
151
|
/* ── Mount ──────────────────────────────────────────────────────────────────── */
|
|
66
152
|
function mount() {
|
|
67
153
|
const el = $('runtime-view');
|
|
@@ -122,6 +208,7 @@ function mount() {
|
|
|
122
208
|
_totalTokensSaved = 0;
|
|
123
209
|
_toolCalls = 0;
|
|
124
210
|
_activeTools.clear();
|
|
211
|
+
_settledToolTimes.clear();
|
|
125
212
|
renderAll();
|
|
126
213
|
});
|
|
127
214
|
|
|
@@ -131,6 +218,7 @@ function mount() {
|
|
|
131
218
|
|
|
132
219
|
/* ── Render ─────────────────────────────────────────────────────────────────── */
|
|
133
220
|
function renderAll() {
|
|
221
|
+
expireStaleActiveTools();
|
|
134
222
|
renderKPIs();
|
|
135
223
|
renderMcpStrip();
|
|
136
224
|
renderFeed();
|
|
@@ -162,10 +250,12 @@ function renderMcpStrip() {
|
|
|
162
250
|
</div>`;
|
|
163
251
|
});
|
|
164
252
|
|
|
165
|
-
const resolved = _resolvedMcp.slice(0, MAX_RESOLVED_MCP).map(({ tool, durationMs, error }) => {
|
|
166
|
-
const cls = error ? 'rt-mcp-pill rt-mcp-err' : 'rt-mcp-pill rt-mcp-done';
|
|
253
|
+
const resolved = _resolvedMcp.slice(0, MAX_RESOLVED_MCP).map(({ tool, durationMs, error, stale }) => {
|
|
254
|
+
const cls = error ? 'rt-mcp-pill rt-mcp-err' : stale ? 'rt-mcp-pill rt-mcp-long' : 'rt-mcp-pill rt-mcp-done';
|
|
167
255
|
const badge = error
|
|
168
256
|
? `<span class="rt-mcp-badge rt-mcp-badge-err">err</span>`
|
|
257
|
+
: stale
|
|
258
|
+
? `<span class="rt-mcp-badge rt-mcp-badge-ok">stale</span>`
|
|
169
259
|
: `<span class="rt-mcp-badge rt-mcp-badge-ok">${humanMs(durationMs)}</span>`;
|
|
170
260
|
return `<div class="${cls}">${esc(tool)}${badge}</div>`;
|
|
171
261
|
});
|
|
@@ -182,6 +272,7 @@ function renderMcpStrip() {
|
|
|
182
272
|
function ensurePulseTimer() {
|
|
183
273
|
if (_pulseTimer || _activeTools.size === 0) return;
|
|
184
274
|
_pulseTimer = setInterval(() => {
|
|
275
|
+
expireStaleActiveTools();
|
|
185
276
|
if (_activeTools.size === 0) {
|
|
186
277
|
clearInterval(_pulseTimer);
|
|
187
278
|
_pulseTimer = null;
|
|
@@ -271,7 +362,8 @@ function showUpgradeNudge(msg, ctaUrl) {
|
|
|
271
362
|
export function ingestEvent(evt) {
|
|
272
363
|
const { type = '', payload = {} } = evt;
|
|
273
364
|
const category = categoryFor(type);
|
|
274
|
-
const tool =
|
|
365
|
+
const tool = toolNameFromPayload(payload);
|
|
366
|
+
const eventTime = eventMillis(evt, payload);
|
|
275
367
|
const tokensSaved = Number(payload.tokensSaved ?? evt.tokensSaved ?? 0);
|
|
276
368
|
const durationMs = Number(payload.durationMs ?? 0);
|
|
277
369
|
const phase = String(payload.phase ?? payload.stage ?? '');
|
|
@@ -280,18 +372,22 @@ export function ingestEvent(evt) {
|
|
|
280
372
|
const callId = String(payload.callId ?? '');
|
|
281
373
|
const trackKey = callId || tool;
|
|
282
374
|
if (type === 'mcp.call.start' && trackKey) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
375
|
+
if (!wasSettledAfter(tool, eventTime)) {
|
|
376
|
+
_activeTools.set(trackKey, { tool, startedAt: Date.now(), callId });
|
|
377
|
+
_toolCalls++;
|
|
378
|
+
ensurePulseTimer();
|
|
379
|
+
}
|
|
380
|
+
} else if (
|
|
381
|
+
type === 'mcp.call.complete' ||
|
|
382
|
+
type === 'mcp.handler.complete' ||
|
|
383
|
+
type === 'mcp.handler.failed' ||
|
|
384
|
+
(type === 'tool.invocation' && trackKey)
|
|
385
|
+
) {
|
|
386
|
+
if (!settleActiveTool(trackKey, tool, payload, { durationMs, eventTime })) {
|
|
387
|
+
rememberSettledTool(tool || trackKey, eventTime);
|
|
294
388
|
}
|
|
389
|
+
} else if (type === 'nexus.shutdown' || type === 'orchestrator.disposed') {
|
|
390
|
+
clearActiveToolsAsStale();
|
|
295
391
|
}
|
|
296
392
|
|
|
297
393
|
// Toast for license events
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1126,9 +1126,26 @@ export class DashboardServer {
|
|
|
1126
1126
|
const capabilities = this.buildAdvertisedCapabilities();
|
|
1127
1127
|
const compatibility = this.buildCompatibilityStatus(capabilities);
|
|
1128
1128
|
const runtimeEnvelope = this.buildProbeRuntimeEnvelope();
|
|
1129
|
+
const workspace = this.resolveCanonicalWorkspaceContext();
|
|
1129
1130
|
return {
|
|
1130
1131
|
dashboardApiVersion: DASHBOARD_API_VERSION,
|
|
1131
|
-
projectName:
|
|
1132
|
+
projectName: workspace.repoName,
|
|
1133
|
+
repoIdentity: {
|
|
1134
|
+
repoName: workspace.repoName,
|
|
1135
|
+
repoRoot: workspace.repoRoot,
|
|
1136
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
1137
|
+
workspaceSource: workspace.workspaceSource,
|
|
1138
|
+
workspaceStateKey: workspace.stateKey,
|
|
1139
|
+
remoteUrl: workspace.remoteUrl ?? null,
|
|
1140
|
+
currentRepoId: null,
|
|
1141
|
+
},
|
|
1142
|
+
scanContext: {
|
|
1143
|
+
repoRoot: workspace.repoRoot,
|
|
1144
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
1145
|
+
workspaceSource: workspace.workspaceSource,
|
|
1146
|
+
workspaceStateKey: workspace.stateKey,
|
|
1147
|
+
notes: ['Probe snapshot uses the lightweight compatibility contract.'],
|
|
1148
|
+
},
|
|
1132
1149
|
capabilities,
|
|
1133
1150
|
compatibility,
|
|
1134
1151
|
dashboardUrl: this.getAddress(),
|
|
@@ -1513,6 +1530,10 @@ export class DashboardServer {
|
|
|
1513
1530
|
if (payload.compatibility?.status !== 'compatible') {
|
|
1514
1531
|
return false;
|
|
1515
1532
|
}
|
|
1533
|
+
const stateRoot = payload.runtimeEnvelope?.workspace?.stateRoot;
|
|
1534
|
+
if (!stateRoot || path.resolve(stateRoot) !== path.resolve(resolveNexusStateDir())) {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1516
1537
|
const required = payload.compatibility?.requiredCapabilities?.length
|
|
1517
1538
|
? payload.compatibility.requiredCapabilities
|
|
1518
1539
|
: [...DASHBOARD_COMPATIBILITY_CAPABILITIES];
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Persistence: SQLite (same pattern as memory.db / graph.db)
|
|
13
13
|
*/
|
|
14
|
+
export declare const NGRAM_WARMUP_MAX_DB_BYTES: number;
|
|
15
|
+
export declare const NGRAM_DEFAULT_ROTATE_BYTES: number;
|
|
16
|
+
export declare const NGRAM_DEFAULT_FOOTPRINT_BYTES: number;
|
|
14
17
|
export declare function getNgramWalPath(dbPath: string): string;
|
|
15
18
|
export declare function getNgramShmPath(dbPath: string): string;
|
|
16
19
|
export declare function getNgramFootprintBytes(dbPath: string): number;
|
|
@@ -147,7 +150,7 @@ export declare class NgramIndex {
|
|
|
147
150
|
optimizeStorage(force?: boolean): void;
|
|
148
151
|
/**
|
|
149
152
|
* Operator-focused maintenance for the on-disk ngram DB.
|
|
150
|
-
* - Bounds runaway DB growth via rotation
|
|
153
|
+
* - Bounds runaway DB growth via rotation, counting the
|
|
151
154
|
* full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
|
|
152
155
|
* - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
|
|
153
156
|
*/
|
|
@@ -45,7 +45,9 @@ function safeStatSize(filePath) {
|
|
|
45
45
|
// wal/shm misses the bug that turned ngram-index.db-wal into 84GB on disk.
|
|
46
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
47
|
const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
48
|
-
const
|
|
48
|
+
export const NGRAM_WARMUP_MAX_DB_BYTES = 200 * 1024 * 1024; // 200 MB
|
|
49
|
+
export const NGRAM_DEFAULT_ROTATE_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
|
|
50
|
+
export const NGRAM_DEFAULT_FOOTPRINT_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
|
|
49
51
|
const NGRAM_DEFAULT_CHECKPOINT_INTERVAL_MS = 30_000;
|
|
50
52
|
const NGRAM_DEFAULT_CHECKPOINT_DOC_COUNT = 200;
|
|
51
53
|
export function getNgramWalPath(dbPath) {
|
|
@@ -199,13 +201,13 @@ export class NgramIndex {
|
|
|
199
201
|
this.warmHashSet();
|
|
200
202
|
try {
|
|
201
203
|
const sizeBytes = safeStatSize(this.dbPath);
|
|
202
|
-
// Only VACUUM on medium-size DBs. Large DBs
|
|
204
|
+
// Only VACUUM on medium-size DBs. Large DBs take too long
|
|
203
205
|
// synchronously — VACUUM rewrites the entire file. Skip here; let
|
|
204
206
|
// periodic maintenance handle it.
|
|
205
|
-
if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <=
|
|
207
|
+
if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <= NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
206
208
|
this.optimizeStorage(true);
|
|
207
209
|
}
|
|
208
|
-
else if (sizeBytes >
|
|
210
|
+
else if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
209
211
|
logNgramNoticeOnce(`ngram:vacuum-skip:${this.dbPath}`, `[NgramIndex] skipping VACUUM on large DB (${Math.round(sizeBytes / 1024 / 1024)}MB) db=${this.dbPath}`);
|
|
210
212
|
}
|
|
211
213
|
}
|
|
@@ -217,17 +219,17 @@ export class NgramIndex {
|
|
|
217
219
|
// Count the full SQLite footprint (db + wal + shm). The 84GB regression
|
|
218
220
|
// happened because a 32MB db file had a 84GB -wal sibling that this
|
|
219
221
|
// routine never inspected.
|
|
220
|
-
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES',
|
|
222
|
+
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
|
|
221
223
|
const dbBytes = safeStatSize(this.dbPath);
|
|
222
224
|
const footprint = getNgramFootprintBytes(this.dbPath);
|
|
223
225
|
if (footprint <= 0 || footprint < rotateBytes)
|
|
224
226
|
return;
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
227
|
+
// The n-gram DB is a rebuildable cache, so oversized rotation drops it by
|
|
228
|
+
// default. Operators can opt into one retained archive for forensics with
|
|
229
|
+
// NEXUS_NGRAM_ARCHIVE_OVERSIZE=1.
|
|
228
230
|
const dir = path.dirname(this.dbPath);
|
|
229
231
|
const base = path.basename(this.dbPath);
|
|
230
|
-
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE
|
|
232
|
+
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
|
|
231
233
|
const existing = fs.existsSync(dir)
|
|
232
234
|
? fs.readdirSync(dir).filter((entry) => entry.startsWith(`${base}.oversize.`)).sort().reverse()
|
|
233
235
|
: [];
|
|
@@ -324,8 +326,8 @@ export class NgramIndex {
|
|
|
324
326
|
// Size guard: SELECT DISTINCT on very large DBs blocks startup under swap pressure.
|
|
325
327
|
try {
|
|
326
328
|
const sizeBytes = fs.statSync(this.dbPath).size;
|
|
327
|
-
if (sizeBytes >
|
|
328
|
-
logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB >
|
|
329
|
+
if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
330
|
+
logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB > ${Math.round(NGRAM_WARMUP_MAX_DB_BYTES / 1024 / 1024)}MB) db=${this.dbPath}`);
|
|
329
331
|
return;
|
|
330
332
|
}
|
|
331
333
|
}
|
|
@@ -667,17 +669,17 @@ export class NgramIndex {
|
|
|
667
669
|
}
|
|
668
670
|
/**
|
|
669
671
|
* Operator-focused maintenance for the on-disk ngram DB.
|
|
670
|
-
* - Bounds runaway DB growth via rotation
|
|
672
|
+
* - Bounds runaway DB growth via rotation, counting the
|
|
671
673
|
* full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
|
|
672
674
|
* - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
|
|
673
675
|
*/
|
|
674
676
|
maintainBounded(options = {}) {
|
|
675
677
|
const dbBytes = safeStatSize(this.dbPath);
|
|
676
678
|
const footprint = this.getSqliteFootprintBytes();
|
|
677
|
-
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES',
|
|
679
|
+
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
|
|
678
680
|
const vacuumMaxBytes = readEnvBytes('NEXUS_NGRAM_VACUUM_MAX_BYTES', 256 * 1024 * 1024);
|
|
679
681
|
if (footprint >= rotateBytes && footprint > 0) {
|
|
680
|
-
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE
|
|
682
|
+
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
|
|
681
683
|
const rotatedPath = `${this.dbPath}.oversize.${Date.now()}`;
|
|
682
684
|
const removeSibling = (suffix) => {
|
|
683
685
|
try {
|
|
@@ -169,7 +169,15 @@ export class WorkflowRuntime {
|
|
|
169
169
|
workflow.steps.forEach((step) => {
|
|
170
170
|
if (step.command)
|
|
171
171
|
verify.add(step.command);
|
|
172
|
-
|
|
172
|
+
const verifierStep = step.role === 'verifier' || step.checkpoint === 'before-verify';
|
|
173
|
+
if (verifierStep) {
|
|
174
|
+
step.bindings
|
|
175
|
+
.filter((binding) => binding.type === 'run_command' && binding.command)
|
|
176
|
+
.forEach((binding) => verify.add(binding.command || ''));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
actions.push(...step.bindings);
|
|
180
|
+
}
|
|
173
181
|
});
|
|
174
182
|
}
|
|
175
183
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -213,6 +213,7 @@ export declare class NexusPrime {
|
|
|
213
213
|
awaitReady(): Promise<void>;
|
|
214
214
|
getClientRegistry(): ClientRegistry;
|
|
215
215
|
getRuntimeHotAt(): number | null;
|
|
216
|
+
getDashboardAddress(): string | null;
|
|
216
217
|
getSynapse(): SynapseRuntime | null;
|
|
217
218
|
getArchitects(): ArchitectsRuntime | null;
|
|
218
219
|
/**
|
package/dist/index.js
CHANGED
|
@@ -847,6 +847,9 @@ export class NexusPrime {
|
|
|
847
847
|
getRuntimeHotAt() {
|
|
848
848
|
return this.runtimeHotAt;
|
|
849
849
|
}
|
|
850
|
+
getDashboardAddress() {
|
|
851
|
+
return this.dashboardServer.getAddress();
|
|
852
|
+
}
|
|
850
853
|
getSynapse() {
|
|
851
854
|
return this.synapse;
|
|
852
855
|
}
|
|
@@ -1070,7 +1073,7 @@ export class NexusPrime {
|
|
|
1070
1073
|
const bootstrapManifest = ensureBootstrap({
|
|
1071
1074
|
packageRoot: PACKAGE_ROOT,
|
|
1072
1075
|
workspaceRoot: this.getWorkspaceContext().workspaceRoot,
|
|
1073
|
-
phase: '
|
|
1076
|
+
phase: 'install',
|
|
1074
1077
|
silent: true,
|
|
1075
1078
|
});
|
|
1076
1079
|
this.runtime.recordBootstrapManifestStatus?.(bootstrapManifest);
|
|
@@ -26,7 +26,11 @@ function mapOperative(row) {
|
|
|
26
26
|
}
|
|
27
27
|
export function insertOperative(db, input) {
|
|
28
28
|
const id = input.id ?? randomUUID();
|
|
29
|
-
const
|
|
29
|
+
const baseName = (input.name?.trim() || `operative-${id.slice(0, 8)}`).slice(0, 80);
|
|
30
|
+
let name = baseName;
|
|
31
|
+
for (let attempt = 2; getOperativeByName(db, name); attempt++) {
|
|
32
|
+
name = `${baseName}-${attempt}`;
|
|
33
|
+
}
|
|
30
34
|
const budgetScope = input.budgetScope ?? (input.strikeTeamId ? 'crew' : 'operative');
|
|
31
35
|
const budgetScopeId = input.budgetScopeId ?? input.strikeTeamId ?? id;
|
|
32
36
|
db.prepare(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.21",
|
|
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",
|