nexus-prime 7.2.0 → 7.3.1

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.
@@ -7,9 +7,14 @@ function parseInteger(value, fallback) {
7
7
  const parsed = Number.parseInt(value ?? '', 10);
8
8
  return Number.isFinite(parsed) ? parsed : fallback;
9
9
  }
10
+ function clampInteger(value, min, max) {
11
+ if (!Number.isFinite(value))
12
+ return min;
13
+ return Math.max(min, Math.min(max, Math.floor(value)));
14
+ }
10
15
  export const ArchitectsConfig = {
11
16
  enabled: parseBoolean(process.env.ARCHITECTS_ENABLED, true),
12
- maxConcurrent: parseInteger(process.env.ARCHITECTS_MAX_CONCURRENT, -1),
17
+ maxConcurrent: clampInteger(parseInteger(process.env.ARCHITECTS_MAX_CONCURRENT, 2), 1, 4),
13
18
  sentinelPatrolMs: parseInteger(process.env.ARCHITECTS_SENTINEL_PATROL_MS, 120_000),
14
19
  wardPatrolMs: parseInteger(process.env.ARCHITECTS_WARD_PATROL_MS, 180_000),
15
20
  convergenceStrategy: (process.env.ARCHITECTS_CONVERGENCE_STRATEGY || 'bisecting'),
@@ -1,4 +1,15 @@
1
1
  import type { StateScope } from '../install/state-locator.js';
2
+ import type { LicenseStatus } from '../licensing/types.js';
3
+ export interface NgramFootprint {
4
+ dbPath: string;
5
+ dbBytes: number;
6
+ walBytes: number;
7
+ shmBytes: number;
8
+ totalBytes: number;
9
+ walLimitBytes: number;
10
+ footprintLimitBytes: number;
11
+ archives: number;
12
+ }
2
13
  export interface DoctorReport {
3
14
  stateDir: string;
4
15
  totals: Record<StateScope, number>;
@@ -15,5 +26,13 @@ export interface DoctorReport {
15
26
  limit: number;
16
27
  }>;
17
28
  suggestCleanup: boolean;
29
+ /** Install manifest's recorded architecture generation, if present. */
30
+ installArchGeneration?: string;
31
+ /** Architecture generation built into this binary. */
32
+ currentArchGeneration: string;
33
+ /** ngram-index footprint snapshot for the canonical state-dir DB. */
34
+ ngram: NgramFootprint;
35
+ /** Active license status. */
36
+ license: Pick<LicenseStatus, 'tier' | 'valid' | 'expiresAt' | 'trial' | 'degradedReason'>;
18
37
  }
19
38
  export declare function runDoctorStorage(): DoctorReport;
@@ -2,13 +2,29 @@
2
2
  * nexus doctor storage — read-only disk usage report.
3
3
  *
4
4
  * Shows scope-by-scope bytes under the state dir + tmp prefixes, flags any
5
- * scope that exceeds the corresponding cleanup budget, and prints a single
6
- * remediation hint (`nexus cleanup --fix`) when needed.
5
+ * scope that exceeds the corresponding cleanup budget, surfaces the install
6
+ * architecture generation + ngram WAL footprint + license status, and prints
7
+ * a single remediation hint (`nexus cleanup --fix`) when needed.
7
8
  */
8
9
  import * as fs from 'fs';
10
+ import * as path from 'path';
9
11
  import { resolveRunsBudget, resolveWorktreeBudget } from './cleanup.js';
10
12
  import { dirBytes, formatBytes } from '../install/fs-purge.js';
11
- import { enumerateStatePaths, getNexusStateDir, getRuntimeTmpRoots, getWorktreeRoots, } from '../install/state-locator.js';
13
+ import { enumerateNgramArchives, enumerateStatePaths, getNexusStateDir, getRuntimeTmpRoots, getWorktreeRoots, } from '../install/state-locator.js';
14
+ import { INSTALL_ARCH_GENERATION, loadManifest, } from '../install/manifest.js';
15
+ import { getNgramFootprintBytes } from '../engines/ngram-index.js';
16
+ import { getSharedLicenseManager } from '../licensing/license-manager.js';
17
+ const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024;
18
+ const NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024;
19
+ function readEnvBytesPositive(name, fallback) {
20
+ const raw = process.env[name];
21
+ if (!raw)
22
+ return fallback;
23
+ const parsed = Number(raw);
24
+ if (!Number.isFinite(parsed) || parsed <= 0)
25
+ return fallback;
26
+ return Math.floor(parsed);
27
+ }
12
28
  export function runDoctorStorage() {
13
29
  const stateDir = getNexusStateDir();
14
30
  const entries = [];
@@ -47,14 +63,67 @@ export function runDoctorStorage() {
47
63
  if (totals.runtime > runsBudget.maxBytes) {
48
64
  flags.push({ scope: 'runtime', reason: 'bytes-over-limit', actual: totals.runtime, limit: runsBudget.maxBytes });
49
65
  }
66
+ const ngramDbPath = path.join(stateDir, 'ngram-index.db');
67
+ const walLimitBytes = readEnvBytesPositive('NEXUS_NGRAM_WAL_LIMIT_BYTES', NGRAM_DEFAULT_WAL_LIMIT_BYTES);
68
+ const footprintLimitBytes = readEnvBytesPositive('NEXUS_NGRAM_MAX_FOOTPRINT_BYTES', NGRAM_DEFAULT_FOOTPRINT_BYTES);
69
+ const safeStat = (p) => {
70
+ try {
71
+ return fs.existsSync(p) ? fs.statSync(p).size : 0;
72
+ }
73
+ catch {
74
+ return 0;
75
+ }
76
+ };
77
+ const ngram = {
78
+ dbPath: ngramDbPath,
79
+ dbBytes: safeStat(ngramDbPath),
80
+ walBytes: safeStat(`${ngramDbPath}-wal`),
81
+ shmBytes: safeStat(`${ngramDbPath}-shm`),
82
+ totalBytes: getNgramFootprintBytes(ngramDbPath),
83
+ walLimitBytes,
84
+ footprintLimitBytes,
85
+ archives: enumerateNgramArchives(stateDir).length,
86
+ };
87
+ if (ngram.walBytes > walLimitBytes) {
88
+ flags.push({ scope: 'db', reason: 'ngram-wal-over-limit', actual: ngram.walBytes, limit: walLimitBytes });
89
+ }
90
+ if (ngram.totalBytes > footprintLimitBytes) {
91
+ flags.push({ scope: 'db', reason: 'ngram-footprint-over-limit', actual: ngram.totalBytes, limit: footprintLimitBytes });
92
+ }
93
+ const manifest = loadManifest();
94
+ const license = (() => {
95
+ try {
96
+ const status = getSharedLicenseManager().getStatus();
97
+ return {
98
+ tier: status.tier,
99
+ valid: status.valid,
100
+ expiresAt: status.expiresAt,
101
+ trial: status.trial,
102
+ degradedReason: status.degradedReason,
103
+ };
104
+ }
105
+ catch {
106
+ return { tier: 'free', valid: false, expiresAt: null, trial: false, degradedReason: 'malformed' };
107
+ }
108
+ })();
50
109
  const report = {
51
110
  stateDir,
52
111
  totals,
53
112
  entries,
54
113
  flags,
55
114
  suggestCleanup: flags.length > 0,
115
+ installArchGeneration: manifest.architectureGeneration,
116
+ currentArchGeneration: INSTALL_ARCH_GENERATION,
117
+ ngram,
118
+ license,
56
119
  };
57
120
  console.log(`\n[doctor:storage] state dir: ${stateDir}`);
121
+ console.log(` install arch ${report.installArchGeneration ?? '(unstamped)'} → current ${report.currentArchGeneration}`);
122
+ const trialNote = license.trial && license.expiresAt
123
+ ? ` (trial, ${Math.max(0, Math.ceil((license.expiresAt - Date.now()) / (24 * 60 * 60 * 1000)))}d left)`
124
+ : '';
125
+ console.log(` license tier=${license.tier} valid=${license.valid}${trialNote}${license.degradedReason ? ` reason=${license.degradedReason}` : ''}`);
126
+ console.log(` ngram db db=${formatBytes(ngram.dbBytes)} wal=${formatBytes(ngram.walBytes)} shm=${formatBytes(ngram.shmBytes)} total=${formatBytes(ngram.totalBytes)} (cap=${formatBytes(footprintLimitBytes)}) archives=${ngram.archives}`);
58
127
  for (const [scope, bytes] of Object.entries(totals)) {
59
128
  console.log(` ${scope.padEnd(10)} ${formatBytes(bytes)}`);
60
129
  }
@@ -4,6 +4,7 @@
4
4
  * Invoked via: nexus-prime setup
5
5
  */
6
6
  import { type IDEId } from '../agents/adapters/ide-compat.js';
7
+ import { INSTALL_ARCH_GENERATION } from '../install/manifest.js';
7
8
  export interface WizardOptions {
8
9
  /** Target workspace root (defaults to cwd) */
9
10
  workspaceRoot?: string;
@@ -36,6 +37,20 @@ export declare function configureIDE(ide: IDEId, opts?: WizardOptions): Promise<
36
37
  configPath: string | null;
37
38
  writtenHash: string | null;
38
39
  }>;
40
+ /** Architecture-generation migration step. Idempotent and best-effort: when
41
+ * the install manifest predates INSTALL_ARCH_GENERATION (or carries an older
42
+ * one), we drop stale generated artefacts that the current generation no
43
+ * longer owns. The manifest's path/registration entries are preserved so
44
+ * uninstall can still reverse them. */
45
+ export interface ArchUpgradeResult {
46
+ previousGeneration?: string;
47
+ currentGeneration: typeof INSTALL_ARCH_GENERATION;
48
+ migrated: boolean;
49
+ ngramArchivesPruned: number;
50
+ }
51
+ export declare function runArchitectureUpgrade(opts?: {
52
+ dryRun?: boolean;
53
+ }): ArchUpgradeResult;
39
54
  /** Run the install wizard: detect IDEs and write MCP configs. */
40
55
  export declare function runInstallWizard(opts?: WizardOptions): Promise<WizardResult>;
41
56
  /** Print how to add nexus-prime manually to an MCP client. */
@@ -4,7 +4,7 @@
4
4
  * Invoked via: nexus-prime setup
5
5
  */
6
6
  import { createHash } from 'crypto';
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
8
8
  import { homedir } from 'os';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { dirname, join, resolve } from 'path';
@@ -15,7 +15,9 @@ import { resolveNexusStateDir } from '../engines/runtime-registry.js';
15
15
  import { resolveWorkspaceContext } from '../engines/workspace-resolver.js';
16
16
  import { runPostinstallCleanup } from '../postinstall/cleanup.js';
17
17
  import { openBrowser, printHandoffBanner, withSpinner } from './interactive-setup.js';
18
- import { appendToManifest, recordPath, recordRegistration, recordSetupMarker } from '../install/manifest.js';
18
+ import { appendToManifest, detectArchitectureUpgrade, loadManifest, recordArchitectureGeneration, recordPath, recordRegistration, recordSetupMarker, INSTALL_ARCH_GENERATION, } from '../install/manifest.js';
19
+ import { enumerateNgramArchives } from '../install/state-locator.js';
20
+ import { NEXUS_HOOK_COMMAND_MARKER, writeNexusClaudeCodeHooks } from '../install/claude-code-hooks.js';
19
21
  /** Compute sha256 of a file's content. Returns null if the file cannot be read. */
20
22
  export function computeFileHash(filePath) {
21
23
  try {
@@ -147,35 +149,8 @@ function _nexusMcpEntry(workspaceRoot) {
147
149
  */
148
150
  function _writeClaudeCodeHooks(workspaceRoot) {
149
151
  const settingsPath = join(workspaceRoot, '.claude', 'settings.json');
150
- let existing = {};
151
- if (existsSync(settingsPath)) {
152
- try {
153
- existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
154
- }
155
- catch { /* start fresh if unreadable */ }
156
- }
157
- const NEXUS_CMD_MARKER = 'nexus-prime hook';
158
- const nexusHooks = {
159
- UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: 'nexus-prime hook bootstrap' }] }],
160
- PreToolUse: [{ matcher: 'Edit|Write|MultiEdit', hooks: [{ type: 'command', command: 'nexus-prime hook mindkit' }] }],
161
- PostToolUse: [{ matcher: 'Edit|Write|MultiEdit|Bash', hooks: [{ type: 'command', command: 'nexus-prime hook memory' }] }],
162
- Stop: [{ matcher: '', hooks: [{ type: 'command', command: 'nexus-prime hook session-dna' }] }],
163
- };
164
- const existingHooks = (existing.hooks && typeof existing.hooks === 'object' && !Array.isArray(existing.hooks))
165
- ? existing.hooks
166
- : {};
167
- for (const [event, entries] of Object.entries(nexusHooks)) {
168
- const current = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
169
- // Strip stale nexus-prime hook entries then append fresh ones
170
- const filtered = current.filter((e) => typeof e === 'object' && e !== null &&
171
- !(Array.isArray(e.hooks) &&
172
- e.hooks.some((h) => typeof h === 'object' && h !== null && typeof h.command === 'string' &&
173
- h.command.includes(NEXUS_CMD_MARKER))));
174
- existingHooks[event] = [...filtered, ...entries];
175
- }
176
- existing.hooks = existingHooks;
177
- mkdirSync(dirname(settingsPath), { recursive: true });
178
- writeFileSync(settingsPath, JSON.stringify(existing, null, 2), 'utf8');
152
+ // Single source of truth: hook spec + dedup logic live in install/claude-code-hooks.ts.
153
+ writeNexusClaudeCodeHooks(settingsPath);
179
154
  const isHome = workspaceRoot === homedir();
180
155
  appendToManifest((m) => {
181
156
  const withPath = recordPath(m, { path: settingsPath, kind: 'file', scope: 'state' });
@@ -183,7 +158,7 @@ function _writeClaudeCodeHooks(workspaceRoot) {
183
158
  target: isHome ? 'claude-home' : 'claude-workspace',
184
159
  filePath: settingsPath,
185
160
  jsonPath: ['hooks'],
186
- entryMarker: NEXUS_CMD_MARKER,
161
+ entryMarker: NEXUS_HOOK_COMMAND_MARKER,
187
162
  });
188
163
  });
189
164
  }
@@ -255,6 +230,45 @@ function _writeWorkspaceLocalConfigs(callerIde, workspaceRoot) {
255
230
  }
256
231
  }
257
232
  }
233
+ export function runArchitectureUpgrade(opts = {}) {
234
+ const manifest = loadManifest();
235
+ const detection = detectArchitectureUpgrade(manifest);
236
+ if (!detection.requiresMigration) {
237
+ if (manifest.architectureGeneration !== INSTALL_ARCH_GENERATION && !opts.dryRun) {
238
+ // Fresh install or no recorded entries — just stamp the marker.
239
+ appendToManifest((m) => recordArchitectureGeneration(m));
240
+ }
241
+ return {
242
+ previousGeneration: detection.previousGeneration,
243
+ currentGeneration: detection.currentGeneration,
244
+ migrated: false,
245
+ ngramArchivesPruned: 0,
246
+ };
247
+ }
248
+ let ngramArchivesPruned = 0;
249
+ if (!opts.dryRun) {
250
+ // Stale ngram archives from prior generations: keep nothing — the new
251
+ // rotation path archives at most one and uses footprint accounting.
252
+ try {
253
+ const archives = enumerateNgramArchives();
254
+ for (const archive of archives) {
255
+ try {
256
+ rmSync(archive.path, { force: true });
257
+ ngramArchivesPruned += 1;
258
+ }
259
+ catch { /* best-effort */ }
260
+ }
261
+ }
262
+ catch { /* best-effort */ }
263
+ appendToManifest((m) => recordArchitectureGeneration(m));
264
+ }
265
+ return {
266
+ previousGeneration: detection.previousGeneration,
267
+ currentGeneration: detection.currentGeneration,
268
+ migrated: true,
269
+ ngramArchivesPruned,
270
+ };
271
+ }
258
272
  /** Run the install wizard: detect IDEs and write MCP configs. */
259
273
  export async function runInstallWizard(opts = {}) {
260
274
  const workspaceRoot = resolve(opts.workspaceRoot ?? process.cwd());
@@ -263,6 +277,10 @@ export async function runInstallWizard(opts = {}) {
263
277
  const setupMarker = readSetupMarker();
264
278
  const log = (msg) => { if (verbose)
265
279
  process.stdout.write(msg + '\n'); };
280
+ const upgrade = runArchitectureUpgrade({ dryRun });
281
+ if (upgrade.migrated && verbose) {
282
+ log(` [arch] migrated install ${upgrade.previousGeneration ?? 'legacy'} → ${upgrade.currentGeneration} (ngram archives pruned: ${upgrade.ngramArchivesPruned})`);
283
+ }
266
284
  const detected = detectInstalledIDEs(workspaceRoot);
267
285
  if (detected.length === 0) {
268
286
  log(' No supported IDEs detected in this workspace. Skipping MCP config setup.');
package/dist/cli.js CHANGED
@@ -38,67 +38,24 @@ import { runUninstall } from './cli/uninstall.js';
38
38
  import { runCleanup } from './cli/cleanup.js';
39
39
  import { runDoctorStorage } from './cli/doctor-storage.js';
40
40
  const tokenEngine = new TokenSupremacyEngine();
41
+ import { getNexusHookSpec, writeNexusClaudeCodeHooks, } from './install/claude-code-hooks.js';
41
42
  /**
42
43
  * Write (or merge) nexus-prime Claude Code hook entries into ~/.claude/settings.json.
43
- * Idempotent: removes any pre-existing `nexus-prime hook *` entries before adding fresh ones.
44
+ * Idempotent delegates to the shared writer in install/claude-code-hooks.ts
45
+ * so cli.ts and install-wizard.ts cannot drift on the hook spec or dedup logic.
44
46
  */
45
47
  function writeClaudeCodeHooks(settingsPath, dryRun) {
46
- let settings = {};
47
- if (existsSync(settingsPath)) {
48
- try {
49
- settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
50
- }
51
- catch {
52
- settings = {};
53
- }
54
- }
55
- const hooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
56
- ? { ...settings.hooks }
57
- : {};
58
- const nexusHooks = {
59
- UserPromptSubmit: [
60
- { hooks: [{ type: 'command', command: 'nexus-prime hook bootstrap', timeout: 30 }] },
61
- ],
62
- PreToolUse: [
63
- {
64
- matcher: 'Edit|Write|MultiEdit',
65
- hooks: [{ type: 'command', command: 'nexus-prime hook mindkit', timeout: 10 }],
66
- },
67
- ],
68
- PostToolUse: [
69
- {
70
- matcher: 'Edit|Write|MultiEdit|Bash',
71
- hooks: [{ type: 'command', command: 'nexus-prime hook memory', timeout: 10 }],
72
- },
73
- ],
74
- Stop: [
75
- { hooks: [{ type: 'command', command: 'nexus-prime hook session-dna', timeout: 60 }] },
76
- ],
77
- };
78
- for (const [event, entries] of Object.entries(nexusHooks)) {
79
- const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
80
- // Remove stale nexus-prime hook entries before re-adding
81
- const filtered = existing.filter((entry) => {
82
- if (!entry || typeof entry !== 'object')
83
- return true;
84
- const hooksList = entry.hooks;
85
- if (!Array.isArray(hooksList))
86
- return true;
87
- return !hooksList.some((h) => h && typeof h.command === 'string' &&
88
- h.command.startsWith('nexus-prime hook'));
89
- });
90
- hooks[event] = [...filtered, ...entries];
91
- }
92
- settings.hooks = hooks;
48
+ const result = writeNexusClaudeCodeHooks(settingsPath, { dryRun });
93
49
  if (dryRun) {
94
50
  console.log('Hooks preview (would write to ~/.claude/settings.json):');
95
- console.log(JSON.stringify(nexusHooks, null, 2));
51
+ console.log(JSON.stringify(getNexusHookSpec(), null, 2));
96
52
  return;
97
53
  }
98
- mkdirSync(dirname(settingsPath), { recursive: true });
99
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
100
- console.log(` Hooks: ${settingsPath}`);
101
- Object.keys(nexusHooks).forEach(ev => console.log(` ${ev} → nexus-prime hook ${ev.toLowerCase().replace('tooluse', '').trim() || ev}`));
54
+ console.log(` Hooks: ${settingsPath}${result.changed ? '' : ' (no change)'}`);
55
+ result.events.forEach((ev) => {
56
+ const tail = ev.toLowerCase().replace('tooluse', '').trim() || ev;
57
+ console.log(` ${ev} → nexus-prime hook ${tail}`);
58
+ });
102
59
  }
103
60
  const program = new Command();
104
61
  let nexus = null;
@@ -1,5 +1,25 @@
1
+ import { DASHBOARD_API_VERSION } from '../contract.js';
2
+ import { INSTALL_ARCH_GENERATION, loadManifest } from '../../install/manifest.js';
1
3
  import { getToolHealthSummary } from '../../agents/adapters/mcp/tool-health.js';
2
4
  export const handleHealthRoutes = async (ctx, req, res, url) => {
5
+ if (req.method === 'GET'
6
+ && (url.pathname === '/api/version' || url.pathname === '/api/dashboard/version')) {
7
+ // Stable version endpoint so external probes (CI smoke, foreign installs
8
+ // sharing port 3377) can identify the running dashboard generation.
9
+ const manifest = (() => { try {
10
+ return loadManifest();
11
+ }
12
+ catch {
13
+ return null;
14
+ } })();
15
+ ctx.respondJson(res, {
16
+ dashboardApiVersion: DASHBOARD_API_VERSION,
17
+ architectureGeneration: INSTALL_ARCH_GENERATION,
18
+ installArchGeneration: manifest?.architectureGeneration ?? null,
19
+ product: 'nexus-prime',
20
+ });
21
+ return true;
22
+ }
3
23
  if (req.method === 'GET' && url.pathname === '/api/license') {
4
24
  const { getSharedLicenseManager } = await import('../../licensing/index.js');
5
25
  ctx.respondJson(res, getSharedLicenseManager().getStatus());
@@ -21,6 +21,7 @@ import { handleAuthoringRoutes } from './routes/authoring.js';
21
21
  import { handleArchitectsRoutes } from './routes/architects.js';
22
22
  import { handleWorkforceRoutes } from './routes/workforce.js';
23
23
  import { handleGraphRoutes } from './routes/graph.js';
24
+ import { DASHBOARD_API_VERSION } from './contract.js';
24
25
  import { SseBroker } from './stream/sse-broker.js';
25
26
  import { nexusEventBus } from '../engines/event-bus.js';
26
27
  const __filename = fileURLToPath(import.meta.url);
@@ -30,7 +31,6 @@ const DEFAULT_PORT = parseInt(process.env.NEXUS_DASHBOARD_PORT || '3377', 10);
30
31
  const MAX_PORT_SCAN = 24;
31
32
  const DASHBOARD_PROBE_TIMEOUT_MS = 3000;
32
33
  const DASHBOARD_PROBE_ATTEMPTS = 2;
33
- const DASHBOARD_API_VERSION = '4';
34
34
  const DASHBOARD_SCHEMA_VERSION = 1;
35
35
  const DASHBOARD_PRETTY_JSON = process.env.NEXUS_DASHBOARD_PRETTY_JSON === '1';
36
36
  const CORE_CAPABILITIES = {
@@ -11,6 +11,9 @@
11
11
  *
12
12
  * Persistence: SQLite (same pattern as memory.db / graph.db)
13
13
  */
14
+ export declare function getNgramWalPath(dbPath: string): string;
15
+ export declare function getNgramShmPath(dbPath: string): string;
16
+ export declare function getNgramFootprintBytes(dbPath: string): number;
14
17
  export interface Posting {
15
18
  docId: string;
16
19
  locMask: number;
@@ -54,6 +57,9 @@ export declare class NgramIndex {
54
57
  private deleteStmt;
55
58
  private lookupStmt;
56
59
  private docExistsStmt;
60
+ private writesSinceCheckpoint;
61
+ private lastCheckpointAt;
62
+ private quotaSkipNoticeShown;
57
63
  private _searchStats;
58
64
  constructor(dbPath?: string, options?: NgramIndexOptions);
59
65
  private rotateOversizeDbIfNeeded;
@@ -64,14 +70,35 @@ export declare class NgramIndex {
64
70
  private warmHashSet;
65
71
  /** Warm the hash set after a deferred init. No-op if already warm. */
66
72
  warmHashSetDeferred(): void;
73
+ /** Run an opportunistic WAL checkpoint. PASSIVE first, escalate to TRUNCATE
74
+ * if the WAL is still over its configured limit. Returns the WAL byte size
75
+ * observed after the attempt so callers can report progress. */
76
+ private checkpointIfNeeded;
77
+ /** Footprint quota check before a write. Returns true if the write should
78
+ * proceed; false if it should be skipped. Throws when NEXUS_NGRAM_STRICT_QUOTA=1
79
+ * so callers can surface hard quota failures during tests/CI. */
80
+ private allowWrite;
67
81
  /** Index a document's text content */
68
82
  addDocument(docId: string, text: string): void;
83
+ /** Trigger a passive checkpoint when either the doc or time interval threshold
84
+ * is hit. Cheap on a healthy DB, prevents WAL growth on bulk indexing. */
85
+ private maybePeriodicCheckpoint;
69
86
  /** Remove a document from the index */
70
87
  removeDocument(docId: string): void;
71
88
  /** Check if a document is already indexed */
72
89
  isIndexed(docId: string): boolean;
73
90
  /** Get count of indexed documents */
74
91
  getDocCount(): number;
92
+ /** Path to the underlying SQLite database file. */
93
+ getDbPath(): string;
94
+ /** Bytes occupied by the .db file alone. */
95
+ getDbBytes(): number;
96
+ /** Bytes occupied by the .db-wal file. */
97
+ getWalBytes(): number;
98
+ /** Bytes occupied by the .db-shm file. */
99
+ getShmBytes(): number;
100
+ /** Total SQLite footprint = db + wal + shm. Use this for quota checks. */
101
+ getSqliteFootprintBytes(): number;
75
102
  /**
76
103
  * Search for documents matching a text query.
77
104
  * Returns candidate document IDs ranked by trigram match count.
@@ -120,8 +147,9 @@ export declare class NgramIndex {
120
147
  optimizeStorage(force?: boolean): void;
121
148
  /**
122
149
  * Operator-focused maintenance for the on-disk ngram DB.
123
- * - Bounds runaway DB growth via rotation (default >= 1GB)
124
- * - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty
150
+ * - Bounds runaway DB growth via rotation (default >= 1GB), counting the
151
+ * full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
152
+ * - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
125
153
  */
126
154
  maintainBounded(options?: {
127
155
  force?: boolean;
@@ -132,7 +160,13 @@ export declare class NgramIndex {
132
160
  uniqueTrigramCount: number;
133
161
  dbSizeBytes: number;
134
162
  };
135
- /** Close the database connection */
163
+ /** Close the database connection.
164
+ *
165
+ * Always truncates the WAL so the .db-wal sibling can never outgrow the
166
+ * configured cap on shutdown — the failure mode that produced the 84GB
167
+ * WAL on a user machine. VACUUM is only run when the operator has both a
168
+ * small DB and explicitly opts in via NEXUS_NGRAM_VACUUM_ON_CLOSE=1, since
169
+ * blind VACUUM on multi-GB DBs blocks shutdown indefinitely. */
136
170
  close(): void;
137
171
  }
138
172
  /** Get or create the shared NgramIndex instance */