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.
- package/dist/agents/adapters/mcp/handlers/memory.js +44 -5
- 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.js +11 -6
- package/dist/dashboard/app/styles/runtime.css +148 -0
- package/dist/dashboard/app/styles/workforce.css +28 -0
- package/dist/dashboard/app/views/board.js +19 -5
- package/dist/dashboard/app/views/runtime.js +248 -18
- package/dist/dashboard/app/views/workforce.js +11 -4
- package/dist/engines/event-bus.d.ts +4 -0
- package/dist/engines/workflow-runtime.js +9 -1
- package/dist/synapse/operatives/crud.js +5 -1
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
|
|
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:
|
|
84
|
-
|
|
85
|
-
|
|
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 =
|
|
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,
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
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,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
|
-
<
|
|
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
|
-
</
|
|
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 =
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
564
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
@@ -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.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",
|