nexus-prime 7.9.20 → 7.9.22

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.
@@ -11,6 +11,35 @@ import { requireRuntime } from '../util/require-runtime.js';
11
11
  import { nxlResult } from '../../../../nxl/index.js';
12
12
  import { consolidateMemory } from '../../../../engines/memory-consolidator.js';
13
13
  import { recordFirstMemory } from '../../../../engines/telemetry.js';
14
+ function memoryScopeForTags(tags) {
15
+ return tags.some((tag) => tag.startsWith('#project')) ? 'project' : 'session';
16
+ }
17
+ function memoryStatusSummary(hctx) {
18
+ try {
19
+ const stats = hctx.nexusRef.getMemoryStats?.();
20
+ const health = typeof hctx.nexusRef.getMemoryHealth === 'function'
21
+ ? hctx.nexusRef.getMemoryHealth()
22
+ : null;
23
+ const healthText = health
24
+ ? `${health.active} active, ${health.quarantined} quarantined, ${health.shared} shared`
25
+ : null;
26
+ const topTags = Array.isArray(stats?.topTags) && stats.topTags.length
27
+ ? stats.topTags.slice(0, 4).join(', ')
28
+ : 'none yet';
29
+ const tierText = stats
30
+ ? `${stats.prefrontal} working, ${stats.hippocampus} episodic, ${stats.cortex} semantic`
31
+ : null;
32
+ return [
33
+ 'Memory status:',
34
+ tierText,
35
+ healthText,
36
+ `top tags ${topTags}`,
37
+ ].filter(Boolean).join(' ');
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
14
43
  export async function handleMemoryGroup(toolName, hctx, request, args, ctx) {
15
44
  const runtimeError = requireRuntime(hctx);
16
45
  if (runtimeError)
@@ -28,8 +57,10 @@ export async function handleMemoryGroup(toolName, hctx, request, args, ctx) {
28
57
  void recordFirstMemory().catch(() => { });
29
58
  hctx.sessionDNA.recordMemoryStore();
30
59
  const nudge = hctx.telemetry.planningNudge('store', { priority });
60
+ const scope = memoryScopeForTags(tags);
61
+ const status = memoryStatusSummary(hctx);
31
62
  if (ctx) {
32
- ctx.meta.memoryScope = tags.some((tag) => tag.startsWith('#project')) ? 'project' : 'session';
63
+ ctx.meta.memoryScope = scope;
33
64
  }
34
65
  // Console ASCII UI
35
66
  hctx.box('🧠 CORTEX MEMORY STORED', [
@@ -41,6 +72,8 @@ export async function handleMemoryGroup(toolName, hctx, request, args, ctx) {
41
72
  type: 'text',
42
73
  text: [
43
74
  'Memory stored. ID: ' + id,
75
+ `Memory link: ${scope} scope, priority ${priority.toFixed(2)}, ${tags.length} tag(s).`,
76
+ status,
44
77
  nudge,
45
78
  ].filter(Boolean).join('\n\n'),
46
79
  }],
@@ -52,13 +85,15 @@ export async function handleMemoryGroup(toolName, hctx, request, args, ctx) {
52
85
  const fmt = String(request.params.arguments?.format ?? 'text');
53
86
  const crossClient = Boolean(request.params.arguments?.cross_client ?? false);
54
87
  const memories = await hctx.nexusRef.recallMemory(query, k, crossClient ? undefined : hctx.getToolProfile());
55
- nexusEventBus.emit('memory.recall', { query, count: memories.length });
88
+ const preview = memories.slice(0, 3).map((m) => String(m ?? '').replace(/\s+/g, ' ').slice(0, 96));
89
+ nexusEventBus.emit('memory.recall', { query, count: memories.length, k, crossClient, format: fmt, preview });
56
90
  hctx.telemetry.recordRecall(memories.length);
57
91
  hctx.sessionDNA.recordMemoryRecall();
58
92
  if (ctx) {
59
93
  ctx.meta.memoryRecallCount = memories.length;
60
94
  }
61
95
  const nudge = hctx.telemetry.planningNudge('recall', { count: memories.length });
96
+ const status = memoryStatusSummary(hctx);
62
97
  // Console ASCII UI
63
98
  hctx.box('🔍 CORTEX MEMORY RECALL', [
64
99
  `Query: ${query.replace(/\n/g, ' ').substring(0, 57).padEnd(59, ' ')}`,
@@ -80,9 +115,13 @@ export async function handleMemoryGroup(toolName, hctx, request, args, ctx) {
80
115
  return {
81
116
  content: [{
82
117
  type: 'text',
83
- text: (memories.length > 0
84
- ? `🧠 ${memories.length} memories recalled for "${query}":\n\n${formatted}`
85
- : `No memories found for "${query}". Fresh session or new topic.`) + '\n\n' + nudge,
118
+ text: [
119
+ memories.length > 0
120
+ ? `Memory recall connected ${memories.length} item(s) for "${query}".\n\n${formatted}`
121
+ : `No memories found for "${query}". Fresh session or new topic.`,
122
+ status,
123
+ nudge,
124
+ ].filter(Boolean).join('\n\n'),
86
125
  }],
87
126
  };
88
127
  }
@@ -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) => {
@@ -20,6 +20,25 @@
20
20
  border-radius: 8px;
21
21
  padding: 14px 18px;
22
22
  min-width: 0;
23
+ text-align: left;
24
+ }
25
+
26
+ .rt-kpi-action {
27
+ cursor: pointer;
28
+ color: inherit;
29
+ font: inherit;
30
+ transition: border-color 0.15s, background 0.15s, transform 0.15s;
31
+ }
32
+
33
+ .rt-kpi-action:hover,
34
+ .rt-kpi-action[aria-expanded="true"] {
35
+ border-color: var(--accent);
36
+ background: oklch(87% 0.19 152 / 7%);
37
+ }
38
+
39
+ .rt-kpi-action:focus-visible {
40
+ outline: 2px solid var(--accent);
41
+ outline-offset: 2px;
23
42
  }
24
43
 
25
44
  .rt-kpi-val {
@@ -234,6 +253,135 @@
234
253
  min-height: 0;
235
254
  }
236
255
 
256
+ /* ── Token telemetry flyout ─────────────────────────────────────────────────── */
257
+ .rt-token-flyout-slot {
258
+ position: relative;
259
+ }
260
+
261
+ .rt-token-flyout {
262
+ border: 1px solid var(--border);
263
+ border-radius: 8px;
264
+ background: var(--surface);
265
+ padding: 14px;
266
+ box-shadow: 0 18px 45px oklch(0% 0 0 / 35%);
267
+ animation: rt-ev-in 0.12s ease;
268
+ }
269
+
270
+ .rt-token-head {
271
+ display: flex;
272
+ align-items: flex-start;
273
+ justify-content: space-between;
274
+ gap: 12px;
275
+ margin-bottom: 12px;
276
+ }
277
+
278
+ .rt-token-title {
279
+ font: 700 14px ui-sans-serif, system-ui, sans-serif;
280
+ color: var(--text);
281
+ }
282
+
283
+ .rt-token-sub {
284
+ margin-top: 3px;
285
+ font-size: 12px;
286
+ color: var(--text-muted);
287
+ }
288
+
289
+ .rt-token-error { color: var(--bad, #ef4444); }
290
+
291
+ .rt-token-close {
292
+ border: 1px solid var(--border);
293
+ border-radius: 6px;
294
+ background: transparent;
295
+ color: var(--text-muted);
296
+ padding: 4px 9px;
297
+ cursor: pointer;
298
+ font-size: 12px;
299
+ }
300
+
301
+ .rt-token-close:hover {
302
+ border-color: var(--accent);
303
+ color: var(--accent);
304
+ }
305
+
306
+ .rt-token-grid {
307
+ display: grid;
308
+ grid-template-columns: repeat(4, minmax(0, 1fr));
309
+ gap: 8px;
310
+ margin-bottom: 12px;
311
+ }
312
+
313
+ .rt-token-stat {
314
+ border: 1px solid var(--border);
315
+ border-radius: 7px;
316
+ padding: 10px;
317
+ background: var(--bg-panel, transparent);
318
+ }
319
+
320
+ .rt-token-stat span,
321
+ .rt-token-row span {
322
+ color: var(--text-muted);
323
+ font-size: 11px;
324
+ }
325
+
326
+ .rt-token-stat strong {
327
+ display: block;
328
+ margin-top: 5px;
329
+ font: 700 20px/1 var(--font-mono, ui-monospace, monospace);
330
+ color: var(--accent);
331
+ }
332
+
333
+ .rt-token-columns {
334
+ display: grid;
335
+ grid-template-columns: 1fr 1fr;
336
+ gap: 12px;
337
+ }
338
+
339
+ .rt-token-section-title {
340
+ color: var(--text-muted);
341
+ font: 700 11px var(--font-mono, ui-monospace, monospace);
342
+ letter-spacing: 0.06em;
343
+ text-transform: uppercase;
344
+ margin-bottom: 6px;
345
+ }
346
+
347
+ .rt-token-row {
348
+ display: grid;
349
+ grid-template-columns: minmax(0, 1fr) auto auto;
350
+ gap: 8px;
351
+ align-items: center;
352
+ border-top: 1px solid var(--border);
353
+ padding: 6px 0;
354
+ min-width: 0;
355
+ }
356
+
357
+ .rt-token-row strong {
358
+ color: var(--text);
359
+ font: 700 12px var(--font-mono, ui-monospace, monospace);
360
+ }
361
+
362
+ .rt-token-row span:first-child {
363
+ overflow: hidden;
364
+ text-overflow: ellipsis;
365
+ white-space: nowrap;
366
+ }
367
+
368
+ .rt-token-muted {
369
+ color: var(--text-muted);
370
+ font-size: 12px;
371
+ padding: 8px 0;
372
+ }
373
+
374
+ @media (max-width: 900px) {
375
+ .runtime-kpis,
376
+ .rt-token-grid,
377
+ .rt-token-columns {
378
+ grid-template-columns: 1fr;
379
+ }
380
+ .runtime-kpis {
381
+ display: grid;
382
+ }
383
+ }
384
+
237
385
  .rt-mcp-strip-inner {
238
386
  display: flex;
239
387
  flex-wrap: wrap;
@@ -101,6 +101,34 @@
101
101
  }
102
102
  .dtags { display: flex; flex-wrap: wrap; gap: 4px; }
103
103
 
104
+ .dispatch-strip {
105
+ margin-top: 10px;
106
+ padding: 10px;
107
+ border: 1px solid var(--border);
108
+ border-radius: var(--radius);
109
+ background: var(--bg-panel);
110
+ }
111
+ .ds-stages { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 7px; }
112
+ .ds-stage {
113
+ font: 0.68rem var(--font-mono);
114
+ padding: 2px 6px;
115
+ border: 1px solid var(--border);
116
+ border-radius: 999px;
117
+ color: var(--text-dim);
118
+ }
119
+ .ds-stage.ds-done { color: var(--accent); border-color: rgba(0,255,136,0.28); }
120
+ .ds-stage.ds-active { color: var(--secondary); border-color: rgba(0,212,255,0.35); }
121
+ .ds-sep { color: var(--text-dim); font-size: 0.68rem; }
122
+ .ds-stdout, .ds-summary {
123
+ font-size: 0.74rem;
124
+ color: var(--text-muted);
125
+ line-height: 1.45;
126
+ margin-top: 5px;
127
+ }
128
+ .ds-meta { display: flex; flex-wrap: wrap; gap: 8px; color: var(--text-dim); font: 0.68rem var(--font-mono); }
129
+ .ds-error { margin-top: 6px; color: var(--warning); font-size: 0.74rem; }
130
+ .ds-stop-btn { margin-top: 8px; }
131
+
104
132
  /* ── Unified workforce kanban ── */
105
133
  .workforce-kanban { padding: 0; background: transparent; border: none !important; }
106
134
  .kanban-meta { font-size: 0.75rem; color: var(--text-dim); margin-bottom: 8px; padding: 0 4px; }
@@ -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,10 +24,16 @@ 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
29
32
  let _toastTimer = null;
33
+ let _tokenFlyoutOpen = false;
34
+ let _tokenFlyoutLoading = false;
35
+ let _tokenFlyoutError = '';
36
+ let _tokenTelemetry = null;
30
37
 
31
38
  /* ── Category metadata ──────────────────────────────────────────────────────── */
32
39
  const CATEGORY_META = {
@@ -62,6 +69,110 @@ function humanMs(ms) {
62
69
  return `${(ms / 1000).toFixed(1)}s`;
63
70
  }
64
71
 
72
+ function fmtTokens(n) {
73
+ const v = Number(n ?? 0);
74
+ if (!Number.isFinite(v) || v <= 0) return '0';
75
+ if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
76
+ if (v >= 10_000) return `${Math.round(v / 1000)}k`;
77
+ if (v >= 1000) return `${(v / 1000).toFixed(1)}k`;
78
+ return Math.round(v).toLocaleString();
79
+ }
80
+
81
+ function fmtPct(n) {
82
+ const v = Number(n ?? 0);
83
+ if (!Number.isFinite(v)) return '0%';
84
+ return `${Math.round(v)}%`;
85
+ }
86
+
87
+ function fmtTime(ts) {
88
+ const n = Number(ts ?? 0);
89
+ if (!Number.isFinite(n) || n <= 0) return 'recent';
90
+ return new Date(n).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
91
+ }
92
+
93
+ function toolNameFromPayload(payload) {
94
+ return String(payload.toolName ?? payload.tool ?? payload.name ?? '').trim();
95
+ }
96
+
97
+ function eventMillis(evt, payload) {
98
+ const raw = evt?.time ?? payload?.time ?? payload?.ts ?? null;
99
+ const numeric = Number(raw);
100
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
101
+ const parsed = Date.parse(String(raw ?? ''));
102
+ return Number.isFinite(parsed) ? parsed : Date.now();
103
+ }
104
+
105
+ function capResolvedMcp() {
106
+ if (_resolvedMcp.length > MAX_RESOLVED_MCP) _resolvedMcp.length = MAX_RESOLVED_MCP;
107
+ }
108
+
109
+ function rememberSettledTool(tool, eventTime) {
110
+ if (!tool) return;
111
+ const previous = _settledToolTimes.get(tool) ?? 0;
112
+ if (eventTime >= previous) _settledToolTimes.set(tool, eventTime);
113
+ if (_settledToolTimes.size > MAX_RESOLVED_MCP * 2) {
114
+ const oldest = [..._settledToolTimes.entries()].sort((a, b) => a[1] - b[1])[0]?.[0];
115
+ if (oldest) _settledToolTimes.delete(oldest);
116
+ }
117
+ }
118
+
119
+ function wasSettledAfter(tool, eventTime) {
120
+ const settledAt = tool ? _settledToolTimes.get(tool) : null;
121
+ return Number.isFinite(settledAt) && Number.isFinite(eventTime) && eventTime <= settledAt;
122
+ }
123
+
124
+ function activeToolKey(trackKey, tool) {
125
+ if (trackKey && _activeTools.has(trackKey)) return trackKey;
126
+ if (tool && _activeTools.has(tool)) return tool;
127
+ if (tool) {
128
+ for (const [key, entry] of _activeTools.entries()) {
129
+ if (entry.tool === tool) return key;
130
+ }
131
+ }
132
+ if (!trackKey && !tool && _activeTools.size === 1) {
133
+ return _activeTools.keys().next().value;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function settleActiveTool(trackKey, tool, payload = {}, opts = {}) {
139
+ const key = activeToolKey(trackKey, tool);
140
+ if (!key) return false;
141
+ const entry = _activeTools.get(key);
142
+ if (!entry) return false;
143
+ const elapsed = Number(opts.durationMs ?? payload.durationMs ?? 0) || (Date.now() - entry.startedAt);
144
+ const status = String(payload.status ?? '').toLowerCase();
145
+ _resolvedMcp.unshift({
146
+ tool: entry.tool || tool || key,
147
+ durationMs: elapsed,
148
+ error: payload.error ?? (status && status !== 'ok' ? status : null),
149
+ stale: opts.stale === true,
150
+ });
151
+ rememberSettledTool(entry.tool || tool || key, Number(opts.eventTime ?? Date.now()));
152
+ capResolvedMcp();
153
+ _activeTools.delete(key);
154
+ if (entry.callId) _activeTools.delete(entry.callId);
155
+ if (entry.tool) _activeTools.delete(entry.tool);
156
+ if (trackKey) _activeTools.delete(trackKey);
157
+ if (tool) _activeTools.delete(tool);
158
+ return true;
159
+ }
160
+
161
+ function expireStaleActiveTools() {
162
+ const now = Date.now();
163
+ for (const [key, entry] of [..._activeTools.entries()]) {
164
+ if (now - entry.startedAt > STALE_CALL_MS) {
165
+ settleActiveTool(key, entry.tool, {}, { stale: true, durationMs: now - entry.startedAt });
166
+ }
167
+ }
168
+ }
169
+
170
+ function clearActiveToolsAsStale() {
171
+ for (const [key, entry] of [..._activeTools.entries()]) {
172
+ settleActiveTool(key, entry.tool, {}, { stale: true, durationMs: Date.now() - entry.startedAt });
173
+ }
174
+ }
175
+
65
176
  /* ── Mount ──────────────────────────────────────────────────────────────────── */
66
177
  function mount() {
67
178
  const el = $('runtime-view');
@@ -73,10 +184,10 @@ function mount() {
73
184
  <div class="rt-kpi-val" id="rt-toolcalls">0</div>
74
185
  <div class="rt-kpi-lbl">Tool Calls</div>
75
186
  </div>
76
- <div class="rt-kpi" id="rt-kpi-saved">
187
+ <button class="rt-kpi rt-kpi-action" id="rt-kpi-saved" type="button" aria-expanded="false" aria-controls="rt-token-flyout" title="Open token telemetry">
77
188
  <div class="rt-kpi-val" id="rt-tokens-saved">0</div>
78
189
  <div class="rt-kpi-lbl">Tokens Saved</div>
79
- </div>
190
+ </button>
80
191
  <div class="rt-kpi">
81
192
  <div class="rt-kpi-val" id="rt-active-count">0</div>
82
193
  <div class="rt-kpi-lbl">Active Now</div>
@@ -84,6 +195,7 @@ function mount() {
84
195
  </div>
85
196
 
86
197
  <div class="rt-mcp-strip" id="rt-mcp-strip"></div>
198
+ <div class="rt-token-flyout-slot" id="rt-token-flyout-slot"></div>
87
199
 
88
200
  <div class="rt-filter-bar">
89
201
  <button class="rt-filter-btn active" data-filter="all">All</button>
@@ -122,16 +234,27 @@ function mount() {
122
234
  _totalTokensSaved = 0;
123
235
  _toolCalls = 0;
124
236
  _activeTools.clear();
237
+ _settledToolTimes.clear();
125
238
  renderAll();
126
239
  });
127
240
 
241
+ $('rt-kpi-saved')?.addEventListener('click', () => {
242
+ _tokenFlyoutOpen = !_tokenFlyoutOpen;
243
+ renderTokenFlyout();
244
+ if (_tokenFlyoutOpen && !_tokenTelemetry && !_tokenFlyoutLoading) {
245
+ void loadTokenTelemetry();
246
+ }
247
+ });
248
+
128
249
  _mounted = true;
129
250
  renderAll();
130
251
  }
131
252
 
132
253
  /* ── Render ─────────────────────────────────────────────────────────────────── */
133
254
  function renderAll() {
255
+ expireStaleActiveTools();
134
256
  renderKPIs();
257
+ renderTokenFlyout();
135
258
  renderMcpStrip();
136
259
  renderFeed();
137
260
  }
@@ -145,6 +268,96 @@ function renderKPIs() {
145
268
  ? (_totalTokensSaved >= 1000 ? `${(_totalTokensSaved / 1000).toFixed(1)}k` : String(_totalTokensSaved))
146
269
  : '0';
147
270
  if (activeEl) activeEl.textContent = String(_activeTools.size);
271
+ const tokenBtn = $('rt-kpi-saved');
272
+ if (tokenBtn) tokenBtn.setAttribute('aria-expanded', _tokenFlyoutOpen ? 'true' : 'false');
273
+ }
274
+
275
+ async function loadTokenTelemetry() {
276
+ _tokenFlyoutLoading = true;
277
+ _tokenFlyoutError = '';
278
+ renderTokenFlyout();
279
+ try {
280
+ const [summary, lifetimeRaw, bySource, timeline] = await Promise.all([
281
+ api('/api/tokens/summary', 0),
282
+ api('/api/tokens/lifetime', 0),
283
+ api('/api/tokens/by-source', 0),
284
+ api('/api/tokens/timeline?limit=8', 0),
285
+ ]);
286
+ _tokenTelemetry = {
287
+ summary: summary ?? {},
288
+ lifetime: lifetimeRaw?.data ?? lifetimeRaw ?? {},
289
+ bySource: bySource ?? {},
290
+ timeline: Array.isArray(timeline) ? timeline : [],
291
+ };
292
+ } catch (err) {
293
+ _tokenFlyoutError = err?.message || String(err);
294
+ } finally {
295
+ _tokenFlyoutLoading = false;
296
+ renderTokenFlyout();
297
+ }
298
+ }
299
+
300
+ function renderTokenFlyout() {
301
+ const slot = $('rt-token-flyout-slot');
302
+ if (!slot) return;
303
+ const btn = $('rt-kpi-saved');
304
+ if (btn) btn.setAttribute('aria-expanded', _tokenFlyoutOpen ? 'true' : 'false');
305
+ if (!_tokenFlyoutOpen) {
306
+ slot.innerHTML = '';
307
+ return;
308
+ }
309
+
310
+ if (_tokenFlyoutLoading && !_tokenTelemetry) {
311
+ slot.innerHTML = `<div class="rt-token-flyout" id="rt-token-flyout">
312
+ <div class="rt-token-head"><div><div class="rt-token-title">Token telemetry</div><div class="rt-token-sub">Loading current runtime ledger...</div></div><button class="rt-token-close" id="rt-token-close" type="button">Close</button></div>
313
+ </div>`;
314
+ } else if (_tokenFlyoutError) {
315
+ slot.innerHTML = `<div class="rt-token-flyout" id="rt-token-flyout">
316
+ <div class="rt-token-head"><div><div class="rt-token-title">Token telemetry</div><div class="rt-token-sub rt-token-error">${esc(_tokenFlyoutError)}</div></div><button class="rt-token-close" id="rt-token-close" type="button">Close</button></div>
317
+ </div>`;
318
+ } else {
319
+ const data = _tokenTelemetry ?? {};
320
+ const summary = data.summary ?? {};
321
+ const lifetime = data.lifetime ?? {};
322
+ const bySource = data.bySource ?? {};
323
+ const timeline = Array.isArray(data.timeline) ? data.timeline : [];
324
+ const sourceRows = Object.entries(bySource).slice(0, 5).map(([source, value]) => {
325
+ const item = typeof value === 'object' && value ? value : { savedTokens: Number(value ?? 0) };
326
+ const saved = item.savedTokens ?? item.tokensSaved ?? item.saved ?? 0;
327
+ const gross = item.grossInputTokens ?? item.tokensOptimized ?? item.gross ?? 0;
328
+ return `<div class="rt-token-row"><span>${esc(source)}</span><strong>${fmtTokens(saved)}</strong><span>${fmtTokens(gross)} gross</span></div>`;
329
+ }).join('') || '<div class="rt-token-muted">No source ledger yet.</div>';
330
+ const timelineRows = timeline.slice(0, 6).map((item) => {
331
+ const saved = item.savedTokens ?? item.tokensSaved ?? item.saved ?? 0;
332
+ const run = item.runId ?? item.sessionId ?? item.source ?? 'runtime';
333
+ return `<div class="rt-token-row"><span>${esc(String(run).slice(0, 18))}</span><strong>${fmtTokens(saved)}</strong><span>${fmtTime(item.timestamp ?? item.ts ?? item.time)}</span></div>`;
334
+ }).join('') || '<div class="rt-token-muted">No recent token events yet.</div>';
335
+ slot.innerHTML = `<div class="rt-token-flyout" id="rt-token-flyout">
336
+ <div class="rt-token-head">
337
+ <div>
338
+ <div class="rt-token-title">Token telemetry</div>
339
+ <div class="rt-token-sub">Runtime ledger, lifetime savings, and live SSE savings for this tab.</div>
340
+ </div>
341
+ <button class="rt-token-close" id="rt-token-close" type="button">Close</button>
342
+ </div>
343
+ <div class="rt-token-grid">
344
+ <div class="rt-token-stat"><span>Recent saved</span><strong>${fmtTokens(summary.savedTokens ?? summary.saved)}</strong></div>
345
+ <div class="rt-token-stat"><span>Recent compression</span><strong>${fmtPct(summary.compressionPct)}</strong></div>
346
+ <div class="rt-token-stat"><span>Lifetime saved</span><strong>${fmtTokens(lifetime.savedTokens ?? lifetime.totalSaved)}</strong></div>
347
+ <div class="rt-token-stat"><span>Live tab saved</span><strong>${fmtTokens(_totalTokensSaved)}</strong></div>
348
+ </div>
349
+ <div class="rt-token-columns">
350
+ <div><div class="rt-token-section-title">By source</div>${sourceRows}</div>
351
+ <div><div class="rt-token-section-title">Recent runs</div>${timelineRows}</div>
352
+ </div>
353
+ </div>`;
354
+ }
355
+
356
+ $('rt-token-close')?.addEventListener('click', () => {
357
+ _tokenFlyoutOpen = false;
358
+ renderTokenFlyout();
359
+ renderKPIs();
360
+ });
148
361
  }
149
362
 
150
363
  function renderMcpStrip() {
@@ -162,10 +375,12 @@ function renderMcpStrip() {
162
375
  </div>`;
163
376
  });
164
377
 
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';
378
+ const resolved = _resolvedMcp.slice(0, MAX_RESOLVED_MCP).map(({ tool, durationMs, error, stale }) => {
379
+ const cls = error ? 'rt-mcp-pill rt-mcp-err' : stale ? 'rt-mcp-pill rt-mcp-long' : 'rt-mcp-pill rt-mcp-done';
167
380
  const badge = error
168
381
  ? `<span class="rt-mcp-badge rt-mcp-badge-err">err</span>`
382
+ : stale
383
+ ? `<span class="rt-mcp-badge rt-mcp-badge-ok">stale</span>`
169
384
  : `<span class="rt-mcp-badge rt-mcp-badge-ok">${humanMs(durationMs)}</span>`;
170
385
  return `<div class="${cls}">${esc(tool)}${badge}</div>`;
171
386
  });
@@ -182,6 +397,7 @@ function renderMcpStrip() {
182
397
  function ensurePulseTimer() {
183
398
  if (_pulseTimer || _activeTools.size === 0) return;
184
399
  _pulseTimer = setInterval(() => {
400
+ expireStaleActiveTools();
185
401
  if (_activeTools.size === 0) {
186
402
  clearInterval(_pulseTimer);
187
403
  _pulseTimer = null;
@@ -271,7 +487,8 @@ function showUpgradeNudge(msg, ctaUrl) {
271
487
  export function ingestEvent(evt) {
272
488
  const { type = '', payload = {} } = evt;
273
489
  const category = categoryFor(type);
274
- const tool = String(payload.toolName ?? payload.tool ?? '');
490
+ const tool = toolNameFromPayload(payload);
491
+ const eventTime = eventMillis(evt, payload);
275
492
  const tokensSaved = Number(payload.tokensSaved ?? evt.tokensSaved ?? 0);
276
493
  const durationMs = Number(payload.durationMs ?? 0);
277
494
  const phase = String(payload.phase ?? payload.stage ?? '');
@@ -280,18 +497,22 @@ export function ingestEvent(evt) {
280
497
  const callId = String(payload.callId ?? '');
281
498
  const trackKey = callId || tool;
282
499
  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);
500
+ if (!wasSettledAfter(tool, eventTime)) {
501
+ _activeTools.set(trackKey, { tool, startedAt: Date.now(), callId });
502
+ _toolCalls++;
503
+ ensurePulseTimer();
504
+ }
505
+ } else if (
506
+ type === 'mcp.call.complete' ||
507
+ type === 'mcp.handler.complete' ||
508
+ type === 'mcp.handler.failed' ||
509
+ (type === 'tool.invocation' && trackKey)
510
+ ) {
511
+ if (!settleActiveTool(trackKey, tool, payload, { durationMs, eventTime })) {
512
+ rememberSettledTool(tool || trackKey, eventTime);
294
513
  }
514
+ } else if (type === 'nexus.shutdown' || type === 'orchestrator.disposed') {
515
+ clearActiveToolsAsStale();
295
516
  }
296
517
 
297
518
  // Toast for license events
@@ -325,11 +546,20 @@ export function ingestEvent(evt) {
325
546
  break;
326
547
  case 'memory.store':
327
548
  label = 'memory · store';
328
- detail = String(payload.key ?? payload.content ?? '').slice(0, 80);
549
+ detail = [
550
+ payload.id ? `id ${String(payload.id).slice(0, 12)}` : null,
551
+ payload.tier ? `tier ${payload.tier}` : null,
552
+ payload.priority != null ? `priority ${Number(payload.priority).toFixed(2)}` : null,
553
+ Array.isArray(payload.tags) && payload.tags.length ? `tags ${payload.tags.slice(0, 3).join(', ')}` : null,
554
+ ].filter(Boolean).join(' · ') || String(payload.key ?? payload.content ?? '').slice(0, 80);
329
555
  break;
330
556
  case 'memory.recall':
331
557
  label = 'memory · recall';
332
- detail = String(payload.query ?? '').slice(0, 80);
558
+ detail = [
559
+ `${Number(payload.count ?? 0)} recalled`,
560
+ payload.crossClient ? 'cross-client' : null,
561
+ String(payload.query ?? '').slice(0, 60),
562
+ ].filter(Boolean).join(' · ');
333
563
  break;
334
564
  case 'tokens.optimized': {
335
565
  const src = payload.source ? ` · ${payload.source}` : '';
@@ -45,6 +45,7 @@ export function handleDispatchEvent(evt) {
45
45
  const type = evt.type ?? '';
46
46
 
47
47
  if (type === 'dispatch.started') {
48
+ if (oid) _dispatches.delete(`__warmup__:${oid}`);
48
49
  run.status = 'spawning';
49
50
  run.invoker = p.invokerId ?? '';
50
51
  } else if (type === 'dispatch.event') {
@@ -117,18 +118,20 @@ function _buildDispatchStrip(run) {
117
118
  }).join('<span class="ds-sep">→</span>');
118
119
 
119
120
  const isFinal = ['complete','failed','cancelled'].includes(run.status);
121
+ const isPendingAdapter = run.pendingAdapter === true;
120
122
 
121
123
  return `<div class="dispatch-strip" data-run-id="${esc(run.runId)}">
122
124
  <div class="ds-stages">${stageHtml}</div>
123
125
  ${run.messages.length ? `<div class="ds-stdout">${esc(run.messages[run.messages.length - 1])}</div>` : ''}
124
126
  <div class="ds-meta">
127
+ ${run.invoker ? `<span>${esc(run.invoker)}</span>` : ''}
125
128
  ${run.tokens ? `<span>${run.tokens.toLocaleString()} tokens</span>` : ''}
126
129
  ${run.costUsd ? `<span>$${run.costUsd.toFixed(4)}</span>` : ''}
127
130
  ${run.filesChanged.length ? `<span>${run.filesChanged.length} file(s)</span>` : ''}
128
131
  </div>
129
132
  ${run.summary ? `<div class="ds-summary">${esc(String(run.summary).slice(0, 160))}</div>` : ''}
130
133
  ${run.error ? `<div class="ds-error">${esc(run.error)}</div>` : ''}
131
- ${!isFinal ? `<button class="btn btn-sm ds-stop-btn" data-stop-run="${esc(run.runId)}">Stop</button>` : ''}
134
+ ${!isFinal && !isPendingAdapter ? `<button class="btn btn-sm ds-stop-btn" data-stop-run="${esc(run.runId)}">Stop</button>` : ''}
132
135
  </div>`;
133
136
  }
134
137
 
@@ -560,14 +563,18 @@ function _showHireSheet(specialistId, name) {
560
563
  stripDiv.setAttribute('data-dispatch-strip', '');
561
564
  drawerBody.appendChild(stripDiv);
562
565
  }
563
- const warmupRun = { runId: '__warmup__', operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: ['Warm-up dispatching…'], filesChanged: [] };
564
- _dispatches.set('__warmup__', warmupRun);
566
+ const warmupKey = `__warmup__:${operativeId}`;
567
+ const warmupRun = { runId: warmupKey, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: ['Adapter pending; waiting for dispatch.started…'], filesChanged: [], pendingAdapter: true };
568
+ _dispatches.set(warmupKey, warmupRun);
565
569
  stripDiv.innerHTML = _buildDispatchStrip(warmupRun);
566
570
  }
567
571
  const fd = real.data?.firstDispatch;
568
- _dispatches.delete('__warmup__');
569
572
  if (fd?.runId) {
573
+ _dispatches.delete(`__warmup__:${operativeId}`);
570
574
  _dispatches.set(fd.runId, { runId: fd.runId, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: [], filesChanged: [] });
575
+ } else if (fd?.queued) {
576
+ const pending = _dispatches.get(`__warmup__:${operativeId}`);
577
+ if (pending) pending.messages = ['First sortie queued; adapter start will stream here.'];
571
578
  }
572
579
  _refreshDrawerForOp(operativeId);
573
580
  }
@@ -24,6 +24,10 @@ export interface NexusEventPayloads {
24
24
  'memory.recall': {
25
25
  query: string;
26
26
  count: number;
27
+ k?: number;
28
+ crossClient?: boolean;
29
+ format?: string;
30
+ preview?: string[];
27
31
  };
28
32
  'memory.flushed': {
29
33
  count: number;
@@ -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.22",
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",