nexus-prime 7.9.18 → 7.9.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/adapters/mcp/definitions.js +2 -3
- package/dist/agents/adapters/mcp/handlers/governance.js +1 -1
- package/dist/cli/doctor-storage.js +1 -2
- package/dist/cli.js +12 -11
- package/dist/daemon/client.d.ts +1 -0
- package/dist/daemon/server.js +1 -0
- package/dist/dashboard/app/styles/board.css +8 -0
- package/dist/dashboard/app/views/board.js +75 -15
- package/dist/dashboard/app/views/context-log.js +32 -3
- package/dist/dashboard/app/views/knowledge.js +104 -13
- package/dist/dashboard/routes/events.js +91 -1
- package/dist/dashboard/routes/memory.js +71 -16
- package/dist/dashboard/routes/runtime.js +125 -13
- package/dist/dashboard/server.js +22 -1
- package/dist/dashboard/welcome.html +13 -13
- package/dist/engines/knowledge-fabric.d.ts +2 -8
- package/dist/engines/ngram-index.d.ts +4 -1
- package/dist/engines/ngram-index.js +16 -14
- package/dist/engines/orchestrator.d.ts +2 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/package.json +2 -2
|
@@ -976,11 +976,10 @@ export function buildMcpToolDefinitions() {
|
|
|
976
976
|
required: ['taskId', 'goal', 'findings'],
|
|
977
977
|
},
|
|
978
978
|
},
|
|
979
|
-
// When NEXUS_DISABLE_WORKFORCE=1 (
|
|
979
|
+
// When NEXUS_DISABLE_WORKFORCE=1 (explicit break-glass mode),
|
|
980
980
|
// strip the 24 nexus_synapse_* / nexus_architects_* tools from the
|
|
981
981
|
// catalog so the model doesn't see surface area for engines that
|
|
982
|
-
// aren't initialized at runtime.
|
|
983
|
-
// re-enable both engines + their tools.
|
|
982
|
+
// aren't initialized at runtime.
|
|
984
983
|
...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : synapseToolDefinitions),
|
|
985
984
|
...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : architectsToolDefinitions),
|
|
986
985
|
// ── Workforce (unified worker+job layer) ──────────────────────────
|
|
@@ -173,7 +173,7 @@ export async function handleGovernanceGroup(toolName, hctx, request, args, ctx)
|
|
|
173
173
|
const manifest = ensureBootstrap({
|
|
174
174
|
packageRoot: PROJECT_ROOT,
|
|
175
175
|
workspaceRoot: hctx.nexusRef.getWorkspaceContext().workspaceRoot,
|
|
176
|
-
phase: '
|
|
176
|
+
phase: 'install',
|
|
177
177
|
silent: true,
|
|
178
178
|
});
|
|
179
179
|
const memoryMaintenance = hctx.nexusRef.maintainMemory();
|
|
@@ -12,10 +12,9 @@ import { resolveRunsBudget, resolveWorktreeBudget } from './cleanup.js';
|
|
|
12
12
|
import { dirBytes, formatBytes } from '../install/fs-purge.js';
|
|
13
13
|
import { enumerateNgramArchives, enumerateStatePaths, getNexusStateDir, getRuntimeTmpRoots, getWorktreeRoots, } from '../install/state-locator.js';
|
|
14
14
|
import { INSTALL_ARCH_GENERATION, loadManifest, } from '../install/manifest.js';
|
|
15
|
-
import { getNgramFootprintBytes } from '../engines/ngram-index.js';
|
|
15
|
+
import { getNgramFootprintBytes, NGRAM_DEFAULT_FOOTPRINT_BYTES } from '../engines/ngram-index.js';
|
|
16
16
|
import { getSharedLicenseManager } from '../licensing/license-manager.js';
|
|
17
17
|
const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
18
|
-
const NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024;
|
|
19
18
|
function readEnvBytesPositive(name, fallback) {
|
|
20
19
|
const raw = process.env[name];
|
|
21
20
|
if (!raw)
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { cliSetup, configureIDE, computeFileHash, readSetupMarker, writeSetupMar
|
|
|
30
30
|
import { isNewUser, promptLicenseKey, printReturningUserBanner } from './cli/interactive-setup.js';
|
|
31
31
|
import { runHookBootstrap, runHookMemory, runHookMindkit, runHookGhostPass, runHookSessionDna } from './cli/hook.js';
|
|
32
32
|
import { resolveWorkspaceContext } from './engines/workspace-resolver.js';
|
|
33
|
-
import { ensureDaemonReady, getDaemonStatus, stopDaemon } from './daemon/client.js';
|
|
33
|
+
import { ensureDaemonReady, getDaemonStatus, pingDaemonHealth, stopDaemon } from './daemon/client.js';
|
|
34
34
|
import { NexusDaemonServer } from './daemon/server.js';
|
|
35
35
|
import { DaemonSupervisor } from './daemon/supervisor.js';
|
|
36
36
|
import { startDaemonBackedMcpProxy } from './daemon/proxy.js';
|
|
@@ -720,8 +720,10 @@ program
|
|
|
720
720
|
try {
|
|
721
721
|
const record = await ensureDaemonManaged({ force: options.force });
|
|
722
722
|
const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
|
|
723
|
+
const health = await pingDaemonHealth(record).catch(() => null);
|
|
724
|
+
const dashboardUrl = health?.dashboardUrl ?? `http://localhost:${dashboardPort}`;
|
|
723
725
|
console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
|
|
724
|
-
console.log(`Dashboard:
|
|
726
|
+
console.log(`Dashboard: ${dashboardUrl}`);
|
|
725
727
|
}
|
|
726
728
|
catch (err) {
|
|
727
729
|
console.error(`Failed to start daemon: ${err.message}`);
|
|
@@ -820,15 +822,14 @@ program
|
|
|
820
822
|
if (process.env.NEXUS_DAEMON_FAST_START === undefined) {
|
|
821
823
|
process.env.NEXUS_DAEMON_FAST_START = '1';
|
|
822
824
|
}
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
process.env.NEXUS_DISABLE_WORKFORCE = '1';
|
|
825
|
+
// Keep daemon startup fast, but do not disable workforce. Synapse and
|
|
826
|
+
// Architects can lazily warm on first hire/tool call unless the operator
|
|
827
|
+
// explicitly sets the NEXUS_DISABLE_WORKFORCE escape hatch.
|
|
828
|
+
if (process.env.NEXUS_SYNAPSE_LAZY === undefined) {
|
|
829
|
+
process.env.NEXUS_SYNAPSE_LAZY = '1';
|
|
830
|
+
}
|
|
831
|
+
if (process.env.NEXUS_ARCHITECTS_LAZY === undefined) {
|
|
832
|
+
process.env.NEXUS_ARCHITECTS_LAZY = '1';
|
|
832
833
|
}
|
|
833
834
|
const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
|
|
834
835
|
const daemon = new NexusDaemonServer(workspaceContext);
|
package/dist/daemon/client.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -390,6 +390,7 @@ export class NexusDaemonServer {
|
|
|
390
390
|
stateKey: this.workspace.stateKey,
|
|
391
391
|
workspaceRoot: this.workspace.workspaceRoot,
|
|
392
392
|
repoRoot: this.workspace.repoRoot,
|
|
393
|
+
dashboardUrl: this.nexus?.getDashboardAddress?.() ?? null,
|
|
393
394
|
startedAt: this.lockRecord?.startedAt ?? Date.now(),
|
|
394
395
|
});
|
|
395
396
|
return;
|
|
@@ -395,6 +395,12 @@
|
|
|
395
395
|
color: var(--text-main); margin-bottom: 4px;
|
|
396
396
|
}
|
|
397
397
|
.frh-sub { font-size: var(--text-sm); color: var(--text-dim); }
|
|
398
|
+
.frh-command {
|
|
399
|
+
display: grid;
|
|
400
|
+
grid-template-columns: minmax(220px, 1fr) auto auto;
|
|
401
|
+
gap: 8px;
|
|
402
|
+
margin-bottom: 14px;
|
|
403
|
+
}
|
|
398
404
|
.frh-picks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
399
405
|
.frh-pick {
|
|
400
406
|
background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
@@ -417,6 +423,8 @@
|
|
|
417
423
|
}
|
|
418
424
|
@media (max-width: 768px) {
|
|
419
425
|
#kanban-board { grid-template-columns: repeat(2, 1fr); }
|
|
426
|
+
.frh-command,
|
|
427
|
+
.frh-picks { grid-template-columns: 1fr; }
|
|
420
428
|
}
|
|
421
429
|
@media (max-width: 480px) {
|
|
422
430
|
#kanban-board { grid-template-columns: 1fr; }
|
|
@@ -76,12 +76,14 @@ function normalizeOperative(op) {
|
|
|
76
76
|
|
|
77
77
|
/* ── Data loader ── */
|
|
78
78
|
export async function load() {
|
|
79
|
-
const [tok, life, op, sh, health] = await Promise.all([
|
|
79
|
+
const [tok, life, op, sh, health, memHealth, runs] = await Promise.all([
|
|
80
80
|
api('/api/tokens/summary'),
|
|
81
81
|
api('/api/tokens/lifetime', 15000),
|
|
82
82
|
api('/api/dashboard/surface/operate', 5000),
|
|
83
83
|
api('/api/synapse/health', 5000),
|
|
84
84
|
api('/api/health', 15000),
|
|
85
|
+
api('/api/memory/health', 15000),
|
|
86
|
+
api('/api/runs?limit=12', 3000),
|
|
85
87
|
]);
|
|
86
88
|
// Non-blocking: tool health data from ring buffer
|
|
87
89
|
loadToolHealth();
|
|
@@ -91,6 +93,8 @@ export async function load() {
|
|
|
91
93
|
S.synapseHealthRaw = sh;
|
|
92
94
|
S.synapseHealth = (Array.isArray(sh) ? sh : (sh?.operatives||[])).map(normalizeOperative);
|
|
93
95
|
S.healthData = health;
|
|
96
|
+
S.memHealth = memHealth;
|
|
97
|
+
S.runs = Array.isArray(runs) ? runs : [];
|
|
94
98
|
notifyNotReady([sh]);
|
|
95
99
|
// Prefetch curated specialists for first-run hero (non-blocking)
|
|
96
100
|
if (!S.synapseHealth.length && !S.curatedSpecialists) {
|
|
@@ -354,13 +358,14 @@ function renderFirstRunHero() {
|
|
|
354
358
|
if (!parent) return;
|
|
355
359
|
|
|
356
360
|
const hasOps = S.synapseHealth.length > 0;
|
|
361
|
+
const hasRuns = (S.runs || []).length > 0;
|
|
357
362
|
const alreadySeen = (() => { try { return !!localStorage.getItem(FIRST_RUN_KEY); } catch { return false; } })();
|
|
358
363
|
|
|
359
364
|
// Remove any existing hero card
|
|
360
365
|
const existing = $('first-run-hero');
|
|
361
366
|
if (existing) existing.remove();
|
|
362
367
|
|
|
363
|
-
if (hasOps || alreadySeen) return;
|
|
368
|
+
if (hasOps || (alreadySeen && hasRuns)) return;
|
|
364
369
|
|
|
365
370
|
const specs = (S.curatedSpecialists || []).slice(0, 3);
|
|
366
371
|
if (!specs.length) return; // Still loading — will re-render when prefetch resolves
|
|
@@ -376,10 +381,15 @@ function renderFirstRunHero() {
|
|
|
376
381
|
card.className = 'first-run-hero card';
|
|
377
382
|
card.innerHTML = `
|
|
378
383
|
<div class="frh-header">
|
|
379
|
-
<div class="frh-title"
|
|
380
|
-
<div class="frh-sub"
|
|
384
|
+
<div class="frh-title">${hasRuns ? 'Your next hire' : 'Start the first real run'}</div>
|
|
385
|
+
<div class="frh-sub">${hasRuns ? 'Hire a specialist to start running tasks autonomously.' : 'Run a goal from the dashboard or hire a specialist. The run will appear in Board and Context Log.'}</div>
|
|
381
386
|
</div>
|
|
382
387
|
${noticesHtml}
|
|
388
|
+
<div class="frh-command">
|
|
389
|
+
<input id="frh-goal-input" class="form-input" type="text" placeholder="Inspect this repo and suggest the next fix" autocomplete="off">
|
|
390
|
+
<button class="btn btn-primary btn-sm" id="frh-run-btn">Run goal</button>
|
|
391
|
+
<button class="btn btn-sm" id="frh-context-btn">Open context</button>
|
|
392
|
+
</div>
|
|
383
393
|
<div class="frh-picks">
|
|
384
394
|
${specs.map(s => `
|
|
385
395
|
<div class="frh-pick" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}">
|
|
@@ -393,6 +403,33 @@ function renderFirstRunHero() {
|
|
|
393
403
|
<button class="btn btn-ghost btn-sm frh-dismiss" style="margin-top:var(--space-3)">Dismiss</button>`;
|
|
394
404
|
|
|
395
405
|
// Wire buttons before inserting
|
|
406
|
+
card.querySelector('#frh-run-btn')?.addEventListener('click', async () => {
|
|
407
|
+
const input = card.querySelector('#frh-goal-input');
|
|
408
|
+
const button = card.querySelector('#frh-run-btn');
|
|
409
|
+
const goal = (input?.value || `Inspect ${S.workspace?.repoName || 'this repo'} and report the next best action`).trim();
|
|
410
|
+
if (!goal) return;
|
|
411
|
+
if (button) {
|
|
412
|
+
button.disabled = true;
|
|
413
|
+
button.textContent = 'Queueing…';
|
|
414
|
+
}
|
|
415
|
+
setFirstRunStatus('Queueing dashboard run…');
|
|
416
|
+
const result = await post('/api/orchestrate', { goal, source: 'dashboard-onboarding' });
|
|
417
|
+
if (result.ok) {
|
|
418
|
+
setFirstRunStatus('Run queued. Board and Context Log will update as Nexus writes artifacts.');
|
|
419
|
+
bustCache('/api/runs?limit=12');
|
|
420
|
+
bustCache('/api/events');
|
|
421
|
+
setTimeout(load, 900);
|
|
422
|
+
} else {
|
|
423
|
+
setFirstRunStatus(result.error || 'Run failed to queue.', 'bad');
|
|
424
|
+
if (button) {
|
|
425
|
+
button.disabled = false;
|
|
426
|
+
button.textContent = 'Run goal';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
card.querySelector('#frh-context-btn')?.addEventListener('click', () => {
|
|
431
|
+
window.location.hash = '#context-log';
|
|
432
|
+
});
|
|
396
433
|
card.querySelectorAll('.frh-hire-btn').forEach(btn => {
|
|
397
434
|
btn.addEventListener('click', async e => {
|
|
398
435
|
e.stopPropagation();
|
|
@@ -452,18 +489,41 @@ function renderAgentsLiveStrip() {
|
|
|
452
489
|
/* ── Kanban ── */
|
|
453
490
|
function buildKanbanCols() {
|
|
454
491
|
const cols={planning:[],hiring:[],running:[],ghostpass:[],done:[]};
|
|
455
|
-
const op=S.operateSurface;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
492
|
+
const op=S.operateSurface;
|
|
493
|
+
if (op) {
|
|
494
|
+
const pc=op.orchestration?.planningContext||op.planningContext;
|
|
495
|
+
if (pc?.goal) cols.planning.push({id:'ctx',goal:pc.goal,status:'planning',tokens:null,time:pc.startedAt,role:null});
|
|
496
|
+
const ws=op.orchestration?.workerPlan?.workers||op.workerPlan?.workers||[];
|
|
497
|
+
for (const w of ws) {
|
|
498
|
+
const st=String(w.status||'').toLowerCase();
|
|
499
|
+
const sg=st==='active'||st==='running'?'running':st==='hiring'?'hiring':st==='complete'||st==='done'?'done':st==='reviewing'||st.includes('ghost')?'ghostpass':null;
|
|
500
|
+
if (sg) cols[sg].push({id:w.id||w.workerId||w.goal,goal:w.goal||w.task||w.approach||'(worker)',status:st,tokens:w.tokensUsed||w.budget,time:w.startedAt||w.createdAt,role:w.role});
|
|
501
|
+
}
|
|
463
502
|
}
|
|
464
|
-
for (const r of (S.runs||[]).slice(0,
|
|
465
|
-
|
|
466
|
-
|
|
503
|
+
for (const r of (S.runs||[]).slice(0,8)) {
|
|
504
|
+
const runId = r.runId || r.id;
|
|
505
|
+
if (!runId) continue;
|
|
506
|
+
const status = String(r.status || r.state || '').toLowerCase();
|
|
507
|
+
const stage = String(r.stage || '').toLowerCase();
|
|
508
|
+
const lane = status.includes('complete') || status === 'done' || status === 'failed'
|
|
509
|
+
? 'done'
|
|
510
|
+
: stage.includes('hire')
|
|
511
|
+
? 'hiring'
|
|
512
|
+
: stage.includes('ghost') || stage.includes('review')
|
|
513
|
+
? 'ghostpass'
|
|
514
|
+
: status === 'running' || stage.includes('orchestrat')
|
|
515
|
+
? 'running'
|
|
516
|
+
: 'planning';
|
|
517
|
+
if (!cols[lane].some(c=>c.id===runId)) {
|
|
518
|
+
cols[lane].push({
|
|
519
|
+
id: runId,
|
|
520
|
+
goal: r.goal||r.mandate||'(run)',
|
|
521
|
+
status: status || stage || 'queued',
|
|
522
|
+
tokens:r.tokensUsed||r.tokenCount,
|
|
523
|
+
time:r.completedAt||r.updatedAt||r.createdAt||r.startedAt,
|
|
524
|
+
role:null,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
467
527
|
}
|
|
468
528
|
return cols;
|
|
469
529
|
}
|
|
@@ -89,17 +89,42 @@ function decisionRows(entries) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function renderRunRail(runs) {
|
|
92
|
-
if (!runs.length) return
|
|
92
|
+
if (!runs.length) return `<div class="empty">
|
|
93
|
+
<div class="empty-title">No runs recorded</div>
|
|
94
|
+
<div class="empty-sub">Run a goal from the command bar and its context spine will appear here.</div>
|
|
95
|
+
</div>`;
|
|
93
96
|
return runs.map(run => {
|
|
94
97
|
const id = runIdOf(run);
|
|
95
98
|
const active = id === S.contextLogSelectedRunId;
|
|
96
99
|
return `<button class="context-log-run ${active ? 'active' : ''}" data-context-run="${esc(id)}">
|
|
97
100
|
<span>${esc(String(id).slice(-10) || 'run')}</span>
|
|
98
|
-
<small>${esc(run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt))}</small>
|
|
101
|
+
<small>${esc(run.stage || run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt || run.startedAt || run.updatedAt))}</small>
|
|
99
102
|
</button>`;
|
|
100
103
|
}).join('');
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function emptyContextMain() {
|
|
107
|
+
return `<div class="context-log-summary context-log-empty-state">
|
|
108
|
+
<div class="context-log-kpi"><span>Runs</span><strong>0</strong></div>
|
|
109
|
+
<div class="context-log-kpi"><span>Context</span><strong>—</strong></div>
|
|
110
|
+
<div class="context-log-kpi"><span>Decision</span><strong>—</strong></div>
|
|
111
|
+
<div class="context-log-selection">
|
|
112
|
+
<div>
|
|
113
|
+
<span>Start</span>
|
|
114
|
+
<button class="btn btn-sm" id="context-log-run-goal-btn">Run a goal</button>
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<span>Expected here</span>
|
|
118
|
+
${chips(['request brief', 'selection plan', 'context events', 'decisions'])}
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<span>History</span>
|
|
122
|
+
<span class="context-log-empty">Previous runs persist after the dashboard restarts.</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
export async function load() {
|
|
104
129
|
const runs = await api('/api/runs?limit=30', 3000);
|
|
105
130
|
S.contextLogRuns = Array.isArray(runs) ? runs : [];
|
|
@@ -130,7 +155,7 @@ export function render() {
|
|
|
130
155
|
${renderRunRail(S.contextLogRuns || [])}
|
|
131
156
|
</aside>
|
|
132
157
|
<section class="context-log-main">
|
|
133
|
-
${spine ? summaryCard(spine) :
|
|
158
|
+
${spine ? summaryCard(spine) : emptyContextMain()}
|
|
134
159
|
<div class="context-log-grid">
|
|
135
160
|
<div>
|
|
136
161
|
<div class="shd">Context events</div>
|
|
@@ -150,6 +175,10 @@ export function render() {
|
|
|
150
175
|
await load();
|
|
151
176
|
});
|
|
152
177
|
});
|
|
178
|
+
$('context-log-run-goal-btn')?.addEventListener('click', () => {
|
|
179
|
+
window.location.hash = '#board';
|
|
180
|
+
setTimeout(() => $('cmd-input')?.focus(), 100);
|
|
181
|
+
});
|
|
153
182
|
$('context-log-refresh-btn')?.addEventListener('click', async () => {
|
|
154
183
|
bustCache('/api/runs?limit=30');
|
|
155
184
|
if (S.contextLogSelectedRunId) bustCache(spineUrl(S.contextLogSelectedRunId));
|
|
@@ -17,6 +17,20 @@ function fmtNum(n) {
|
|
|
17
17
|
return String(Math.round(v));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function collectionIdOf(collection) {
|
|
21
|
+
return collection?.collectionId || collection?.id || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectionNameOf(collection) {
|
|
25
|
+
return collection?.name || collectionIdOf(collection) || 'collection';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function collectionCounts(collection) {
|
|
29
|
+
const sources = collection?.sourceCount ?? collection?.documentCount ?? collection?.count ?? 0;
|
|
30
|
+
const chunks = collection?.chunkCount ?? 0;
|
|
31
|
+
return `${fmtNum(sources)} sources · ${fmtNum(chunks)} chunks`;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/* ── Data loader ── */
|
|
21
35
|
export async function load() {
|
|
22
36
|
const [rags, topo] = await Promise.all([
|
|
@@ -46,24 +60,38 @@ function renderRagCollections() {
|
|
|
46
60
|
$('rag-create-btn')?.addEventListener('click', _showCreateSheet);
|
|
47
61
|
return;
|
|
48
62
|
}
|
|
49
|
-
el.innerHTML=`<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;justify-content:flex-
|
|
63
|
+
el.innerHTML=`<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;justify-content:space-between;flex-wrap:wrap">
|
|
64
|
+
<span style="font-size:var(--text-sm);color:var(--text-muted)">Attach collections to feed the orchestrator and dashboard chat.</span>
|
|
50
65
|
<button class="btn btn-sm" id="rag-create-btn">New collection</button>
|
|
51
66
|
</div>
|
|
52
|
-
<div>${cols.map(c
|
|
67
|
+
<div>${cols.map(c=> {
|
|
68
|
+
const id = collectionIdOf(c);
|
|
69
|
+
const name = collectionNameOf(c);
|
|
70
|
+
const attached = (c.attachedRuntimeIds || []).length || (c.attachedSessionIds || []).length;
|
|
71
|
+
return `<div class="mission-row">
|
|
53
72
|
<div class="mission-body">
|
|
54
|
-
<div class="mission-title">${esc(
|
|
73
|
+
<div class="mission-title">${esc(name)}</div>
|
|
55
74
|
<div class="mission-meta" style="font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-dim)">
|
|
56
|
-
${
|
|
75
|
+
${collectionCounts(c)} · ${attached ? 'attached' : 'detached'}
|
|
57
76
|
</div>
|
|
58
77
|
</div>
|
|
59
|
-
<button class="btn btn-sm" data-ingest="${esc(
|
|
60
|
-
<button class="btn btn-sm" data-
|
|
61
|
-
|
|
78
|
+
<button class="btn btn-sm" data-ingest="${esc(id)}" data-ingestname="${esc(name)}">Add source</button>
|
|
79
|
+
<button class="btn btn-sm" data-ingest-repo="${esc(id)}" data-ingestname="${esc(name)}">Ingest repo</button>
|
|
80
|
+
<button class="btn btn-sm" data-attach="${esc(id)}" data-attached="${attached ? '1' : '0'}">${attached ? 'Detach' : 'Attach'}</button>
|
|
81
|
+
<button class="btn btn-sm" data-drop-target="${esc(id)}" data-drop-name="${esc(name)}" title="Drop files to upload">Upload</button>
|
|
82
|
+
</div>`;
|
|
83
|
+
}).join('')}</div>
|
|
62
84
|
${_renderDropZone(cols)}`;
|
|
63
85
|
|
|
64
86
|
$('rag-create-btn')?.addEventListener('click', _showCreateSheet);
|
|
65
87
|
el.querySelectorAll('[data-ingest]').forEach(btn => {
|
|
66
|
-
btn.addEventListener('click', () =>
|
|
88
|
+
btn.addEventListener('click', () => _showIngestSheet(btn.dataset.ingest, btn.dataset.ingestname));
|
|
89
|
+
});
|
|
90
|
+
el.querySelectorAll('[data-ingest-repo]').forEach(btn => {
|
|
91
|
+
btn.addEventListener('click', () => _ingestRepoCollection(btn.dataset.ingestRepo, btn.dataset.ingestname, btn));
|
|
92
|
+
});
|
|
93
|
+
el.querySelectorAll('[data-attach]').forEach(btn => {
|
|
94
|
+
btn.addEventListener('click', () => _toggleCollectionAttach(btn.dataset.attach, btn.dataset.attached === '1', btn));
|
|
67
95
|
});
|
|
68
96
|
el.querySelectorAll('[data-drop-target]').forEach(btn => {
|
|
69
97
|
btn.addEventListener('click', () => _focusDropZone(btn.dataset.dropTarget, btn.dataset.dropName));
|
|
@@ -73,7 +101,7 @@ function renderRagCollections() {
|
|
|
73
101
|
|
|
74
102
|
/** Render the drag-and-drop upload zone below the collection list. */
|
|
75
103
|
function _renderDropZone(cols) {
|
|
76
|
-
const opts = cols.map(c=>`<option value="${esc(c
|
|
104
|
+
const opts = cols.map(c=>`<option value="${esc(collectionIdOf(c))}">${esc(collectionNameOf(c))}</option>`).join('');
|
|
77
105
|
return `
|
|
78
106
|
<div id="rag-drop-zone" class="rag-drop-zone" aria-label="Drop files to ingest" role="region">
|
|
79
107
|
<div class="rag-drop-inner" id="rag-drop-inner">
|
|
@@ -157,7 +185,7 @@ async function _uploadFiles(files) {
|
|
|
157
185
|
const r = await post(`/api/rag/collections/${encodeURIComponent(collectionId)}/ingest`, { inputs }).catch(() => null);
|
|
158
186
|
|
|
159
187
|
if (progress) progress.textContent = '';
|
|
160
|
-
if (r
|
|
188
|
+
if (r?.ok) {
|
|
161
189
|
_toast(`Ingested ${inputs.length} file(s) into collection.`, 'good');
|
|
162
190
|
bustCache('/api/rag/collections');
|
|
163
191
|
setTimeout(load, 1200);
|
|
@@ -166,12 +194,13 @@ async function _uploadFiles(files) {
|
|
|
166
194
|
}
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
async function
|
|
197
|
+
async function _ingestRepoCollection(id, name, btn) {
|
|
170
198
|
btn.disabled=true; btn.textContent='Ingesting…';
|
|
171
199
|
const r=await post('/api/rag/ingest', { collectionId: id });
|
|
172
|
-
btn.disabled=false; btn.textContent='Ingest';
|
|
200
|
+
btn.disabled=false; btn.textContent='Ingest repo';
|
|
173
201
|
if (r.ok) {
|
|
174
|
-
|
|
202
|
+
const added = r.data?.sourcesAdded ?? r.data?.chunksAdded ?? 0;
|
|
203
|
+
_toast(`Collection "${name}" ingested ${added ? fmtNum(added) : 'repo'} source(s).`, 'good');
|
|
175
204
|
bustCache('/api/rag/collections');
|
|
176
205
|
setTimeout(load, 1500);
|
|
177
206
|
} else {
|
|
@@ -179,6 +208,68 @@ async function _ingestCollection(id, name, btn) {
|
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
async function _toggleCollectionAttach(id, attached, btn) {
|
|
212
|
+
btn.disabled = true;
|
|
213
|
+
btn.textContent = attached ? 'Detaching…' : 'Attaching…';
|
|
214
|
+
const r = await post(`/api/rag/collections/${encodeURIComponent(id)}/${attached ? 'detach' : 'attach'}`, {});
|
|
215
|
+
if (r.ok) {
|
|
216
|
+
_toast(attached ? 'Collection detached.' : 'Collection attached.', 'good');
|
|
217
|
+
bustCache('/api/rag/collections');
|
|
218
|
+
setTimeout(load, 700);
|
|
219
|
+
} else {
|
|
220
|
+
btn.disabled = false;
|
|
221
|
+
btn.textContent = attached ? 'Detach' : 'Attach';
|
|
222
|
+
_toast(`Collection update failed: ${r.error}`, 'bad');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _showIngestSheet(collectionId, collectionName) {
|
|
227
|
+
openDrawer({
|
|
228
|
+
title: `Add source to ${collectionName}`,
|
|
229
|
+
body: `<div class="dsec">
|
|
230
|
+
<div class="dsec-title">Ingest source</div>
|
|
231
|
+
<label class="form-label" for="rag-ingest-url">URL</label>
|
|
232
|
+
<input id="rag-ingest-url" class="form-input" type="url" placeholder="https://docs.example.com/page">
|
|
233
|
+
<label class="form-label" for="rag-ingest-file" style="margin-top:10px">File or folder path</label>
|
|
234
|
+
<input id="rag-ingest-file" class="form-input" type="text" placeholder="${esc(S.workspace?.repoRoot || '/path/to/repo')}">
|
|
235
|
+
<label class="form-label" for="rag-ingest-text" style="margin-top:10px">Text note</label>
|
|
236
|
+
<textarea id="rag-ingest-text" class="form-input" rows="5" placeholder="Paste notes, API docs, or decision context"></textarea>
|
|
237
|
+
<label class="form-label" for="rag-ingest-tags" style="margin-top:10px">Tags</label>
|
|
238
|
+
<input id="rag-ingest-tags" class="form-input" type="text" placeholder="repo,dashboard">
|
|
239
|
+
<button class="btn btn-primary" id="rag-ingest-confirm" style="margin-top:var(--space-4)">Ingest source</button>
|
|
240
|
+
<div id="rag-ingest-status" style="margin-top:var(--space-3);font-size:var(--text-sm);color:var(--text-muted)"></div>
|
|
241
|
+
</div>`,
|
|
242
|
+
});
|
|
243
|
+
$('rag-ingest-confirm')?.addEventListener('click', () => _submitIngest(collectionId));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function _submitIngest(collectionId) {
|
|
247
|
+
const status = $('rag-ingest-status');
|
|
248
|
+
const url = ($('rag-ingest-url')?.value || '').trim();
|
|
249
|
+
const fileOrFolder = ($('rag-ingest-file')?.value || '').trim();
|
|
250
|
+
const text = ($('rag-ingest-text')?.value || '').trim();
|
|
251
|
+
const tags = ($('rag-ingest-tags')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
252
|
+
const inputs = [];
|
|
253
|
+
if (url) inputs.push({ url, tags });
|
|
254
|
+
if (fileOrFolder) inputs.push({ folderPath: fileOrFolder, filePath: fileOrFolder, tags });
|
|
255
|
+
if (text) inputs.push({ text, label: 'dashboard-note', tags });
|
|
256
|
+
if (!inputs.length) {
|
|
257
|
+
if (status) { status.textContent = 'Add a URL, file/folder path, or text note.'; status.style.color = 'var(--bad)'; }
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (status) { status.textContent = 'Ingesting…'; status.style.color = 'var(--text-muted)'; }
|
|
261
|
+
const r = await post(`/api/rag/collections/${encodeURIComponent(collectionId)}/ingest`, { inputs }).catch(() => null);
|
|
262
|
+
if (r?.ok) {
|
|
263
|
+
if (status) { status.textContent = `Added ${r.data?.sourcesAdded ?? inputs.length} source(s).`; status.style.color = 'var(--ok)'; }
|
|
264
|
+
_toast('RAG source ingested.', 'good');
|
|
265
|
+
bustCache('/api/rag/collections');
|
|
266
|
+
setTimeout(load, 900);
|
|
267
|
+
} else if (status) {
|
|
268
|
+
status.textContent = `Ingest failed: ${r?.error ?? 'unknown error'}`;
|
|
269
|
+
status.style.color = 'var(--bad)';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
182
273
|
function _showCreateSheet() {
|
|
183
274
|
openDrawer({
|
|
184
275
|
title: 'Create collection',
|
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { CONTEXT_LOG_FILE, DECISION_LOG_FILE, REQUEST_BRIEF_FILE, SELECTION_PLAN_FILE, } from '../../engines/orchestrator/decision-spine.js';
|
|
4
4
|
import { getSharedNgramIndex } from '../../engines/ngram-index.js';
|
|
5
|
+
import { getRun as getStoredRun, getSpans } from '../../engines/orchestrator/store.js';
|
|
5
6
|
import { getSharedTelemetry } from '../../engines/telemetry-remote.js';
|
|
6
7
|
import { getDashboardEventCards } from '../stream/sse-broker.js';
|
|
7
8
|
async function readJsonArtifact(run, filename, fallback) {
|
|
@@ -29,6 +30,90 @@ async function readJsonlArtifact(run, filename, fallback) {
|
|
|
29
30
|
return fallback;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
function storedRunDecisionSpine(run) {
|
|
34
|
+
const spans = getSpans(run.runId);
|
|
35
|
+
const contextLog = spans.length
|
|
36
|
+
? spans.map((span) => ({
|
|
37
|
+
phase: span.name,
|
|
38
|
+
actor: 'dashboard',
|
|
39
|
+
reason: String(span.attrs?.summary ?? span.attrs?.state ?? span.name),
|
|
40
|
+
createdAt: span.startedAt,
|
|
41
|
+
contextRefs: [span.spanId],
|
|
42
|
+
evidenceRefs: [
|
|
43
|
+
span.attrs?.actualRunId ? `actual:${span.attrs.actualRunId}` : null,
|
|
44
|
+
span.status === 'error' && span.attrs?.error ? `error:${span.attrs.error}` : null,
|
|
45
|
+
].filter(Boolean),
|
|
46
|
+
}))
|
|
47
|
+
: [{
|
|
48
|
+
phase: run.stage ?? 'intake',
|
|
49
|
+
actor: run.client ?? 'dashboard',
|
|
50
|
+
reason: `${run.status ?? 'queued'} run: ${run.goal ?? run.runId}`,
|
|
51
|
+
createdAt: run.updatedAt ?? run.startedAt ?? Date.now(),
|
|
52
|
+
evidenceRefs: [run.runId],
|
|
53
|
+
}];
|
|
54
|
+
const decisionLog = [{
|
|
55
|
+
verb: run.status === 'failed' ? 'blocked' : run.status === 'completed' ? 'completed' : 'queued',
|
|
56
|
+
surface: 'dashboard',
|
|
57
|
+
decision: run.status === 'failed'
|
|
58
|
+
? (run.error ?? 'Run failed')
|
|
59
|
+
: run.status === 'completed'
|
|
60
|
+
? (run.resultSummary ?? 'Run completed')
|
|
61
|
+
: `Run is ${run.stage ?? run.status ?? 'queued'}`,
|
|
62
|
+
reason: run.goal ?? '',
|
|
63
|
+
createdAt: run.completedAt ?? run.updatedAt ?? run.startedAt ?? Date.now(),
|
|
64
|
+
beforeRef: run.runId,
|
|
65
|
+
afterRef: run.status === 'completed' ? 'orchestration-store' : null,
|
|
66
|
+
}];
|
|
67
|
+
const requestBrief = {
|
|
68
|
+
runId: run.runId,
|
|
69
|
+
rawPrompt: run.goal,
|
|
70
|
+
intent: 'orchestrate',
|
|
71
|
+
risk: run.status === 'failed' ? 'needs-attention' : 'standard',
|
|
72
|
+
source: run.client ?? 'dashboard',
|
|
73
|
+
createdAt: run.startedAt,
|
|
74
|
+
};
|
|
75
|
+
const selectionPlan = {
|
|
76
|
+
runId: run.runId,
|
|
77
|
+
intent: 'orchestrate',
|
|
78
|
+
selected: {
|
|
79
|
+
files: [],
|
|
80
|
+
skills: [],
|
|
81
|
+
specialists: [],
|
|
82
|
+
crews: [],
|
|
83
|
+
},
|
|
84
|
+
executionPolicy: {
|
|
85
|
+
route: run.client ?? 'dashboard',
|
|
86
|
+
stage: run.stage,
|
|
87
|
+
progress: run.progress,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
runId: run.runId,
|
|
92
|
+
artifactsPath: null,
|
|
93
|
+
files: {
|
|
94
|
+
requestBrief: REQUEST_BRIEF_FILE,
|
|
95
|
+
selectionPlan: SELECTION_PLAN_FILE,
|
|
96
|
+
contextLog: CONTEXT_LOG_FILE,
|
|
97
|
+
decisionLog: DECISION_LOG_FILE,
|
|
98
|
+
},
|
|
99
|
+
artifacts: {
|
|
100
|
+
requestBrief,
|
|
101
|
+
selectionPlan,
|
|
102
|
+
contextLog,
|
|
103
|
+
decisionLog,
|
|
104
|
+
},
|
|
105
|
+
summary: {
|
|
106
|
+
intent: requestBrief.intent,
|
|
107
|
+
risk: requestBrief.risk,
|
|
108
|
+
modelRoute: null,
|
|
109
|
+
selected: selectionPlan.selected,
|
|
110
|
+
contextEvents: contextLog.length,
|
|
111
|
+
decisions: decisionLog.length,
|
|
112
|
+
latestDecision: decisionLog[decisionLog.length - 1],
|
|
113
|
+
source: 'orchestration-store',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
32
117
|
export const handleEventRoutes = async (ctx, req, res, url) => {
|
|
33
118
|
if (req.method === 'GET' && url.pathname === '/stream') {
|
|
34
119
|
ctx.serveSse(req, res);
|
|
@@ -56,7 +141,12 @@ export const handleEventRoutes = async (ctx, req, res, url) => {
|
|
|
56
141
|
const runId = decodeURIComponent(decisionSpineMatch[1]);
|
|
57
142
|
const run = await ctx.getRuntime()?.getRun?.(runId);
|
|
58
143
|
if (!run) {
|
|
59
|
-
|
|
144
|
+
const stored = getStoredRun(runId);
|
|
145
|
+
if (!stored) {
|
|
146
|
+
ctx.respondJson(res, { error: 'decision-spine-not-found', runId }, 404);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
ctx.respondJson(res, storedRunDecisionSpine(stored));
|
|
60
150
|
return true;
|
|
61
151
|
}
|
|
62
152
|
const [requestBrief, selectionPlan, contextLog, decisionLog] = await Promise.all([
|
|
@@ -1,5 +1,50 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import { resolveWorkspaceContext } from '../../engines/workspace-resolver.js';
|
|
4
|
+
function safeWorkspacePath(value, allowedRoots) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return undefined;
|
|
7
|
+
const abs = path.resolve(String(value));
|
|
8
|
+
return allowedRoots.some((root) => abs === root || abs.startsWith(root + path.sep)) ? abs : undefined;
|
|
9
|
+
}
|
|
10
|
+
async function isDirectory(value) {
|
|
11
|
+
try {
|
|
12
|
+
return (await fs.stat(value)).isDirectory();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function normalizeRagInputs(body, allowedRoots) {
|
|
19
|
+
const rawInputs = Array.isArray(body?.inputs) ? body.inputs : [body ?? {}];
|
|
20
|
+
const normalized = await Promise.all(rawInputs
|
|
21
|
+
.map(async (entry) => {
|
|
22
|
+
let filePath = safeWorkspacePath(entry?.filePath, allowedRoots);
|
|
23
|
+
let folderPath = safeWorkspacePath(entry?.folderPath, allowedRoots);
|
|
24
|
+
if (filePath && !folderPath && await isDirectory(filePath)) {
|
|
25
|
+
folderPath = filePath;
|
|
26
|
+
filePath = undefined;
|
|
27
|
+
}
|
|
28
|
+
if (filePath && folderPath && filePath === folderPath) {
|
|
29
|
+
if (await isDirectory(filePath)) {
|
|
30
|
+
filePath = undefined;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
folderPath = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
filePath,
|
|
38
|
+
folderPath,
|
|
39
|
+
folderGlob: entry?.folderGlob ? String(entry.folderGlob) : undefined,
|
|
40
|
+
url: entry?.url ? String(entry.url) : undefined,
|
|
41
|
+
text: entry?.text ? String(entry.text) : undefined,
|
|
42
|
+
label: entry?.label ? String(entry.label) : undefined,
|
|
43
|
+
tags: Array.isArray(entry?.tags) ? entry.tags.map(String) : [],
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
return normalized.filter((entry) => entry.filePath || entry.folderPath || entry.url || entry.text);
|
|
47
|
+
}
|
|
3
48
|
export const handleMemoryRoutes = async (ctx, req, res, url) => {
|
|
4
49
|
if (req.method === 'GET' && url.pathname === '/api/knowledge-fabric/session') {
|
|
5
50
|
const snapshot = ctx.resolveRuntimeSnapshot(url);
|
|
@@ -222,22 +267,32 @@ export const handleMemoryRoutes = async (ctx, req, res, url) => {
|
|
|
222
267
|
ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
|
|
223
268
|
return true;
|
|
224
269
|
}
|
|
225
|
-
const inputs =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
:
|
|
270
|
+
const inputs = await normalizeRagInputs(body, allowedRoots);
|
|
271
|
+
ctx.respondJson(res, await orchestrator.ingestRagCollection(collectionId, inputs));
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (req.method === 'POST' && url.pathname === '/api/rag/ingest') {
|
|
275
|
+
const body = await ctx.readJsonBody(req);
|
|
276
|
+
const collectionId = String(body?.collectionId ?? body?.id ?? '').trim();
|
|
277
|
+
const orchestrator = ctx.getOrchestrator();
|
|
278
|
+
const workspace = ctx.workspaceContextProvider?.() ?? resolveWorkspaceContext({ workspaceRoot: ctx.repoRoot });
|
|
279
|
+
const allowedRoots = [workspace.workspaceRoot, workspace.repoRoot].map((root) => path.resolve(root));
|
|
280
|
+
if (!orchestrator) {
|
|
281
|
+
ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (!collectionId) {
|
|
285
|
+
ctx.respondJson(res, { error: 'collectionId-required' }, 400);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
let inputs = await normalizeRagInputs(body, allowedRoots);
|
|
289
|
+
if (!inputs.length && !body?.inputs && !body?.filePath && !body?.folderPath && !body?.url && !body?.text) {
|
|
290
|
+
inputs = [{
|
|
291
|
+
folderPath: workspace.repoRoot,
|
|
292
|
+
label: workspace.repoName || path.basename(workspace.repoRoot),
|
|
293
|
+
tags: ['repo', 'dashboard'],
|
|
294
|
+
}];
|
|
295
|
+
}
|
|
241
296
|
ctx.respondJson(res, await orchestrator.ingestRagCollection(collectionId, inputs));
|
|
242
297
|
return true;
|
|
243
298
|
}
|
|
@@ -5,6 +5,7 @@ import { execAsync } from '../../utils/exec-async.js';
|
|
|
5
5
|
import { recordFirstInteraction } from '../../engines/telemetry.js';
|
|
6
6
|
import { pushDispatch, cancelDispatch, listActiveDispatches } from '../../engines/dispatch/push-dispatch.js';
|
|
7
7
|
import { invokerRegistry } from '../../invokers/registry.js';
|
|
8
|
+
import { completeRun, createRun, failRun, getRun as getStoredRun, listRecentRuns, startSpan, endSpan, updateStage, } from '../../engines/orchestrator/store.js';
|
|
8
9
|
function normalizeLifetimeTokenPayload(record) {
|
|
9
10
|
const savedTokens = Number(record?.totalSavedTokens ?? 0);
|
|
10
11
|
const grossInputTokens = Number(record?.totalGrossInputTokens ?? 0);
|
|
@@ -48,6 +49,72 @@ function normalizeTokenSummaryPayload(record) {
|
|
|
48
49
|
net: compressedTokens,
|
|
49
50
|
};
|
|
50
51
|
}
|
|
52
|
+
function mergeDashboardRuns(liveRuns, storedRuns, limit) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
return [...(liveRuns ?? []), ...(storedRuns ?? [])]
|
|
55
|
+
.map((run) => {
|
|
56
|
+
const runId = run?.runId ?? run?.id;
|
|
57
|
+
return runId ? { ...run, runId, id: run.id ?? runId } : null;
|
|
58
|
+
})
|
|
59
|
+
.filter((run) => {
|
|
60
|
+
if (!run?.runId || seen.has(run.runId))
|
|
61
|
+
return false;
|
|
62
|
+
seen.add(run.runId);
|
|
63
|
+
return true;
|
|
64
|
+
})
|
|
65
|
+
.sort((left, right) => {
|
|
66
|
+
const leftTs = Number(left.updatedAt ?? left.completedAt ?? left.createdAt ?? left.startedAt ?? 0);
|
|
67
|
+
const rightTs = Number(right.updatedAt ?? right.completedAt ?? right.createdAt ?? right.startedAt ?? 0);
|
|
68
|
+
return rightTs - leftTs;
|
|
69
|
+
})
|
|
70
|
+
.slice(0, Math.max(1, limit));
|
|
71
|
+
}
|
|
72
|
+
function summarizeDashboardRun(run, fallbackGoal) {
|
|
73
|
+
const state = run?.state ?? run?.status ?? 'completed';
|
|
74
|
+
const summary = run?.summary ?? run?.resultSummary ?? run?.message ?? '';
|
|
75
|
+
return String(summary || `${state}: ${fallbackGoal}`).slice(0, 1024);
|
|
76
|
+
}
|
|
77
|
+
async function tokenLedgerFallback(ctx) {
|
|
78
|
+
const memory = ctx.getMemory?.();
|
|
79
|
+
if (!memory)
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const { TokenAnalyticsEngine } = await import('../../engines/token-analytics.js');
|
|
83
|
+
const report = new TokenAnalyticsEngine(memory).getLifetimeReport();
|
|
84
|
+
const optimized = Number(report.totalTokensOptimized ?? 0);
|
|
85
|
+
const saved = Number(report.totalTokensSaved ?? 0);
|
|
86
|
+
const forwarded = Number(report.totalTokensForwarded ?? Math.max(0, optimized - saved));
|
|
87
|
+
if (optimized <= 0 && saved <= 0 && forwarded <= 0)
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
totalSavedTokens: saved,
|
|
91
|
+
totalGrossInputTokens: optimized,
|
|
92
|
+
totalCompressedTokens: forwarded,
|
|
93
|
+
totalRuns: Number(report.totalSessions ?? 0),
|
|
94
|
+
lastUpdatedAt: Date.now(),
|
|
95
|
+
tokenLedger: report,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function mergeLifetimeTokenRecords(fileRecord, ledgerRecord) {
|
|
103
|
+
if (!ledgerRecord)
|
|
104
|
+
return fileRecord;
|
|
105
|
+
return {
|
|
106
|
+
...fileRecord,
|
|
107
|
+
totalSavedTokens: Math.max(Number(fileRecord?.totalSavedTokens ?? 0), Number(ledgerRecord.totalSavedTokens ?? 0)),
|
|
108
|
+
totalGrossInputTokens: Math.max(Number(fileRecord?.totalGrossInputTokens ?? 0), Number(ledgerRecord.totalGrossInputTokens ?? 0)),
|
|
109
|
+
totalCompressedTokens: Math.max(Number(fileRecord?.totalCompressedTokens ?? 0), Number(ledgerRecord.totalCompressedTokens ?? 0)),
|
|
110
|
+
totalRuns: Math.max(Number(fileRecord?.totalRuns ?? 0), Number(ledgerRecord.totalRuns ?? 0)),
|
|
111
|
+
lastUpdatedAt: Math.max(Number(fileRecord?.lastUpdatedAt ?? 0), Number(ledgerRecord.lastUpdatedAt ?? 0)),
|
|
112
|
+
sources: {
|
|
113
|
+
lifetimeFile: fileRecord,
|
|
114
|
+
tokenLedger: ledgerRecord.tokenLedger,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
51
118
|
async function readGitBranch(repoRoot) {
|
|
52
119
|
try {
|
|
53
120
|
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
|
@@ -64,7 +131,9 @@ async function readGitBranch(repoRoot) {
|
|
|
64
131
|
export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
65
132
|
if (req.method === 'GET' && url.pathname === '/api/runs') {
|
|
66
133
|
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
67
|
-
|
|
134
|
+
const liveRuns = ctx.getRuntime()?.listRuns(limit) ?? [];
|
|
135
|
+
const storedRuns = listRecentRuns(limit);
|
|
136
|
+
ctx.respondJson(res, mergeDashboardRuns(liveRuns, storedRuns, limit));
|
|
68
137
|
return true;
|
|
69
138
|
}
|
|
70
139
|
if (req.method === 'GET' && url.pathname === '/api/usage') {
|
|
@@ -140,7 +209,8 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
140
209
|
if (req.method === 'GET' && url.pathname === '/api/tokens/lifetime') {
|
|
141
210
|
try {
|
|
142
211
|
const { readLifetimeTokens } = await import('../../engines/lifetime-tokens.js');
|
|
143
|
-
|
|
212
|
+
const merged = mergeLifetimeTokenRecords(readLifetimeTokens(), await tokenLedgerFallback(ctx));
|
|
213
|
+
ctx.respondJson(res, { ok: true, data: normalizeLifetimeTokenPayload(merged) });
|
|
144
214
|
}
|
|
145
215
|
catch (err) {
|
|
146
216
|
ctx.respondJson(res, { ok: false, error: err?.message ?? 'Failed to read lifetime tokens' });
|
|
@@ -167,7 +237,7 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
167
237
|
}
|
|
168
238
|
if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
|
|
169
239
|
const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
|
|
170
|
-
const run = await ctx.getRuntime()?.getRun?.(runId);
|
|
240
|
+
const run = await ctx.getRuntime()?.getRun?.(runId) ?? getStoredRun(runId);
|
|
171
241
|
ctx.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
|
|
172
242
|
return true;
|
|
173
243
|
}
|
|
@@ -215,13 +285,39 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
215
285
|
ctx.respondJson(res, { error: 'goal-required' }, 400);
|
|
216
286
|
return true;
|
|
217
287
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
288
|
+
const dashboardRunId = `dash_${Date.now()}`;
|
|
289
|
+
createRun(dashboardRunId, goal, 'dashboard-chat');
|
|
290
|
+
updateStage(dashboardRunId, 'orchestrating', 35);
|
|
291
|
+
const spanId = startSpan(dashboardRunId, 'dashboard.runtime.execute', undefined, { source: 'dashboard-chat' });
|
|
292
|
+
try {
|
|
293
|
+
const run = await orchestrator.orchestrate(goal, { ...body, goal, source: body.source ?? 'dashboard-chat' });
|
|
294
|
+
const actualRunId = run?.runId ?? dashboardRunId;
|
|
295
|
+
if (actualRunId !== dashboardRunId) {
|
|
296
|
+
createRun(actualRunId, goal, 'dashboard-chat');
|
|
297
|
+
updateStage(actualRunId, 'orchestrating', 80);
|
|
298
|
+
}
|
|
299
|
+
completeRun(dashboardRunId, summarizeDashboardRun(run, goal));
|
|
300
|
+
if (actualRunId !== dashboardRunId)
|
|
301
|
+
completeRun(actualRunId, summarizeDashboardRun(run, goal));
|
|
302
|
+
endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
|
|
303
|
+
nexusEventBus.emit('dashboard.action', {
|
|
304
|
+
action: 'runtime.execute',
|
|
305
|
+
status: run.state,
|
|
306
|
+
target: actualRunId,
|
|
307
|
+
});
|
|
308
|
+
ctx.respondJson(res, run, 201);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
const message = err?.message ?? String(err);
|
|
312
|
+
failRun(dashboardRunId, message);
|
|
313
|
+
endSpan(spanId, 'error', { error: message });
|
|
314
|
+
nexusEventBus.emit('dashboard.action', {
|
|
315
|
+
action: 'runtime.execute.failed',
|
|
316
|
+
status: 'fail',
|
|
317
|
+
target: dashboardRunId,
|
|
318
|
+
});
|
|
319
|
+
ctx.respondJson(res, { error: message, runId: dashboardRunId }, 500);
|
|
320
|
+
}
|
|
225
321
|
return true;
|
|
226
322
|
}
|
|
227
323
|
if (req.method === 'POST' && url.pathname === '/api/runtime/plan') {
|
|
@@ -315,22 +411,38 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
315
411
|
return true;
|
|
316
412
|
}
|
|
317
413
|
const runId = `run_${Date.now()}`;
|
|
414
|
+
createRun(runId, goal, 'dashboard');
|
|
415
|
+
updateStage(runId, 'queued', 10);
|
|
416
|
+
const spanId = startSpan(runId, 'dashboard.orchestrate', undefined, { source: body.source ?? 'dashboard' });
|
|
318
417
|
// TTV: first goal submission from the dashboard counts as first interaction.
|
|
319
418
|
void recordFirstInteraction().catch(() => { });
|
|
320
419
|
// Fire-and-forget: orchestration runs in the background; caller polls /api/runs/:id.
|
|
321
|
-
orchestrator.orchestrate(goal, { source: 'dashboard' })
|
|
420
|
+
orchestrator.orchestrate(goal, { source: body.source ?? 'dashboard', dashboardRunId: runId })
|
|
322
421
|
.then((run) => {
|
|
422
|
+
const actualRunId = run?.runId ?? runId;
|
|
423
|
+
if (actualRunId !== runId) {
|
|
424
|
+
createRun(actualRunId, goal, 'dashboard');
|
|
425
|
+
updateStage(actualRunId, 'orchestrating', 80);
|
|
426
|
+
}
|
|
427
|
+
const summary = summarizeDashboardRun(run, goal);
|
|
428
|
+
completeRun(runId, summary);
|
|
429
|
+
if (actualRunId !== runId)
|
|
430
|
+
completeRun(actualRunId, summary);
|
|
431
|
+
endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
|
|
323
432
|
nexusEventBus.emit('dashboard.action', {
|
|
324
433
|
action: 'orchestrate.complete',
|
|
325
434
|
status: run?.state === 'failed' ? 'fail' : 'ok',
|
|
326
|
-
target:
|
|
435
|
+
target: actualRunId,
|
|
327
436
|
});
|
|
328
437
|
})
|
|
329
438
|
.catch((err) => {
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
failRun(runId, message);
|
|
441
|
+
endSpan(spanId, 'error', { error: message });
|
|
330
442
|
nexusEventBus.emit('dashboard.action', {
|
|
331
443
|
action: 'orchestrate.failed',
|
|
332
444
|
status: 'fail',
|
|
333
|
-
target:
|
|
445
|
+
target: runId,
|
|
334
446
|
});
|
|
335
447
|
});
|
|
336
448
|
nexusEventBus.emit('dashboard.action', { action: 'orchestrate.enqueue', status: 'queued', target: runId });
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1126,9 +1126,26 @@ export class DashboardServer {
|
|
|
1126
1126
|
const capabilities = this.buildAdvertisedCapabilities();
|
|
1127
1127
|
const compatibility = this.buildCompatibilityStatus(capabilities);
|
|
1128
1128
|
const runtimeEnvelope = this.buildProbeRuntimeEnvelope();
|
|
1129
|
+
const workspace = this.resolveCanonicalWorkspaceContext();
|
|
1129
1130
|
return {
|
|
1130
1131
|
dashboardApiVersion: DASHBOARD_API_VERSION,
|
|
1131
|
-
projectName:
|
|
1132
|
+
projectName: workspace.repoName,
|
|
1133
|
+
repoIdentity: {
|
|
1134
|
+
repoName: workspace.repoName,
|
|
1135
|
+
repoRoot: workspace.repoRoot,
|
|
1136
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
1137
|
+
workspaceSource: workspace.workspaceSource,
|
|
1138
|
+
workspaceStateKey: workspace.stateKey,
|
|
1139
|
+
remoteUrl: workspace.remoteUrl ?? null,
|
|
1140
|
+
currentRepoId: null,
|
|
1141
|
+
},
|
|
1142
|
+
scanContext: {
|
|
1143
|
+
repoRoot: workspace.repoRoot,
|
|
1144
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
1145
|
+
workspaceSource: workspace.workspaceSource,
|
|
1146
|
+
workspaceStateKey: workspace.stateKey,
|
|
1147
|
+
notes: ['Probe snapshot uses the lightweight compatibility contract.'],
|
|
1148
|
+
},
|
|
1132
1149
|
capabilities,
|
|
1133
1150
|
compatibility,
|
|
1134
1151
|
dashboardUrl: this.getAddress(),
|
|
@@ -1513,6 +1530,10 @@ export class DashboardServer {
|
|
|
1513
1530
|
if (payload.compatibility?.status !== 'compatible') {
|
|
1514
1531
|
return false;
|
|
1515
1532
|
}
|
|
1533
|
+
const stateRoot = payload.runtimeEnvelope?.workspace?.stateRoot;
|
|
1534
|
+
if (!stateRoot || path.resolve(stateRoot) !== path.resolve(resolveNexusStateDir())) {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1516
1537
|
const required = payload.compatibility?.requiredCapabilities?.length
|
|
1517
1538
|
? payload.compatibility.requiredCapabilities
|
|
1518
1539
|
: [...DASHBOARD_COMPATIBILITY_CAPABILITIES];
|
|
@@ -312,14 +312,14 @@
|
|
|
312
312
|
|
|
313
313
|
<!-- Step 3: Ready -->
|
|
314
314
|
<div class="panel" id="panel-3" style="display:none">
|
|
315
|
-
<div class="panel-title" style="color:var(--ok)">✓
|
|
316
|
-
<div class="panel-sub">Your
|
|
315
|
+
<div class="panel-title" style="color:var(--ok)">✓ Control plane ready</div>
|
|
316
|
+
<div class="panel-sub">Your dashboard can now run goals, hire operatives, ingest knowledge, and show context history.</div>
|
|
317
317
|
|
|
318
318
|
<div class="ready-grid">
|
|
319
319
|
<div class="ready-tile">
|
|
320
320
|
<div class="ready-tile-icon">🧠</div>
|
|
321
|
-
<div class="ready-tile-label">
|
|
322
|
-
<div class="ready-tile-desc">
|
|
321
|
+
<div class="ready-tile-label">Run a goal</div>
|
|
322
|
+
<div class="ready-tile-desc">Queue a dashboard run and watch Board plus Context Log fill with real artifacts.</div>
|
|
323
323
|
</div>
|
|
324
324
|
<div class="ready-tile">
|
|
325
325
|
<div class="ready-tile-icon">👥</div>
|
|
@@ -328,13 +328,13 @@
|
|
|
328
328
|
</div>
|
|
329
329
|
<div class="ready-tile">
|
|
330
330
|
<div class="ready-tile-icon">📡</div>
|
|
331
|
-
<div class="ready-tile-label">
|
|
332
|
-
<div class="ready-tile-desc">
|
|
331
|
+
<div class="ready-tile-label">Context history</div>
|
|
332
|
+
<div class="ready-tile-desc">Open prior runs, selected files, decisions, memory events, and model routing.</div>
|
|
333
333
|
</div>
|
|
334
334
|
<div class="ready-tile">
|
|
335
335
|
<div class="ready-tile-icon">🔑</div>
|
|
336
|
-
<div class="ready-tile-label">
|
|
337
|
-
<div class="ready-tile-desc">
|
|
336
|
+
<div class="ready-tile-label">Knowledge ready</div>
|
|
337
|
+
<div class="ready-tile-desc">Create RAG collections, attach them, and ingest repo files or pasted notes.</div>
|
|
338
338
|
</div>
|
|
339
339
|
</div>
|
|
340
340
|
|
|
@@ -348,9 +348,9 @@
|
|
|
348
348
|
</div>
|
|
349
349
|
|
|
350
350
|
<div class="actions" style="margin-top:24px">
|
|
351
|
-
<a class="btn primary" href="
|
|
351
|
+
<a class="btn primary" href="/#board">Open Board →</a>
|
|
352
352
|
<a class="btn" href="/#workforce">Workforce</a>
|
|
353
|
-
<a class="btn" href="/#
|
|
353
|
+
<a class="btn" href="/#context-log">Context Log</a>
|
|
354
354
|
</div>
|
|
355
355
|
</div>
|
|
356
356
|
|
|
@@ -561,8 +561,8 @@
|
|
|
561
561
|
|
|
562
562
|
// ── Step 3: Ready ─────────────────────────────────────────────────────────
|
|
563
563
|
function loadReadyStep() {
|
|
564
|
-
document.getElementById('page-title').textContent = '
|
|
565
|
-
document.getElementById('page-sub').textContent = '
|
|
564
|
+
document.getElementById('page-title').textContent = 'Nexus Prime is ready';
|
|
565
|
+
document.getElementById('page-sub').textContent = 'Open Board, run the first goal, or hire an operative. The dashboard will keep the run history visible.';
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
async function hireOperative() {
|
|
@@ -575,7 +575,7 @@
|
|
|
575
575
|
const res = await fetch('/api/synapse/hire', {
|
|
576
576
|
method: 'POST',
|
|
577
577
|
headers: { 'Content-Type': 'application/json' },
|
|
578
|
-
body: JSON.stringify({ specialistId: 'engineering.rapid-prototyper', budgetCapUsd: 2, name: 'Rapid Prototyper' }),
|
|
578
|
+
body: JSON.stringify({ specialistId: 'engineering.rapid-prototyper', budgetCapUsd: 2, name: 'Rapid Prototyper', fireFirstSortie: true }),
|
|
579
579
|
});
|
|
580
580
|
if (res.ok) {
|
|
581
581
|
const d = await res.json();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MemoryEngine } from './memory.js';
|
|
2
2
|
import { PatternRegistry, type PatternCard, type PatternSearchResult } from './pattern-registry.js';
|
|
3
3
|
import { RuntimeRegistry, type RuntimeRegistrySnapshot } from './runtime-registry.js';
|
|
4
|
-
import { RagCollectionStore, type RagCollectionSummary, type RagRetrievalHit } from './rag-collections.js';
|
|
4
|
+
import { RagCollectionStore, type RagCollectionSummary, type RagIngestInput, type RagRetrievalHit } from './rag-collections.js';
|
|
5
5
|
import { TokenSupremacyEngine, type ReadingPlan } from './token-supremacy.js';
|
|
6
6
|
export type KnowledgeSourceClass = 'repo' | 'memory' | 'rag' | 'patterns' | 'runtime';
|
|
7
7
|
export type ModelTier = 'low' | 'high';
|
|
@@ -171,13 +171,7 @@ export declare class KnowledgeFabricEngine {
|
|
|
171
171
|
tags?: string[];
|
|
172
172
|
scope?: 'session' | 'project';
|
|
173
173
|
}): import("./rag-collections.js").RagCollection;
|
|
174
|
-
ingestCollection(collectionId: string, inputs:
|
|
175
|
-
filePath?: string;
|
|
176
|
-
url?: string;
|
|
177
|
-
text?: string;
|
|
178
|
-
label?: string;
|
|
179
|
-
tags?: string[];
|
|
180
|
-
}>): Promise<{
|
|
174
|
+
ingestCollection(collectionId: string, inputs: RagIngestInput[]): Promise<{
|
|
181
175
|
collection: import("./rag-collections.js").RagCollection;
|
|
182
176
|
sourcesAdded: number;
|
|
183
177
|
chunksAdded: number;
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Persistence: SQLite (same pattern as memory.db / graph.db)
|
|
13
13
|
*/
|
|
14
|
+
export declare const NGRAM_WARMUP_MAX_DB_BYTES: number;
|
|
15
|
+
export declare const NGRAM_DEFAULT_ROTATE_BYTES: number;
|
|
16
|
+
export declare const NGRAM_DEFAULT_FOOTPRINT_BYTES: number;
|
|
14
17
|
export declare function getNgramWalPath(dbPath: string): string;
|
|
15
18
|
export declare function getNgramShmPath(dbPath: string): string;
|
|
16
19
|
export declare function getNgramFootprintBytes(dbPath: string): number;
|
|
@@ -147,7 +150,7 @@ export declare class NgramIndex {
|
|
|
147
150
|
optimizeStorage(force?: boolean): void;
|
|
148
151
|
/**
|
|
149
152
|
* Operator-focused maintenance for the on-disk ngram DB.
|
|
150
|
-
* - Bounds runaway DB growth via rotation
|
|
153
|
+
* - Bounds runaway DB growth via rotation, counting the
|
|
151
154
|
* full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
|
|
152
155
|
* - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
|
|
153
156
|
*/
|
|
@@ -45,7 +45,9 @@ function safeStatSize(filePath) {
|
|
|
45
45
|
// wal/shm misses the bug that turned ngram-index.db-wal into 84GB on disk.
|
|
46
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
47
|
const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
48
|
-
const
|
|
48
|
+
export const NGRAM_WARMUP_MAX_DB_BYTES = 200 * 1024 * 1024; // 200 MB
|
|
49
|
+
export const NGRAM_DEFAULT_ROTATE_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
|
|
50
|
+
export const NGRAM_DEFAULT_FOOTPRINT_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
|
|
49
51
|
const NGRAM_DEFAULT_CHECKPOINT_INTERVAL_MS = 30_000;
|
|
50
52
|
const NGRAM_DEFAULT_CHECKPOINT_DOC_COUNT = 200;
|
|
51
53
|
export function getNgramWalPath(dbPath) {
|
|
@@ -199,13 +201,13 @@ export class NgramIndex {
|
|
|
199
201
|
this.warmHashSet();
|
|
200
202
|
try {
|
|
201
203
|
const sizeBytes = safeStatSize(this.dbPath);
|
|
202
|
-
// Only VACUUM on medium-size DBs. Large DBs
|
|
204
|
+
// Only VACUUM on medium-size DBs. Large DBs take too long
|
|
203
205
|
// synchronously — VACUUM rewrites the entire file. Skip here; let
|
|
204
206
|
// periodic maintenance handle it.
|
|
205
|
-
if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <=
|
|
207
|
+
if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <= NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
206
208
|
this.optimizeStorage(true);
|
|
207
209
|
}
|
|
208
|
-
else if (sizeBytes >
|
|
210
|
+
else if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
209
211
|
logNgramNoticeOnce(`ngram:vacuum-skip:${this.dbPath}`, `[NgramIndex] skipping VACUUM on large DB (${Math.round(sizeBytes / 1024 / 1024)}MB) db=${this.dbPath}`);
|
|
210
212
|
}
|
|
211
213
|
}
|
|
@@ -217,17 +219,17 @@ export class NgramIndex {
|
|
|
217
219
|
// Count the full SQLite footprint (db + wal + shm). The 84GB regression
|
|
218
220
|
// happened because a 32MB db file had a 84GB -wal sibling that this
|
|
219
221
|
// routine never inspected.
|
|
220
|
-
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES',
|
|
222
|
+
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
|
|
221
223
|
const dbBytes = safeStatSize(this.dbPath);
|
|
222
224
|
const footprint = getNgramFootprintBytes(this.dbPath);
|
|
223
225
|
if (footprint <= 0 || footprint < rotateBytes)
|
|
224
226
|
return;
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
227
|
+
// The n-gram DB is a rebuildable cache, so oversized rotation drops it by
|
|
228
|
+
// default. Operators can opt into one retained archive for forensics with
|
|
229
|
+
// NEXUS_NGRAM_ARCHIVE_OVERSIZE=1.
|
|
228
230
|
const dir = path.dirname(this.dbPath);
|
|
229
231
|
const base = path.basename(this.dbPath);
|
|
230
|
-
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE
|
|
232
|
+
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
|
|
231
233
|
const existing = fs.existsSync(dir)
|
|
232
234
|
? fs.readdirSync(dir).filter((entry) => entry.startsWith(`${base}.oversize.`)).sort().reverse()
|
|
233
235
|
: [];
|
|
@@ -324,8 +326,8 @@ export class NgramIndex {
|
|
|
324
326
|
// Size guard: SELECT DISTINCT on very large DBs blocks startup under swap pressure.
|
|
325
327
|
try {
|
|
326
328
|
const sizeBytes = fs.statSync(this.dbPath).size;
|
|
327
|
-
if (sizeBytes >
|
|
328
|
-
logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB >
|
|
329
|
+
if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
|
|
330
|
+
logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB > ${Math.round(NGRAM_WARMUP_MAX_DB_BYTES / 1024 / 1024)}MB) db=${this.dbPath}`);
|
|
329
331
|
return;
|
|
330
332
|
}
|
|
331
333
|
}
|
|
@@ -667,17 +669,17 @@ export class NgramIndex {
|
|
|
667
669
|
}
|
|
668
670
|
/**
|
|
669
671
|
* Operator-focused maintenance for the on-disk ngram DB.
|
|
670
|
-
* - Bounds runaway DB growth via rotation
|
|
672
|
+
* - Bounds runaway DB growth via rotation, counting the
|
|
671
673
|
* full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
|
|
672
674
|
* - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
|
|
673
675
|
*/
|
|
674
676
|
maintainBounded(options = {}) {
|
|
675
677
|
const dbBytes = safeStatSize(this.dbPath);
|
|
676
678
|
const footprint = this.getSqliteFootprintBytes();
|
|
677
|
-
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES',
|
|
679
|
+
const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
|
|
678
680
|
const vacuumMaxBytes = readEnvBytes('NEXUS_NGRAM_VACUUM_MAX_BYTES', 256 * 1024 * 1024);
|
|
679
681
|
if (footprint >= rotateBytes && footprint > 0) {
|
|
680
|
-
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE
|
|
682
|
+
const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
|
|
681
683
|
const rotatedPath = `${this.dbPath}.oversize.${Date.now()}`;
|
|
682
684
|
const removeSibling = (suffix) => {
|
|
683
685
|
try {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { MemoryEngine } from './memory.js';
|
|
8
8
|
import { SessionDNAManager } from './session-dna.js';
|
|
9
9
|
import type { ClientRegistry } from './client-registry.js';
|
|
10
|
+
import type { RagIngestInput } from './rag-collections.js';
|
|
10
11
|
import { type ExecutionRun, type ExecutionTask, type SubAgentRuntime } from '../phantom/index.js';
|
|
11
12
|
export * from './orchestrator/types.js';
|
|
12
13
|
import { type Agent, type AutonomyIntent, type BootstrapRequestOptions, type SessionAutonomyState, type SessionBootstrapResult, type OrchestratorOptions } from './orchestrator/types.js';
|
|
@@ -96,13 +97,7 @@ export declare class OrchestratorEngine {
|
|
|
96
97
|
tags?: string[];
|
|
97
98
|
scope?: 'session' | 'project';
|
|
98
99
|
}): import("./rag-collections.js").RagCollection;
|
|
99
|
-
ingestRagCollection(collectionId: string, inputs:
|
|
100
|
-
filePath?: string;
|
|
101
|
-
url?: string;
|
|
102
|
-
text?: string;
|
|
103
|
-
label?: string;
|
|
104
|
-
tags?: string[];
|
|
105
|
-
}>): Promise<{
|
|
100
|
+
ingestRagCollection(collectionId: string, inputs: RagIngestInput[]): Promise<{
|
|
106
101
|
collection: import("./rag-collections.js").RagCollection;
|
|
107
102
|
sourcesAdded: number;
|
|
108
103
|
chunksAdded: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -213,6 +213,7 @@ export declare class NexusPrime {
|
|
|
213
213
|
awaitReady(): Promise<void>;
|
|
214
214
|
getClientRegistry(): ClientRegistry;
|
|
215
215
|
getRuntimeHotAt(): number | null;
|
|
216
|
+
getDashboardAddress(): string | null;
|
|
216
217
|
getSynapse(): SynapseRuntime | null;
|
|
217
218
|
getArchitects(): ArchitectsRuntime | null;
|
|
218
219
|
/**
|
package/dist/index.js
CHANGED
|
@@ -847,6 +847,9 @@ export class NexusPrime {
|
|
|
847
847
|
getRuntimeHotAt() {
|
|
848
848
|
return this.runtimeHotAt;
|
|
849
849
|
}
|
|
850
|
+
getDashboardAddress() {
|
|
851
|
+
return this.dashboardServer.getAddress();
|
|
852
|
+
}
|
|
850
853
|
getSynapse() {
|
|
851
854
|
return this.synapse;
|
|
852
855
|
}
|
|
@@ -1070,7 +1073,7 @@ export class NexusPrime {
|
|
|
1070
1073
|
const bootstrapManifest = ensureBootstrap({
|
|
1071
1074
|
packageRoot: PACKAGE_ROOT,
|
|
1072
1075
|
workspaceRoot: this.getWorkspaceContext().workspaceRoot,
|
|
1073
|
-
phase: '
|
|
1076
|
+
phase: 'install',
|
|
1074
1077
|
silent: true,
|
|
1075
1078
|
});
|
|
1076
1079
|
this.runtime.recordBootstrapManifestStatus?.(bootstrapManifest);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.20",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"test:public": "tsx test/public-surface.test.ts",
|
|
50
50
|
"test:synapse": "tsx src/synapse/__tests__/run.ts",
|
|
51
51
|
"test:architects": "tsx src/architects/__tests__/run.ts",
|
|
52
|
-
"test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/ngram-index.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/dashboard-agent-control.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/install-arch-upgrade.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && tsx test/runtime-lifecycle.test.ts && tsx test/orchestrate-pipeline.test.ts && tsx test/daemon-supervisor.test.ts && tsx test/auto-optimize-tokens.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
|
|
52
|
+
"test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/ngram-index.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/dashboard-agent-control.test.ts && tsx test/dashboard-runtime-adapters.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/install-arch-upgrade.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && tsx test/runtime-lifecycle.test.ts && tsx test/orchestrate-pipeline.test.ts && tsx test/daemon-supervisor.test.ts && tsx test/auto-optimize-tokens.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
|
|
53
53
|
"lint": "eslint src --ext .ts",
|
|
54
54
|
"audit:prod": "npm audit --omit=dev",
|
|
55
55
|
"smoke:release": "tsx scripts/release-smoke.ts",
|