nexus-prime 7.9.20 → 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.
@@ -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 = this.getDecoratedToolDefinitions(hctx.getToolProfile());
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.getRuntime().getUsageSnapshot()?.orchestration?.lastPrompt || '';
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
- this.getRuntime().recordClientInstructionStatus({
719
- clientId: this.name,
720
- clientFamily: this.name === 'openclaw' ? 'antigravity' : this.name,
721
- toolProfile: profile,
722
- status: profile === 'autonomous' ? 'guided' : 'manual',
723
- summary: this.describeClientInstructionStatus(profile),
724
- updatedAt: Date.now(),
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,
package/dist/cli.js CHANGED
@@ -977,11 +977,16 @@ program
977
977
  });
978
978
  }
979
979
  console.error('Starting Nexus Prime MCP Server (standalone)...');
980
- await runStartupHygiene({
981
- repoRoot: workspaceContext.repoRoot,
982
- workspaceStateRoot: workspaceContext.stateRoot,
983
- mode: 'stale',
984
- });
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);
985
990
  nexus = createNexusPrime({
986
991
  adapters: ['mcp'],
987
992
  runtime: {
@@ -996,8 +1001,8 @@ program
996
1001
  if (!adapterReady) {
997
1002
  throw new Error('MCP adapter did not become ready before the startup deadline.');
998
1003
  }
999
- await startup;
1000
1004
  flushPrimedMcpStdioInput();
1005
+ await startup;
1001
1006
  console.error('Nexus Prime MCP Server running on stdio (standalone)');
1002
1007
  console.error('Memory persistence: active (~/.nexus-prime/memory.db)');
1003
1008
  const shutdown = async (signal) => {
@@ -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 ?? h.prefrontal ?? 0,
263
- hippocampus: h.episodic ?? h.hippocampus ?? 0,
264
- cortex: h.semantic ?? h.cortex ?? 0,
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(cmp.state)};padding:8px 12px">
819
- <div style="font-size:13px;font-weight:600;margin-bottom:4px">Completion · run ${esc((cmp.runId || '').slice(-8))} · <span style="color:${stateColor(cmp.state)}">${esc(cmp.state || '')}</span></div>
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 = String(payload.toolName ?? payload.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
- _activeTools.set(trackKey, { tool, startedAt: Date.now(), callId });
284
- _toolCalls++;
285
- ensurePulseTimer();
286
- } else if (type === 'mcp.call.complete' && trackKey) {
287
- const entry = _activeTools.get(trackKey) ?? _activeTools.get(tool);
288
- if (entry) {
289
- const elapsed = Date.now() - entry.startedAt;
290
- _resolvedMcp.unshift({ tool: entry.tool, durationMs: elapsed, error: payload.error ?? null });
291
- if (_resolvedMcp.length > MAX_RESOLVED_MCP) _resolvedMcp.length = MAX_RESOLVED_MCP;
292
- _activeTools.delete(trackKey);
293
- _activeTools.delete(tool);
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
@@ -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
- actions.push(...step.bindings);
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 {
@@ -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 name = input.name ?? `operative-${id.slice(0, 8)}`;
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.20",
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",