gitnexus 1.6.12 → 1.6.13-rc.2
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/README.md +28 -31
- package/dist/cli/analyze.js +7 -24
- package/dist/cli/doctor.js +14 -9
- package/dist/core/lbug/extension-load-error.d.ts +8 -2
- package/dist/core/lbug/extension-load-error.js +23 -16
- package/dist/core/lbug/extension-loader.d.ts +21 -6
- package/dist/core/lbug/extension-loader.js +72 -17
- package/dist/core/lbug/lbug-adapter.d.ts +6 -25
- package/dist/core/lbug/lbug-adapter.js +36 -47
- package/dist/core/lbug/native-check.d.ts +15 -1
- package/dist/core/lbug/native-check.js +45 -22
- package/dist/core/lbug/pool-adapter.js +8 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +32 -1
- package/dist/core/lbug/sidecar-recovery.js +63 -1
- package/dist/core/lbug/vendored-extension-path.d.ts +26 -0
- package/dist/core/lbug/vendored-extension-path.js +98 -0
- package/dist/core/lbug/wal-checkpoint-driver.d.ts +5 -2
- package/dist/core/lbug/wal-checkpoint-driver.js +7 -3
- package/dist/core/run-analyze.d.ts +4 -2
- package/dist/core/run-analyze.js +205 -68
- package/dist/core/search/fts-crash-marker.d.ts +59 -0
- package/dist/core/search/fts-crash-marker.js +55 -0
- package/dist/core/search/fts-indexes.js +2 -2
- package/dist/core/search/fts-policy.d.ts +11 -1
- package/dist/core/search/fts-policy.js +47 -0
- package/dist/core/tree-sitter/vendored-grammars.d.ts +2 -10
- package/dist/core/tree-sitter/vendored-grammars.js +2 -11
- package/dist/core/vendor-root.d.ts +8 -0
- package/dist/core/vendor-root.js +10 -0
- package/dist/mcp/server.js +10 -3
- package/dist/server/api.js +21 -14
- package/dist/storage/repo-meta.d.ts +31 -2
- package/package.json +2 -2
- package/scripts/assert-publish-fts-coverage.cjs +246 -0
- package/scripts/cross-platform-shard.ts +7 -2
- package/scripts/cross-platform-tests.ts +15 -0
- package/scripts/ensure-fts.ts +13 -11
- package/vendor/lbug-fts/LICENSE +27 -0
- package/vendor/lbug-fts/manifest.json +21 -0
- package/vendor/lbug-fts/prebuilds/SHA256SUMS +5 -0
- package/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension +0 -0
- package/web/assets/{agent-DeT_Hy8W.js → agent-DiLVv9Xg.js} +8 -8
- package/web/assets/{index-K4KOdfVv.js → index-coee0RPm.js} +3 -3
- package/web/index.html +1 -1
|
@@ -11,6 +11,8 @@ import { warnIfQueryTextUnbounded } from './query-batch.js';
|
|
|
11
11
|
import { escapeCypherString } from './cypher-escape.js';
|
|
12
12
|
import { withConnLock } from './conn-lock.js';
|
|
13
13
|
import { isWalDriverActive } from './wal-driver-state.js';
|
|
14
|
+
import { loadMeta } from '../../storage/repo-meta.js';
|
|
15
|
+
import { allowsFtsCrashWalPark, hasRecoveredInPlaceFtsAbort } from '../search/fts-crash-marker.js';
|
|
14
16
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
|
|
15
17
|
// Analyze-only, but reached from MCP startup via `pool-adapter.js`. #2802
|
|
16
18
|
// proposed lazy-importing it; rejected — `core/search/bm25-index.ts` statically
|
|
@@ -25,9 +27,10 @@ import { EMBEDDABLE_LABELS } from '../embeddings/types.js';
|
|
|
25
27
|
import { extensionManager, getFtsCapability, resolveAnalyzeInstallPolicy, } from './extension-loader.js';
|
|
26
28
|
// Remedy classification for LOAD failures (#2374/#2383). Pure + node:fs only, so
|
|
27
29
|
// this adds no cycle: `extension-loader.ts` already depends on it.
|
|
28
|
-
import { diagnoseExtensionLoad } from './extension-load-error.js';
|
|
30
|
+
import { diagnoseExtensionLoad, extractExtensionPath } from './extension-load-error.js';
|
|
31
|
+
import { resolveFtsVersionPair } from './vendored-extension-path.js';
|
|
29
32
|
import { classifyDeleteAllError, closeLbugConnection, HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, isDbBusyError, isOpenRetryExhausted, isStorageVersionMismatchError, isWalCorruptionError, throwIfStorageVersionMismatch, bufferPoolExhaustionRemedy, openLbugConnection, sleep, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
30
|
-
import { cleanQuarantinedMissingShadowWals, finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, lbugLockRemediation, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, sidecarPreflightDisabled, } from './sidecar-recovery.js';
|
|
33
|
+
import { assertReadOnlyFtsCrashSafe, cleanQuarantinedMissingShadowWals, finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, lbugLockRemediation, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, sidecarPreflightDisabled, } from './sidecar-recovery.js';
|
|
31
34
|
import { isProcessAlive } from '../../utils/process-identity.js';
|
|
32
35
|
import { logger } from '../logger.js';
|
|
33
36
|
import { SPRING_AUTO_CONFIGURATION_REASONS, SPRING_AUTO_CONFIGURATION_SYNTHETIC_ID_PREFIX, } from '../ingestion/frameworks/spring/auto-configuration.js';
|
|
@@ -196,10 +199,9 @@ const DB_LOCK_RETRY_DELAY_MS = 500;
|
|
|
196
199
|
/**
|
|
197
200
|
* Return true when the error message indicates a write was attempted against
|
|
198
201
|
* a read-only LadybugDB connection. The MCP query pool opens DBs read-only,
|
|
199
|
-
* so any path that calls a `CREATE_*` procedure there will surface this
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* analyze` and either already happened or will happen on the next run.
|
|
202
|
+
* so any path that calls a `CREATE_*` procedure there will surface this.
|
|
203
|
+
* Index creation is owned by `gitnexus analyze` and either already happened
|
|
204
|
+
* or will happen on the next run.
|
|
203
205
|
*/
|
|
204
206
|
export const isReadOnlyDbError = (err) => {
|
|
205
207
|
// Walk the `cause` chain (bounded) so a wrapped read-only error — e.g. the
|
|
@@ -426,8 +428,10 @@ const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
|
|
|
426
428
|
* `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
|
|
427
429
|
* one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
|
|
428
430
|
*/
|
|
429
|
-
const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
|
|
430
|
-
|
|
431
|
+
const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr, crashEvidence) => {
|
|
432
|
+
// Latitude defaults to refusal. Only the analyze writer passes
|
|
433
|
+
// `fts-inplace-checkpointed`; serve never does (R9).
|
|
434
|
+
await guardWalQuarantine(dbPath, mode, triggeringErr, logger, crashEvidence);
|
|
431
435
|
};
|
|
432
436
|
const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
|
|
433
437
|
await refuseLargeWalQuarantine(dbPath, 'read-only', err);
|
|
@@ -454,8 +458,25 @@ const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
|
|
|
454
458
|
throw retryErr;
|
|
455
459
|
}
|
|
456
460
|
};
|
|
461
|
+
const writableFtsCrashWalEvidence = async (dbPath) => {
|
|
462
|
+
try {
|
|
463
|
+
const meta = await loadMeta(path.dirname(dbPath));
|
|
464
|
+
if (meta &&
|
|
465
|
+
(allowsFtsCrashWalPark(meta.incrementalInProgress) ||
|
|
466
|
+
hasRecoveredInPlaceFtsAbort(meta.capabilities?.fts))) {
|
|
467
|
+
return { kind: 'fts-inplace-checkpointed' };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
};
|
|
457
475
|
const reopenWritableAfterMissingShadow = async (dbPath, err) => {
|
|
458
|
-
|
|
476
|
+
// Analyze writers may park a leftover in-place FTS abort WAL. Serve/embed
|
|
477
|
+
// refuse first via assertReadOnlyFtsCrashSafe and must not pass this
|
|
478
|
+
// evidence themselves (R9); read-only reopen never parks.
|
|
479
|
+
await refuseLargeWalQuarantine(dbPath, 'writable', err, await writableFtsCrashWalEvidence(dbPath));
|
|
459
480
|
try {
|
|
460
481
|
await quarantineWalForMissingShadow(dbPath, {
|
|
461
482
|
logger,
|
|
@@ -662,6 +683,7 @@ const doInitLbug = async (dbPath, options = {}) => {
|
|
|
662
683
|
// create databases and don't need the lock.
|
|
663
684
|
// ---------------------------------------------------------------------------
|
|
664
685
|
if (readOnly) {
|
|
686
|
+
await assertReadOnlyFtsCrashSafe(dbPath);
|
|
665
687
|
await preflightLbugSidecars(dbPath, {
|
|
666
688
|
mode: 'read-only',
|
|
667
689
|
logger,
|
|
@@ -2984,8 +3006,8 @@ export const loadVectorExtension = async (targetConn, opts = {}) => {
|
|
|
2984
3006
|
};
|
|
2985
3007
|
/**
|
|
2986
3008
|
* Default stemmer for FTS indexes. Single source so the analyze path
|
|
2987
|
-
* (`getSearchFTSStemmer`) and
|
|
2988
|
-
*
|
|
3009
|
+
* (`getSearchFTSStemmer`) and `createFTSIndex` defaults can never silently
|
|
3010
|
+
* diverge.
|
|
2989
3011
|
*/
|
|
2990
3012
|
export const DEFAULT_FTS_STEMMER = 'porter';
|
|
2991
3013
|
/**
|
|
@@ -3284,41 +3306,6 @@ export const ensureFtsRowDmlSafe = async (indexRows, options = {}) => {
|
|
|
3284
3306
|
return false;
|
|
3285
3307
|
return await loadFTSExtension(undefined, { policy: resolveAnalyzeInstallPolicy() });
|
|
3286
3308
|
};
|
|
3287
|
-
/**
|
|
3288
|
-
* Lazy-create an FTS index, caching the fact in-process.
|
|
3289
|
-
*
|
|
3290
|
-
* Kept for writable maintenance paths that need to lazily materialize an
|
|
3291
|
-
* index. Read-only query paths must not call this; production analysis owns
|
|
3292
|
-
* creating the configured search indexes before the database is served.
|
|
3293
|
-
*
|
|
3294
|
-
* Safe to call repeatedly — the in-process Set guarantees only the first
|
|
3295
|
-
* call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
|
|
3296
|
-
*
|
|
3297
|
-
* Defense in depth: if the active connection is read-only (e.g. the MCP
|
|
3298
|
-
* pool adapter), `CREATE_FTS_INDEX` will fail with "Cannot execute write
|
|
3299
|
-
* operations in a read-only database". Treat that as a no-op and cache
|
|
3300
|
-
* the key so callers don't loop on a path that can never succeed here —
|
|
3301
|
-
* the index is owned by `gitnexus analyze` (writable) and either already
|
|
3302
|
-
* exists or will be created on the next analyze.
|
|
3303
|
-
*/
|
|
3304
|
-
export const ensureFTSIndex = async (tableName, indexName, properties, stemmer = DEFAULT_FTS_STEMMER) => {
|
|
3305
|
-
const key = ftsIndexKey(tableName, indexName);
|
|
3306
|
-
if (ensuredFTSIndexes.has(key))
|
|
3307
|
-
return;
|
|
3308
|
-
try {
|
|
3309
|
-
await createFTSIndex(tableName, indexName, properties, stemmer);
|
|
3310
|
-
}
|
|
3311
|
-
catch (e) {
|
|
3312
|
-
// Read-only DB: writable analyze owns index creation; silently skip
|
|
3313
|
-
// and cache so callers don't loop on a path that can never succeed
|
|
3314
|
-
// here (the MCP query pool opens DBs read-only by design).
|
|
3315
|
-
if (isReadOnlyDbError(e)) {
|
|
3316
|
-
ensuredFTSIndexes.add(key);
|
|
3317
|
-
return;
|
|
3318
|
-
}
|
|
3319
|
-
throw e;
|
|
3320
|
-
}
|
|
3321
|
-
};
|
|
3322
3309
|
/**
|
|
3323
3310
|
* Classify a `QUERY_FTS_INDEX` failure so a genuinely-missing index (normal —
|
|
3324
3311
|
* this table's FTS index hasn't been built yet) is distinguished from a real
|
|
@@ -3541,7 +3528,9 @@ export const dropFTSIndex = async (tableName, indexName) => {
|
|
|
3541
3528
|
// extension binary is not re-inspected, falling back to a fresh structural
|
|
3542
3529
|
// diagnosis when nothing recorded one.
|
|
3543
3530
|
const ftsCapability = getFtsCapability();
|
|
3544
|
-
const
|
|
3531
|
+
const inspectPath = extractExtensionPath(ftsCapability?.reason);
|
|
3532
|
+
const { remedy } = ftsCapability?.diagnosis ??
|
|
3533
|
+
diagnoseExtensionLoad(ftsCapability?.reason, 'FTS', inspectPath, resolveFtsVersionPair(inspectPath));
|
|
3545
3534
|
// Deliberately message-only: `remedy` is generated text (fixed system paths
|
|
3546
3535
|
// at most), and LadybugDB's own path-bearing `reason` is NEVER interpolated
|
|
3547
3536
|
// here — the #2374/#2375 redaction contract.
|
|
@@ -29,7 +29,11 @@ export interface FtsProbeResult {
|
|
|
29
29
|
loaded: boolean;
|
|
30
30
|
/** Collapsed LadybugDB error when `loaded` is false. */
|
|
31
31
|
reason?: string;
|
|
32
|
+
/** Policy `never` refused the probe — not a load failure. */
|
|
33
|
+
suppressed?: boolean;
|
|
32
34
|
}
|
|
35
|
+
export type FtsAvailabilityLabel = 'available' | 'unavailable' | 'suppressed';
|
|
36
|
+
export declare const ftsAvailabilityLabel: (probe: FtsProbeResult) => FtsAvailabilityLabel;
|
|
33
37
|
/** Same shape for every optional extension; `FtsProbeResult` is the legacy name. */
|
|
34
38
|
export type ExtensionProbeResult = FtsProbeResult;
|
|
35
39
|
/**
|
|
@@ -48,7 +52,16 @@ export type ExtensionProbeResult = FtsProbeResult;
|
|
|
48
52
|
* cannot cancel an in-flight native call, so a future thread-blocking case
|
|
49
53
|
* would need an out-of-process probe.
|
|
50
54
|
*/
|
|
51
|
-
|
|
55
|
+
/** Local copy of the env policy parse — must not import extension-loader
|
|
56
|
+
* (that module statically pulls lbug-config, which would break doctor when
|
|
57
|
+
* the native addon is missing). */
|
|
58
|
+
type ProbeInstallPolicy = 'auto' | 'load-only' | 'never';
|
|
59
|
+
export interface FtsProbeOptions {
|
|
60
|
+
policy?: ProbeInstallPolicy;
|
|
61
|
+
/** Injected vendored path. `null` skips path-LOAD; omit to resolve from disk. */
|
|
62
|
+
vendoredPath?: string | null;
|
|
63
|
+
}
|
|
64
|
+
export declare function probeFtsExtensionLoad(timeoutMs?: number, opts?: FtsProbeOptions): Promise<FtsProbeResult>;
|
|
52
65
|
/**
|
|
53
66
|
* Live-probe `LOAD EXTENSION vector`, the VECTOR counterpart of the FTS probe.
|
|
54
67
|
*
|
|
@@ -65,3 +78,4 @@ export declare function probeFtsExtensionLoad(timeoutMs?: number): Promise<FtsPr
|
|
|
65
78
|
* as the FTS one above.
|
|
66
79
|
*/
|
|
67
80
|
export declare function probeVectorExtensionLoad(timeoutMs?: number): Promise<ExtensionProbeResult>;
|
|
81
|
+
export {};
|
|
@@ -2,6 +2,8 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { escapeCypherString } from './cypher-escape.js';
|
|
6
|
+
import { resolveVendoredFtsPath } from './vendored-extension-path.js';
|
|
5
7
|
/** Cap the out-of-process native load probe so a hung filesystem cannot wedge a
|
|
6
8
|
* CLI startup gate (same bounding rationale as the extension probe below). */
|
|
7
9
|
const NATIVE_LOAD_PROBE_TIMEOUT_MS = 15_000;
|
|
@@ -334,6 +336,13 @@ export function glibcTooOldMessage(stderr) {
|
|
|
334
336
|
' - Or use the GitNexus container image, which bundles a current glibc.',
|
|
335
337
|
].join('\n');
|
|
336
338
|
}
|
|
339
|
+
export const ftsAvailabilityLabel = (probe) => {
|
|
340
|
+
if (probe.loaded)
|
|
341
|
+
return 'available';
|
|
342
|
+
if (probe.suppressed)
|
|
343
|
+
return 'suppressed';
|
|
344
|
+
return 'unavailable';
|
|
345
|
+
};
|
|
337
346
|
const DEFAULT_FTS_PROBE_TIMEOUT_MS = 10_000;
|
|
338
347
|
/** Close each result, swallowing close-time errors so a successful LOAD is not
|
|
339
348
|
* misreported as a failure (native-check keeps no static lbug dependency, so it
|
|
@@ -349,24 +358,23 @@ const closeProbeResults = (result) => {
|
|
|
349
358
|
}
|
|
350
359
|
}
|
|
351
360
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return await probeExtensionLoad('fts', timeoutMs);
|
|
361
|
+
const resolveProbeInstallPolicy = () => {
|
|
362
|
+
const raw = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
363
|
+
if (raw === 'load-only' || raw === 'never' || raw === 'auto')
|
|
364
|
+
return raw;
|
|
365
|
+
return 'load-only';
|
|
366
|
+
};
|
|
367
|
+
export async function probeFtsExtensionLoad(timeoutMs = DEFAULT_FTS_PROBE_TIMEOUT_MS, opts) {
|
|
368
|
+
const policy = opts?.policy ?? resolveProbeInstallPolicy();
|
|
369
|
+
if (policy === 'never') {
|
|
370
|
+
return {
|
|
371
|
+
loaded: false,
|
|
372
|
+
suppressed: true,
|
|
373
|
+
reason: 'suppressed by policy GITNEXUS_LBUG_EXTENSION_INSTALL=never',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const vendoredPath = opts?.vendoredPath !== undefined ? opts.vendoredPath : resolveVendoredFtsPath();
|
|
377
|
+
return await probeExtensionLoad('fts', timeoutMs, vendoredPath);
|
|
370
378
|
}
|
|
371
379
|
/**
|
|
372
380
|
* Live-probe `LOAD EXTENSION vector`, the VECTOR counterpart of the FTS probe.
|
|
@@ -389,7 +397,7 @@ export async function probeVectorExtensionLoad(timeoutMs = DEFAULT_FTS_PROBE_TIM
|
|
|
389
397
|
/**
|
|
390
398
|
* Shared LOAD probe. `extension` is a fixed internal literal, never user input.
|
|
391
399
|
*/
|
|
392
|
-
async function probeExtensionLoad(extension, timeoutMs) {
|
|
400
|
+
async function probeExtensionLoad(extension, timeoutMs, vendoredPath) {
|
|
393
401
|
let timer;
|
|
394
402
|
const timeout = new Promise((resolve) => {
|
|
395
403
|
timer = setTimeout(() => resolve({
|
|
@@ -397,6 +405,11 @@ async function probeExtensionLoad(extension, timeoutMs) {
|
|
|
397
405
|
reason: 'probe timed out — extension file or filesystem unresponsive',
|
|
398
406
|
}), timeoutMs);
|
|
399
407
|
});
|
|
408
|
+
const statements = [];
|
|
409
|
+
if (extension === 'fts' && vendoredPath) {
|
|
410
|
+
statements.push(`LOAD EXTENSION '${escapeCypherString(path.resolve(vendoredPath))}'`);
|
|
411
|
+
}
|
|
412
|
+
statements.push(`LOAD EXTENSION ${extension}`);
|
|
400
413
|
const probe = (async () => {
|
|
401
414
|
try {
|
|
402
415
|
const { default: lbug } = await import('@ladybugdb/core');
|
|
@@ -405,9 +418,19 @@ async function probeExtensionLoad(extension, timeoutMs) {
|
|
|
405
418
|
try {
|
|
406
419
|
const conn = new lbug.Connection(db);
|
|
407
420
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
421
|
+
let lastReason;
|
|
422
|
+
for (const sql of statements) {
|
|
423
|
+
try {
|
|
424
|
+
const result = await conn.query(sql);
|
|
425
|
+
closeProbeResults(result);
|
|
426
|
+
return { loaded: true };
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
430
|
+
lastReason = message.replace(/\s+/g, ' ').trim();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { loaded: false, reason: lastReason };
|
|
411
434
|
}
|
|
412
435
|
finally {
|
|
413
436
|
await conn.close().catch(() => { });
|
|
@@ -20,7 +20,7 @@ import { isReadOnlyDbError, loadFTSExtension, loadVectorExtension } from './lbug
|
|
|
20
20
|
import { closeQueryResults } from './query-result-utils.js';
|
|
21
21
|
import { warnIfQueryTextUnbounded } from './query-batch.js';
|
|
22
22
|
import { createLbugDatabase, isWalCorruptionError, sleep, throwIfStorageVersionMismatch, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
23
|
-
import { guardWalQuarantine, isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
23
|
+
import { assertReadOnlyFtsCrashSafe, FtsReaderUnrepairableError, guardWalQuarantine, isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
24
24
|
export async function statDbIdentity(dbPath) {
|
|
25
25
|
try {
|
|
26
26
|
const s = await fs.stat(dbPath);
|
|
@@ -439,6 +439,9 @@ async function tryQuarantineForMissingShadow(dbPath, opts) {
|
|
|
439
439
|
// refuseLargeWalQuarantine (issue #2382 review, Finding B). Kept OUTSIDE the
|
|
440
440
|
// try so the actionable recovery message propagates to the MCP caller rather
|
|
441
441
|
// than being re-wrapped as a rename failure.
|
|
442
|
+
// Never pass crash evidence: the pool is a reader/MCP surface and must
|
|
443
|
+
// keep today's large-WAL refusal (R9). Analyze parks via the dirty-recovery
|
|
444
|
+
// family before it opens.
|
|
442
445
|
await guardWalQuarantine(dbPath, opts.reason, opts.err, poolSidecarLogger);
|
|
443
446
|
try {
|
|
444
447
|
const quarantinePath = await quarantineWalForMissingShadow(dbPath, {
|
|
@@ -502,6 +505,7 @@ async function openReadOnlyDatabase(dbPath) {
|
|
|
502
505
|
let db;
|
|
503
506
|
silenceStdout();
|
|
504
507
|
try {
|
|
508
|
+
await assertReadOnlyFtsCrashSafe(dbPath);
|
|
505
509
|
await preflightLbugSidecars(dbPath, {
|
|
506
510
|
mode: 'read-only',
|
|
507
511
|
logger: poolSidecarLogger,
|
|
@@ -706,6 +710,9 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
706
710
|
// Not retryable: the on-disk file's storage version doesn't change
|
|
707
711
|
// on its own. Fail immediately with an actionable message.
|
|
708
712
|
throwIfStorageVersionMismatch(lastError);
|
|
713
|
+
if (lastError instanceof FtsReaderUnrepairableError) {
|
|
714
|
+
throw lastError;
|
|
715
|
+
}
|
|
709
716
|
if (isWalCorruptionError(lastError)) {
|
|
710
717
|
try {
|
|
711
718
|
const db = await tryQuarantineAndReopen(dbPath, repoId);
|
|
@@ -25,6 +25,31 @@ export interface SidecarRecoveryLogger {
|
|
|
25
25
|
debug?: (message: string) => void;
|
|
26
26
|
}
|
|
27
27
|
export declare const TINY_ORPHAN_WAL_BYTES: number;
|
|
28
|
+
/**
|
|
29
|
+
* Analyze-writer crash evidence for WAL quarantine latitude (KTD5).
|
|
30
|
+
* `mode` is a warning label only and must not carry this. Omit on serve
|
|
31
|
+
* and the MCP pool — those keep today's large-WAL refusal.
|
|
32
|
+
*/
|
|
33
|
+
export type WalCrashEvidence = {
|
|
34
|
+
readonly kind: 'fts-inplace-checkpointed';
|
|
35
|
+
};
|
|
36
|
+
export declare const CLEAN_LBUG_SIDECARS_COMMAND = "gitnexus clean --lbug-sidecars";
|
|
37
|
+
export declare const FTS_READER_REPAIR_COMMAND = "gitnexus analyze --repair-fts";
|
|
38
|
+
export declare const ftsReaderRefuseMessage: (dbPath: string) => string;
|
|
39
|
+
export declare class FtsReaderUnrepairableError extends Error {
|
|
40
|
+
readonly code: "FTS_READER_UNREPAIRABLE";
|
|
41
|
+
constructor(dbPath: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Advisory reader gate (KTD10 / R9b). When meta names an in-place FTS
|
|
45
|
+
* abort (live dirty flag, or a persisted in-place `native-abort`) and a
|
|
46
|
+
* WAL is still live, refuse before the native open. Does not write,
|
|
47
|
+
* rename, or repair. Parking still requires the conjunctive
|
|
48
|
+
* `allowsFtsCrashWalPark` warrant. Missing or unreadable meta falls
|
|
49
|
+
* through to today's open path.
|
|
50
|
+
*/
|
|
51
|
+
export declare const assertReadOnlyFtsCrashSafe: (dbPath: string) => Promise<void>;
|
|
52
|
+
export declare const ftsCrashParkFailureMessage: (failedPath: string, err?: unknown) => string;
|
|
28
53
|
export declare const isMissingFsError: (err: unknown) => boolean;
|
|
29
54
|
export declare const sidecarPreflightDisabled: () => boolean;
|
|
30
55
|
export declare const statIfExists: (filePath: string) => Promise<{
|
|
@@ -96,7 +121,7 @@ export declare function inspectLbugSidecars(dbPath: string): Promise<LbugSidecar
|
|
|
96
121
|
* the existing recovery path is safe to proceed. `mode` is a label used only in
|
|
97
122
|
* the warning text (e.g. 'read-only', 'writable', 'pool read-only recovery').
|
|
98
123
|
*/
|
|
99
|
-
export declare const guardWalQuarantine: (dbPath: string, mode: string, triggeringErr: unknown, logger: SidecarRecoveryLogger) => Promise<void>;
|
|
124
|
+
export declare const guardWalQuarantine: (dbPath: string, mode: string, triggeringErr: unknown, logger: SidecarRecoveryLogger, crashEvidence?: WalCrashEvidence) => Promise<void>;
|
|
100
125
|
export declare function quarantineWalForMissingShadow(dbPath: string, options: {
|
|
101
126
|
logger: SidecarRecoveryLogger;
|
|
102
127
|
level?: 'debug' | 'info' | 'warn';
|
|
@@ -175,6 +200,12 @@ export declare function finalizeLbugSidecarsAfterClose(dbPath: string, options:
|
|
|
175
200
|
* every subsequent open this run performs is replay-free — or the entry is
|
|
176
201
|
* in `failed` and the caller MUST abort before any DB open.
|
|
177
202
|
*
|
|
203
|
+
* Retention: these parks are not reclaimed on the next writable open
|
|
204
|
+
* (unlike missing-shadow quarantines). FTS-phase parks stay until
|
|
205
|
+
* `gitnexus clean --lbug-sidecars` or the next park overwrites the same
|
|
206
|
+
* fixed `.dirty-recovery` name. That is intentional — the parked bytes
|
|
207
|
+
* are the only forensic copy of a proven in-place abort.
|
|
208
|
+
*
|
|
178
209
|
* @returns `moved` — destination paths now holding the parked bytes;
|
|
179
210
|
* `removed` — source sidecars whose bytes are GONE (forensics lost, replay
|
|
180
211
|
* risk eliminated); `failed` — source sidecars still in place: a
|
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { loadMeta } from '../../storage/repo-meta.js';
|
|
4
|
+
import { shouldRefuseFtsCrashWal } from '../search/fts-crash-marker.js';
|
|
3
5
|
import { HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, sleep, } from './lbug-config.js';
|
|
4
6
|
export const TINY_ORPHAN_WAL_BYTES = 4 * 1024;
|
|
7
|
+
export const CLEAN_LBUG_SIDECARS_COMMAND = 'gitnexus clean --lbug-sidecars';
|
|
8
|
+
export const FTS_READER_REPAIR_COMMAND = 'gitnexus analyze --repair-fts';
|
|
9
|
+
export const ftsReaderRefuseMessage = (dbPath) => `Cannot open ${path.basename(dbPath)} read-only after an in-place FTS abort. ` +
|
|
10
|
+
`The leftover WAL would replay and kill this process. ` +
|
|
11
|
+
`Run \`${FTS_READER_REPAIR_COMMAND}\` after stopping any GitNexus MCP or serve process.`;
|
|
12
|
+
export class FtsReaderUnrepairableError extends Error {
|
|
13
|
+
code = 'FTS_READER_UNREPAIRABLE';
|
|
14
|
+
constructor(dbPath) {
|
|
15
|
+
super(ftsReaderRefuseMessage(dbPath));
|
|
16
|
+
this.name = 'FtsReaderUnrepairableError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const sidecarHasLiveWal = (state) => state.kind === 'orphan-wal' ||
|
|
20
|
+
state.kind === 'tiny-orphan-wal' ||
|
|
21
|
+
state.kind === 'wal-with-shadow';
|
|
22
|
+
/**
|
|
23
|
+
* Advisory reader gate (KTD10 / R9b). When meta names an in-place FTS
|
|
24
|
+
* abort (live dirty flag, or a persisted in-place `native-abort`) and a
|
|
25
|
+
* WAL is still live, refuse before the native open. Does not write,
|
|
26
|
+
* rename, or repair. Parking still requires the conjunctive
|
|
27
|
+
* `allowsFtsCrashWalPark` warrant. Missing or unreadable meta falls
|
|
28
|
+
* through to today's open path.
|
|
29
|
+
*/
|
|
30
|
+
export const assertReadOnlyFtsCrashSafe = async (dbPath) => {
|
|
31
|
+
let meta;
|
|
32
|
+
try {
|
|
33
|
+
meta = await loadMeta(path.dirname(dbPath));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!meta || !shouldRefuseFtsCrashWal(meta.incrementalInProgress, meta.capabilities?.fts)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const state = await inspectLbugSidecars(dbPath);
|
|
42
|
+
if (!sidecarHasLiveWal(state))
|
|
43
|
+
return;
|
|
44
|
+
throw new FtsReaderUnrepairableError(dbPath);
|
|
45
|
+
};
|
|
46
|
+
export const ftsCrashParkFailureMessage = (failedPath, err) => {
|
|
47
|
+
const detail = err instanceof Error ? err.message : err != null ? String(err) : '';
|
|
48
|
+
return (`Cannot park ${path.basename(failedPath)} after an in-place FTS abort` +
|
|
49
|
+
(detail ? ` (${detail})` : '') +
|
|
50
|
+
`. The database was not opened. Run \`${CLEAN_LBUG_SIDECARS_COMMAND}\` ` +
|
|
51
|
+
'after stopping any GitNexus MCP or serve process, then retry ' +
|
|
52
|
+
'`gitnexus analyze` or `gitnexus analyze --repair-fts`.');
|
|
53
|
+
};
|
|
5
54
|
/**
|
|
6
55
|
* Counter-based warn anti-spam (PR #1747 review, Finding 6).
|
|
7
56
|
*
|
|
@@ -251,7 +300,7 @@ export async function inspectLbugSidecars(dbPath) {
|
|
|
251
300
|
* the existing recovery path is safe to proceed. `mode` is a label used only in
|
|
252
301
|
* the warning text (e.g. 'read-only', 'writable', 'pool read-only recovery').
|
|
253
302
|
*/
|
|
254
|
-
export const guardWalQuarantine = async (dbPath, mode, triggeringErr, logger) => {
|
|
303
|
+
export const guardWalQuarantine = async (dbPath, mode, triggeringErr, logger, crashEvidence) => {
|
|
255
304
|
const state = await inspectLbugSidecars(dbPath);
|
|
256
305
|
if (state.kind === 'wal-with-shadow') {
|
|
257
306
|
warnOnce(logger, `${dbPath}:present-shadow-refuse:${mode}`, `GitNexus: refusing to quarantine WAL at ${dbPath}.wal during ${mode} recovery — ` +
|
|
@@ -260,6 +309,13 @@ export const guardWalQuarantine = async (dbPath, mode, triggeringErr, logger) =>
|
|
|
260
309
|
throw new Error(presentShadowUnreachableMessage(dbPath, triggeringErr));
|
|
261
310
|
}
|
|
262
311
|
if (state.kind === 'orphan-wal') {
|
|
312
|
+
if (crashEvidence?.kind === 'fts-inplace-checkpointed') {
|
|
313
|
+
const { failed } = await quarantineSidecarsForDirtyRecovery(dbPath, (message) => logger.warn(message));
|
|
314
|
+
if (failed.length > 0) {
|
|
315
|
+
throw new Error(ftsCrashParkFailureMessage(failed[0]));
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
263
319
|
warnOnce(logger, `${dbPath}:large-wal-refuse:${mode}`, `GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
|
|
264
320
|
'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
|
|
265
321
|
throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
|
|
@@ -449,6 +505,12 @@ const dirtyRecoveryParkedNames = (dbPath) => DIRTY_RECOVERY_SIDECAR_SUFFIXES.fla
|
|
|
449
505
|
* every subsequent open this run performs is replay-free — or the entry is
|
|
450
506
|
* in `failed` and the caller MUST abort before any DB open.
|
|
451
507
|
*
|
|
508
|
+
* Retention: these parks are not reclaimed on the next writable open
|
|
509
|
+
* (unlike missing-shadow quarantines). FTS-phase parks stay until
|
|
510
|
+
* `gitnexus clean --lbug-sidecars` or the next park overwrites the same
|
|
511
|
+
* fixed `.dirty-recovery` name. That is intentional — the parked bytes
|
|
512
|
+
* are the only forensic copy of a proven in-place abort.
|
|
513
|
+
*
|
|
452
514
|
* @returns `moved` — destination paths now holding the parked bytes;
|
|
453
515
|
* `removed` — source sidecars whose bytes are GONE (forensics lost, replay
|
|
454
516
|
* risk eliminated); `failed` — source sidecars still in place: a
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const defaultVendorRoot: () => string;
|
|
2
|
+
export declare const nodePlatformTuple: (platform?: NodeJS.Platform, arch?: string) => string;
|
|
3
|
+
export interface FtsArtifactManifest {
|
|
4
|
+
coreVersion?: string;
|
|
5
|
+
extensionVersion?: string;
|
|
6
|
+
filename?: string;
|
|
7
|
+
unsupportedTuples?: Array<{
|
|
8
|
+
tuple: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare const readFtsArtifactManifest: (vendorRoot?: string) => FtsArtifactManifest;
|
|
13
|
+
export declare const isUnsupportedFtsTuple: (tuple: string, vendorRoot?: string) => boolean;
|
|
14
|
+
/** Relative-path containment — not a prefix match (rejects `vendor-evil`). */
|
|
15
|
+
export declare const isPathInsideRoot: (root: string, candidate: string) => boolean;
|
|
16
|
+
export declare const validateVendoredExtensionPath: (candidate: string, vendorRoot: string) => string | null;
|
|
17
|
+
export declare const inferExtensionVersionFromPath: (filePath: string | null | undefined) => string | undefined;
|
|
18
|
+
export declare const resolveFtsVersionPair: (inspectPath?: string | null, vendorRoot?: string) => {
|
|
19
|
+
expected?: string;
|
|
20
|
+
found?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const resolveVendoredFtsPath: (opts?: {
|
|
23
|
+
tuple?: string;
|
|
24
|
+
vendorRoot?: string;
|
|
25
|
+
filename?: string;
|
|
26
|
+
}) => string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { VENDOR_ROOT } from '../vendor-root.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the packaged FTS extension for this process's Node platform tuple.
|
|
6
|
+
*
|
|
7
|
+
* Lives here (not in extension-loader) so the doctor startup probe can share
|
|
8
|
+
* the same path without importing the loader, which statically pulls lbug-config.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_FILENAME = 'libfts.lbug_extension';
|
|
11
|
+
export const defaultVendorRoot = () => VENDOR_ROOT;
|
|
12
|
+
export const nodePlatformTuple = (platform = process.platform, arch = process.arch) => `${platform}-${arch}`;
|
|
13
|
+
const asOptionalString = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
14
|
+
/** Drop non-array / non-object entries so `.some(entry => entry.tuple)` cannot throw. */
|
|
15
|
+
const asUnsupportedTuples = (value) => {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return undefined;
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const entry of value) {
|
|
20
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
21
|
+
continue;
|
|
22
|
+
const tuple = entry.tuple;
|
|
23
|
+
if (typeof tuple !== 'string' || tuple.length === 0)
|
|
24
|
+
continue;
|
|
25
|
+
const reason = entry.reason;
|
|
26
|
+
entries.push({
|
|
27
|
+
tuple,
|
|
28
|
+
...(typeof reason === 'string' ? { reason } : {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
};
|
|
33
|
+
export const readFtsArtifactManifest = (vendorRoot = defaultVendorRoot()) => {
|
|
34
|
+
const manifestPath = path.join(vendorRoot, 'lbug-fts', 'manifest.json');
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
37
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
38
|
+
const rec = parsed;
|
|
39
|
+
return {
|
|
40
|
+
coreVersion: asOptionalString(rec.coreVersion),
|
|
41
|
+
extensionVersion: asOptionalString(rec.extensionVersion),
|
|
42
|
+
filename: asOptionalString(rec.filename),
|
|
43
|
+
unsupportedTuples: asUnsupportedTuples(rec.unsupportedTuples),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export const isUnsupportedFtsTuple = (tuple, vendorRoot = defaultVendorRoot()) => (readFtsArtifactManifest(vendorRoot).unsupportedTuples ?? []).some((entry) => entry.tuple === tuple);
|
|
53
|
+
/** Relative-path containment — not a prefix match (rejects `vendor-evil`). */
|
|
54
|
+
export const isPathInsideRoot = (root, candidate) => {
|
|
55
|
+
const relative = path.relative(root, candidate);
|
|
56
|
+
if (path.isAbsolute(relative))
|
|
57
|
+
return false;
|
|
58
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..';
|
|
59
|
+
};
|
|
60
|
+
export const validateVendoredExtensionPath = (candidate, vendorRoot) => {
|
|
61
|
+
let realFile;
|
|
62
|
+
let realRoot;
|
|
63
|
+
try {
|
|
64
|
+
realFile = realpathSync(candidate);
|
|
65
|
+
realRoot = realpathSync(vendorRoot);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!/\.lbug_extension$/i.test(realFile))
|
|
71
|
+
return null;
|
|
72
|
+
if (!isPathInsideRoot(realRoot, realFile))
|
|
73
|
+
return null;
|
|
74
|
+
return realFile;
|
|
75
|
+
};
|
|
76
|
+
export const inferExtensionVersionFromPath = (filePath) => {
|
|
77
|
+
if (!filePath)
|
|
78
|
+
return undefined;
|
|
79
|
+
const home = /[/\\]extension[/\\](\d+\.\d+\.\d+)[/\\]/.exec(filePath);
|
|
80
|
+
return home?.[1];
|
|
81
|
+
};
|
|
82
|
+
export const resolveFtsVersionPair = (inspectPath, vendorRoot) => {
|
|
83
|
+
// Ladybug's home path is `~/.lbdb/extension/<coreVersion>/…`. Compare that
|
|
84
|
+
// directory to the packaged core pin, not the (often different) artifact
|
|
85
|
+
// version, or a matching runtime looks skewed.
|
|
86
|
+
const expected = readFtsArtifactManifest(vendorRoot).coreVersion;
|
|
87
|
+
const found = inferExtensionVersionFromPath(inspectPath);
|
|
88
|
+
return { expected, found };
|
|
89
|
+
};
|
|
90
|
+
export const resolveVendoredFtsPath = (opts) => {
|
|
91
|
+
const vendorRoot = opts?.vendorRoot ?? defaultVendorRoot();
|
|
92
|
+
const tuple = opts?.tuple ?? nodePlatformTuple();
|
|
93
|
+
const filename = opts?.filename ?? readFtsArtifactManifest(vendorRoot).filename ?? DEFAULT_FILENAME;
|
|
94
|
+
const candidate = path.resolve(vendorRoot, 'lbug-fts', 'prebuilds', tuple, filename);
|
|
95
|
+
if (!existsSync(candidate))
|
|
96
|
+
return null;
|
|
97
|
+
return validateVendoredExtensionPath(candidate, vendorRoot);
|
|
98
|
+
};
|
|
@@ -65,9 +65,12 @@ export declare const runCheckpointWithRetry: (options?: {
|
|
|
65
65
|
*
|
|
66
66
|
* Honors the `GITNEXUS_WAL_MANUAL_CHECKPOINT=0` opt-out so operators can
|
|
67
67
|
* disable the manual path if it ever interacts badly with a future
|
|
68
|
-
* Ladybug release.
|
|
68
|
+
* Ladybug release. Returns true only when a CHECKPOINT actually flushed
|
|
69
|
+
* (`tryFlushWAL` → true). Opt-out and a no-op flush (no open connection)
|
|
70
|
+
* both return false so an FTS park warrant cannot treat a skipped
|
|
71
|
+
* checkpoint as success.
|
|
69
72
|
*/
|
|
70
|
-
export declare const checkpointOnce: () => Promise<
|
|
73
|
+
export declare const checkpointOnce: () => Promise<boolean>;
|
|
71
74
|
/**
|
|
72
75
|
* Start a periodic manual checkpoint driver. The returned handle has a
|
|
73
76
|
* `stop()` method that resolves once the in-flight checkpoint (if any)
|
|
@@ -108,12 +108,16 @@ export const runCheckpointWithRetry = async (options = {}) => {
|
|
|
108
108
|
*
|
|
109
109
|
* Honors the `GITNEXUS_WAL_MANUAL_CHECKPOINT=0` opt-out so operators can
|
|
110
110
|
* disable the manual path if it ever interacts badly with a future
|
|
111
|
-
* Ladybug release.
|
|
111
|
+
* Ladybug release. Returns true only when a CHECKPOINT actually flushed
|
|
112
|
+
* (`tryFlushWAL` → true). Opt-out and a no-op flush (no open connection)
|
|
113
|
+
* both return false so an FTS park warrant cannot treat a skipped
|
|
114
|
+
* checkpoint as success.
|
|
112
115
|
*/
|
|
113
116
|
export const checkpointOnce = async () => {
|
|
114
117
|
if (!isManualCheckpointEnabled())
|
|
115
|
-
return;
|
|
116
|
-
await runCheckpointWithRetry();
|
|
118
|
+
return false;
|
|
119
|
+
const { flushed } = await runCheckpointWithRetry();
|
|
120
|
+
return flushed;
|
|
117
121
|
};
|
|
118
122
|
/** Default cadence (ms) for the periodic driver. */
|
|
119
123
|
const DEFAULT_PERIOD_MS = 5_000;
|
|
@@ -228,8 +228,10 @@ export interface AnalyzeResult {
|
|
|
228
228
|
* `extension-unavailable` (the LadybugDB FTS extension could not load — the
|
|
229
229
|
* offline-first case, remedied by installing it) vs `build-failed` (the
|
|
230
230
|
* extension loaded but the index build/verify failed non-fatally — remedied by
|
|
231
|
-
* `--repair-fts`, not by installing the extension)
|
|
232
|
-
*
|
|
231
|
+
* `--repair-fts`, not by installing the extension) vs `native-abort` (inferred
|
|
232
|
+
* on the next run from an FTS-phase crash) vs `tuple-missing` (no packaged
|
|
233
|
+
* artifact for this platform). Lets the CLI show the correct recovery hint
|
|
234
|
+
* instead of always blaming a missing extension.
|
|
233
235
|
* `disabled-by-flag` and `disabled-by-env` record intentional opt-out;
|
|
234
236
|
* neither calls for extension installation or repair.
|
|
235
237
|
*/
|