nexus-prime 7.9.21 β†’ 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
  }
@@ -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; }
@@ -30,6 +30,10 @@ let _settledToolTimes = new Map();
30
30
  let _pulseTimer = null;
31
31
  // toast queue
32
32
  let _toastTimer = null;
33
+ let _tokenFlyoutOpen = false;
34
+ let _tokenFlyoutLoading = false;
35
+ let _tokenFlyoutError = '';
36
+ let _tokenTelemetry = null;
33
37
 
34
38
  /* ── Category metadata ──────────────────────────────────────────────────────── */
35
39
  const CATEGORY_META = {
@@ -65,6 +69,27 @@ function humanMs(ms) {
65
69
  return `${(ms / 1000).toFixed(1)}s`;
66
70
  }
67
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
+
68
93
  function toolNameFromPayload(payload) {
69
94
  return String(payload.toolName ?? payload.tool ?? payload.name ?? '').trim();
70
95
  }
@@ -159,10 +184,10 @@ function mount() {
159
184
  <div class="rt-kpi-val" id="rt-toolcalls">0</div>
160
185
  <div class="rt-kpi-lbl">Tool Calls</div>
161
186
  </div>
162
- <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">
163
188
  <div class="rt-kpi-val" id="rt-tokens-saved">0</div>
164
189
  <div class="rt-kpi-lbl">Tokens Saved</div>
165
- </div>
190
+ </button>
166
191
  <div class="rt-kpi">
167
192
  <div class="rt-kpi-val" id="rt-active-count">0</div>
168
193
  <div class="rt-kpi-lbl">Active Now</div>
@@ -170,6 +195,7 @@ function mount() {
170
195
  </div>
171
196
 
172
197
  <div class="rt-mcp-strip" id="rt-mcp-strip"></div>
198
+ <div class="rt-token-flyout-slot" id="rt-token-flyout-slot"></div>
173
199
 
174
200
  <div class="rt-filter-bar">
175
201
  <button class="rt-filter-btn active" data-filter="all">All</button>
@@ -212,6 +238,14 @@ function mount() {
212
238
  renderAll();
213
239
  });
214
240
 
241
+ $('rt-kpi-saved')?.addEventListener('click', () => {
242
+ _tokenFlyoutOpen = !_tokenFlyoutOpen;
243
+ renderTokenFlyout();
244
+ if (_tokenFlyoutOpen && !_tokenTelemetry && !_tokenFlyoutLoading) {
245
+ void loadTokenTelemetry();
246
+ }
247
+ });
248
+
215
249
  _mounted = true;
216
250
  renderAll();
217
251
  }
@@ -220,6 +254,7 @@ function mount() {
220
254
  function renderAll() {
221
255
  expireStaleActiveTools();
222
256
  renderKPIs();
257
+ renderTokenFlyout();
223
258
  renderMcpStrip();
224
259
  renderFeed();
225
260
  }
@@ -233,6 +268,96 @@ function renderKPIs() {
233
268
  ? (_totalTokensSaved >= 1000 ? `${(_totalTokensSaved / 1000).toFixed(1)}k` : String(_totalTokensSaved))
234
269
  : '0';
235
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
+ });
236
361
  }
237
362
 
238
363
  function renderMcpStrip() {
@@ -421,11 +546,20 @@ export function ingestEvent(evt) {
421
546
  break;
422
547
  case 'memory.store':
423
548
  label = 'memory Β· store';
424
- 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);
425
555
  break;
426
556
  case 'memory.recall':
427
557
  label = 'memory Β· recall';
428
- 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(' Β· ');
429
563
  break;
430
564
  case 'tokens.optimized': {
431
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.21",
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",