nexus-prime 7.9.19 → 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.
@@ -976,11 +976,10 @@ export function buildMcpToolDefinitions() {
976
976
  required: ['taskId', 'goal', 'findings'],
977
977
  },
978
978
  },
979
- // When NEXUS_DISABLE_WORKFORCE=1 (the daemon default since v7.6.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. Set NEXUS_DISABLE_WORKFORCE=0 to
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: 'runtime',
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: http://localhost:${dashboardPort}`);
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
- // Default the daemon to "workforce-disabled" mode: Synapse + Architects
824
- // engines stay in the source tree (tests still run, code is preserved)
825
- // but they don't open their SQLite DBs, run schema migrations, or
826
- // surface their 24 MCP tools to clients. Saves ~700ms boot, shrinks
827
- // the model-visible tool catalog, and removes the empty Workforce
828
- // surface from the dashboard. Re-enable with NEXUS_DISABLE_WORKFORCE=0
829
- // for users who actually hire operatives + dispatch missions.
830
- if (process.env.NEXUS_DISABLE_WORKFORCE === undefined) {
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);
@@ -9,6 +9,7 @@ export interface DaemonHealthResponse {
9
9
  stateKey: string;
10
10
  workspaceRoot: string;
11
11
  repoRoot: string;
12
+ dashboardUrl?: string | null;
12
13
  startedAt: number;
13
14
  }
14
15
  export interface DaemonStatus {
@@ -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;
@@ -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: path.basename(this.repoRoot) || 'workspace',
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];
@@ -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 (default >= 1GB), counting the
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 NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024; // 512 MB
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 (>200MB) take too long
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 <= 200 * 1024 * 1024) {
207
+ if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <= NGRAM_WARMUP_MAX_DB_BYTES) {
206
208
  this.optimizeStorage(true);
207
209
  }
208
- else if (sizeBytes > 200 * 1024 * 1024) {
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', 1024 * 1024 * 1024); // 1GB default
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
- // Keep only the most recent oversize archive to bound disk usage. When the
226
- // operator opts out of archiving (ARCHIVE_OVERSIZE=0), drop the leftovers
227
- // outright instead of growing a backlog of multi-GB carcasses.
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 !== '0';
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 > 200 * 1024 * 1024) {
328
- logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB > 200MB) db=${this.dbPath}`);
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 (default >= 1GB), counting the
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', 1024 * 1024 * 1024);
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 !== '0';
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 {
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: 'runtime',
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.19",
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",