memorix 1.9.6 → 1.9.7
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/CHANGELOG.md +11 -0
- package/dist/cli/index.js +98 -86
- package/dist/index.js +98 -86
- package/dist/maintenance-runner.js +96 -84
- package/dist/media-audio-runner.js +96 -84
- package/dist/media-video-runner.js +96 -84
- package/dist/memcode-runtime/CHANGELOG.md +11 -0
- package/dist/sdk.js +97 -85
- package/dist/vector-backfill-runner.js +96 -84
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -1
- package/docs/README.md +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/store/orama-store.ts +16 -1
- package/src/store/sqlite-fts.ts +38 -16
|
@@ -419,6 +419,74 @@ var init_launch_root = __esm({
|
|
|
419
419
|
}
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
+
// src/store/sqlite-reliability.ts
|
|
423
|
+
function isTransientSqliteError(error) {
|
|
424
|
+
const code = error?.code;
|
|
425
|
+
if (typeof code === "string") {
|
|
426
|
+
return /^SQLITE_BUSY(?:_|$)/i.test(code);
|
|
427
|
+
}
|
|
428
|
+
return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
|
|
429
|
+
}
|
|
430
|
+
function isSqliteUnavailableError(error) {
|
|
431
|
+
return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
|
|
432
|
+
}
|
|
433
|
+
function degradedReadError(storeName) {
|
|
434
|
+
return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
|
|
435
|
+
}
|
|
436
|
+
function degradedWriteError(storeName) {
|
|
437
|
+
return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
|
|
438
|
+
}
|
|
439
|
+
function formatSqliteFailure(error) {
|
|
440
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
441
|
+
}
|
|
442
|
+
function delay(ms) {
|
|
443
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
444
|
+
}
|
|
445
|
+
async function retrySqliteBusy(operation, label) {
|
|
446
|
+
for (let attempt = 0; ; attempt++) {
|
|
447
|
+
try {
|
|
448
|
+
return await operation();
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
|
|
451
|
+
const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
|
|
452
|
+
console.error(
|
|
453
|
+
`[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
|
|
454
|
+
);
|
|
455
|
+
await delay(waitMs);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function initializeSqliteStore(factory, dataDir, label) {
|
|
460
|
+
try {
|
|
461
|
+
loadBetterSqlite3();
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw new SqliteUnavailableError(error);
|
|
464
|
+
}
|
|
465
|
+
return retrySqliteBusy(async () => {
|
|
466
|
+
const store = factory();
|
|
467
|
+
await store.init(dataDir);
|
|
468
|
+
return store;
|
|
469
|
+
}, label);
|
|
470
|
+
}
|
|
471
|
+
var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
|
|
472
|
+
var init_sqlite_reliability = __esm({
|
|
473
|
+
"src/store/sqlite-reliability.ts"() {
|
|
474
|
+
"use strict";
|
|
475
|
+
init_esm_shims();
|
|
476
|
+
init_sqlite_db();
|
|
477
|
+
SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
|
|
478
|
+
SqliteUnavailableError = class extends Error {
|
|
479
|
+
code = "MEMORIX_SQLITE_UNAVAILABLE";
|
|
480
|
+
constructor(cause) {
|
|
481
|
+
super(
|
|
482
|
+
cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
|
|
483
|
+
);
|
|
484
|
+
this.name = "SqliteUnavailableError";
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
422
490
|
// src/store/sqlite-fts.ts
|
|
423
491
|
function normalizeFtsToken(token) {
|
|
424
492
|
return token.replace(/"/g, '""');
|
|
@@ -432,22 +500,27 @@ function buildFtsQuery(query) {
|
|
|
432
500
|
function initializeObservationLexicalIndex(db2) {
|
|
433
501
|
const known = ftsAvailability.get(db2);
|
|
434
502
|
if (known !== void 0) return known;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
db2.
|
|
503
|
+
let lastError;
|
|
504
|
+
for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
|
|
505
|
+
try {
|
|
506
|
+
db2.exec(CREATE_OBSERVATION_FTS);
|
|
507
|
+
const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
|
|
508
|
+
const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
|
|
509
|
+
if (observationCount !== indexedCount) {
|
|
510
|
+
db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
|
|
511
|
+
}
|
|
512
|
+
ftsAvailability.set(db2, true);
|
|
513
|
+
return true;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
lastError = error;
|
|
516
|
+
if (!isTransientSqliteError(error) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
|
|
441
517
|
}
|
|
442
|
-
ftsAvailability.set(db2, true);
|
|
443
|
-
return true;
|
|
444
|
-
} catch (error) {
|
|
445
|
-
ftsAvailability.set(db2, false);
|
|
446
|
-
console.warn(
|
|
447
|
-
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error instanceof Error ? error.message : String(error)}`
|
|
448
|
-
);
|
|
449
|
-
return false;
|
|
450
518
|
}
|
|
519
|
+
ftsAvailability.set(db2, false);
|
|
520
|
+
console.warn(
|
|
521
|
+
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
522
|
+
);
|
|
523
|
+
return false;
|
|
451
524
|
}
|
|
452
525
|
function isObservationLexicalIndexEnabled(db2) {
|
|
453
526
|
return ftsAvailability.get(db2) === true;
|
|
@@ -501,11 +574,12 @@ function searchObservationLexically(db2, options) {
|
|
|
501
574
|
score: Math.max(0, Number(row.lexical_score ?? 0))
|
|
502
575
|
}));
|
|
503
576
|
}
|
|
504
|
-
var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS;
|
|
577
|
+
var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS, FTS_INIT_MAX_ATTEMPTS;
|
|
505
578
|
var init_sqlite_fts = __esm({
|
|
506
579
|
"src/store/sqlite-fts.ts"() {
|
|
507
580
|
"use strict";
|
|
508
581
|
init_esm_shims();
|
|
582
|
+
init_sqlite_reliability();
|
|
509
583
|
ftsAvailability = /* @__PURE__ */ new WeakMap();
|
|
510
584
|
FTS_TABLE = "observations_fts";
|
|
511
585
|
CREATE_OBSERVATION_FTS = `
|
|
@@ -539,6 +613,7 @@ CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BE
|
|
|
539
613
|
VALUES (new.id, new.entityName, new.title, new.narrative, new.facts, new.filesModified, new.concepts, new.attachments);
|
|
540
614
|
END;
|
|
541
615
|
`;
|
|
616
|
+
FTS_INIT_MAX_ATTEMPTS = 3;
|
|
542
617
|
}
|
|
543
618
|
});
|
|
544
619
|
|
|
@@ -1726,74 +1801,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
|
|
|
1726
1801
|
}
|
|
1727
1802
|
});
|
|
1728
1803
|
|
|
1729
|
-
// src/store/sqlite-reliability.ts
|
|
1730
|
-
function isTransientSqliteError(error) {
|
|
1731
|
-
const code = error?.code;
|
|
1732
|
-
if (typeof code === "string") {
|
|
1733
|
-
return /^SQLITE_BUSY(?:_|$)/i.test(code);
|
|
1734
|
-
}
|
|
1735
|
-
return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
|
|
1736
|
-
}
|
|
1737
|
-
function isSqliteUnavailableError(error) {
|
|
1738
|
-
return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
|
|
1739
|
-
}
|
|
1740
|
-
function degradedReadError(storeName) {
|
|
1741
|
-
return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
|
|
1742
|
-
}
|
|
1743
|
-
function degradedWriteError(storeName) {
|
|
1744
|
-
return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
|
|
1745
|
-
}
|
|
1746
|
-
function formatSqliteFailure(error) {
|
|
1747
|
-
return error instanceof Error && error.message ? error.message : String(error);
|
|
1748
|
-
}
|
|
1749
|
-
function delay(ms) {
|
|
1750
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1751
|
-
}
|
|
1752
|
-
async function retrySqliteBusy(operation, label) {
|
|
1753
|
-
for (let attempt = 0; ; attempt++) {
|
|
1754
|
-
try {
|
|
1755
|
-
return await operation();
|
|
1756
|
-
} catch (error) {
|
|
1757
|
-
if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
|
|
1758
|
-
const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
|
|
1759
|
-
console.error(
|
|
1760
|
-
`[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
|
|
1761
|
-
);
|
|
1762
|
-
await delay(waitMs);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
async function initializeSqliteStore(factory, dataDir, label) {
|
|
1767
|
-
try {
|
|
1768
|
-
loadBetterSqlite3();
|
|
1769
|
-
} catch (error) {
|
|
1770
|
-
throw new SqliteUnavailableError(error);
|
|
1771
|
-
}
|
|
1772
|
-
return retrySqliteBusy(async () => {
|
|
1773
|
-
const store = factory();
|
|
1774
|
-
await store.init(dataDir);
|
|
1775
|
-
return store;
|
|
1776
|
-
}, label);
|
|
1777
|
-
}
|
|
1778
|
-
var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
|
|
1779
|
-
var init_sqlite_reliability = __esm({
|
|
1780
|
-
"src/store/sqlite-reliability.ts"() {
|
|
1781
|
-
"use strict";
|
|
1782
|
-
init_esm_shims();
|
|
1783
|
-
init_sqlite_db();
|
|
1784
|
-
SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
|
|
1785
|
-
SqliteUnavailableError = class extends Error {
|
|
1786
|
-
code = "MEMORIX_SQLITE_UNAVAILABLE";
|
|
1787
|
-
constructor(cause) {
|
|
1788
|
-
super(
|
|
1789
|
-
cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
|
|
1790
|
-
);
|
|
1791
|
-
this.name = "SqliteUnavailableError";
|
|
1792
|
-
}
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
1804
|
// src/store/file-lock.ts
|
|
1798
1805
|
import { promises as fs2 } from "fs";
|
|
1799
1806
|
import { randomUUID } from "crypto";
|
|
@@ -7686,6 +7693,7 @@ async function searchObservations(options) {
|
|
|
7686
7693
|
};
|
|
7687
7694
|
let queryVector = null;
|
|
7688
7695
|
let embeddingProviderForSearch = null;
|
|
7696
|
+
let embeddingUnavailable = false;
|
|
7689
7697
|
if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
|
|
7690
7698
|
try {
|
|
7691
7699
|
const provider2 = await getEmbeddingProvider();
|
|
@@ -7693,6 +7701,7 @@ async function searchObservations(options) {
|
|
|
7693
7701
|
embeddingProviderForSearch = provider2;
|
|
7694
7702
|
const activeVectorDimensions = getVectorDimensions();
|
|
7695
7703
|
if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
|
|
7704
|
+
embeddingUnavailable = true;
|
|
7696
7705
|
rememberSearchMode(
|
|
7697
7706
|
modeKey,
|
|
7698
7707
|
`fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
|
|
@@ -7732,8 +7741,11 @@ async function searchObservations(options) {
|
|
|
7732
7741
|
hybridWeights: isCJKHeavy ? { text: 0.2, vector: 0.8 } : { text: 0.6, vector: 0.4 }
|
|
7733
7742
|
};
|
|
7734
7743
|
}
|
|
7744
|
+
} else {
|
|
7745
|
+
embeddingUnavailable = true;
|
|
7735
7746
|
}
|
|
7736
7747
|
} catch (error) {
|
|
7748
|
+
embeddingUnavailable = true;
|
|
7737
7749
|
rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
|
|
7738
7750
|
console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
|
|
7739
7751
|
}
|
|
@@ -7741,7 +7753,7 @@ async function searchObservations(options) {
|
|
|
7741
7753
|
mark("preSearch");
|
|
7742
7754
|
let results;
|
|
7743
7755
|
const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
|
|
7744
|
-
const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled);
|
|
7756
|
+
const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled || embeddingUnavailable);
|
|
7745
7757
|
let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch ? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch) : canUsePersistentLexical ? await searchPersistentLexically(options, projectIds, requestLimit) : null;
|
|
7746
7758
|
const durableStore = getOptionalObservationStore();
|
|
7747
7759
|
if (!persistent && queryVector && durableStore?.hasLexicalIndex?.() && count(database) === 0) {
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -13,7 +13,7 @@ Memorix is a TypeScript project built around:
|
|
|
13
13
|
|
|
14
14
|
## Current Development Baseline
|
|
15
15
|
|
|
16
|
-
The current release line is **1.9.
|
|
16
|
+
The current release line is **1.9.7** and package metadata is kept synchronized
|
|
17
17
|
with the root package and MCP Registry manifest.
|
|
18
18
|
|
|
19
19
|
Contributors should assume the following areas are part of the current release line:
|
package/docs/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Historical/deep-reference documents may describe older designs. If they conflict
|
|
|
126
126
|
|
|
127
127
|
## Current Product Line
|
|
128
128
|
|
|
129
|
-
The current product line is **1.9.
|
|
129
|
+
The current product line is **1.9.7**. Multi-device store sync is opt-in and
|
|
130
130
|
keeps local SQLite canonical; existing users do not upload anything until they
|
|
131
131
|
configure a relay provider explicitly.
|
|
132
132
|
The authoritative acceptance contract for the current implementation is
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
3
|
"mcpName": "io.github.AVIDS2/memorix",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.7",
|
|
5
5
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/AVIDS2/memorix",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.9.
|
|
10
|
+
"version": "1.9.7",
|
|
11
11
|
"websiteUrl": "https://mem.rglens.com",
|
|
12
12
|
"icons": [
|
|
13
13
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "memorix",
|
|
25
|
-
"version": "1.9.
|
|
25
|
+
"version": "1.9.7",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
package/src/store/orama-store.ts
CHANGED
|
@@ -909,6 +909,11 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
909
909
|
// Fast-tier queries skip embedding entirely (fulltext is sufficient)
|
|
910
910
|
let queryVector: number[] | null = null;
|
|
911
911
|
let embeddingProviderForSearch: EmbeddingProvider | null = null;
|
|
912
|
+
// Distinct from `embeddingEnabled` (static config): this tracks whether the
|
|
913
|
+
// current query can use embeddings (provider unavailable, incompatible
|
|
914
|
+
// dimensions, TLS, network, or timeout). `canUsePersistentLexical` below
|
|
915
|
+
// must fall back whenever this is true.
|
|
916
|
+
let embeddingUnavailable = false;
|
|
912
917
|
if (quality !== 'fast' && embeddingEnabled && hasQuery && tier !== 'fast') {
|
|
913
918
|
try {
|
|
914
919
|
const provider = await getEmbeddingProvider();
|
|
@@ -916,6 +921,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
916
921
|
embeddingProviderForSearch = provider;
|
|
917
922
|
const activeVectorDimensions = getVectorDimensions();
|
|
918
923
|
if (activeVectorDimensions !== null && provider.dimensions !== activeVectorDimensions) {
|
|
924
|
+
embeddingUnavailable = true;
|
|
919
925
|
rememberSearchMode(
|
|
920
926
|
modeKey,
|
|
921
927
|
`fulltext (embedding dimension mismatch: provider ${provider.dimensions}d vs index ${activeVectorDimensions}d)`,
|
|
@@ -961,9 +967,12 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
961
967
|
: { text: 0.6, vector: 0.4 },
|
|
962
968
|
};
|
|
963
969
|
}
|
|
970
|
+
} else {
|
|
971
|
+
embeddingUnavailable = true;
|
|
964
972
|
}
|
|
965
973
|
} catch (error) {
|
|
966
974
|
// Fallback to fulltext if embedding fails or times out
|
|
975
|
+
embeddingUnavailable = true;
|
|
967
976
|
rememberSearchMode(modeKey, 'fulltext (embedding unavailable)');
|
|
968
977
|
console.error('[memorix] Embedding failed or timed out, falling back to fulltext search');
|
|
969
978
|
}
|
|
@@ -972,8 +981,14 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
972
981
|
mark('preSearch');
|
|
973
982
|
let results: SearchResultLike | Awaited<ReturnType<typeof search>>;
|
|
974
983
|
const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
|
|
984
|
+
// `embeddingEnabled` alone misses the common case: embedding is configured
|
|
985
|
+
// (enabled) but the current provider is unavailable, has incompatible
|
|
986
|
+
// dimensions, or failed at runtime. Without this per-query state, those
|
|
987
|
+
// failures leave queryVector null AND canUsePersistentLexical false, so
|
|
988
|
+
// neither the semantic nor lexical persistent path runs — a silent,
|
|
989
|
+
// deterministic empty result despite the FTS5 index having real matches.
|
|
975
990
|
const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector &&
|
|
976
|
-
(quality === 'fast' || tier === 'fast' || !embeddingEnabled);
|
|
991
|
+
(quality === 'fast' || tier === 'fast' || !embeddingEnabled || embeddingUnavailable);
|
|
977
992
|
let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch
|
|
978
993
|
? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch)
|
|
979
994
|
: canUsePersistentLexical
|
package/src/store/sqlite-fts.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Observation } from '../types.js';
|
|
2
|
+
import { isTransientSqliteError } from './sqlite-reliability.js';
|
|
2
3
|
|
|
3
4
|
/** Rows returned by the derived FTS5 index before normal Observation parsing. */
|
|
4
5
|
export interface SqliteLexicalHit {
|
|
@@ -56,31 +57,52 @@ export function buildFtsQuery(query: string): string | null {
|
|
|
56
57
|
return unique.map((token) => `"${normalizeFtsToken(token)}"`).join(' OR ');
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* initializeObservationLexicalIndex runs exactly once per db handle (called
|
|
62
|
+
* from getDatabase's synchronous setup), so a transient cross-process BUSY
|
|
63
|
+
* here would otherwise be cached as a permanent "unavailable" for that
|
|
64
|
+
* handle's entire lifetime, with no other call site ever re-checking it.
|
|
65
|
+
*/
|
|
66
|
+
const FTS_INIT_MAX_ATTEMPTS = 3;
|
|
67
|
+
|
|
59
68
|
/**
|
|
60
69
|
* Create and reconcile the external-content FTS5 index for one SQLite handle.
|
|
61
|
-
* FTS5 is an optional SQLite capability, so
|
|
62
|
-
* normal fallback rather than a reason to make the durable store
|
|
70
|
+
* FTS5 is an optional SQLite capability, so a genuinely unavailable extension
|
|
71
|
+
* is a normal fallback rather than a reason to make the durable store
|
|
72
|
+
* unusable. Cross-process lock contention (SQLITE_BUSY) is not that case —
|
|
73
|
+
* it is retried a few times, mirroring the retry already applied to the
|
|
74
|
+
* write paths in sqlite-store.ts / session-store.ts / mini-skill-store.ts,
|
|
75
|
+
* since the caller here is synchronous and cannot await retrySqliteBusy.
|
|
63
76
|
*/
|
|
64
77
|
export function initializeObservationLexicalIndex(db: any): boolean {
|
|
65
78
|
const known = ftsAvailability.get(db);
|
|
66
79
|
if (known !== undefined) return known;
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
db.
|
|
81
|
+
let lastError: unknown;
|
|
82
|
+
for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
|
|
83
|
+
try {
|
|
84
|
+
db.exec(CREATE_OBSERVATION_FTS);
|
|
85
|
+
const observationCount = Number(db.prepare('SELECT COUNT(*) AS count FROM observations').get()?.count ?? 0);
|
|
86
|
+
const indexedCount = Number(db.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
|
|
87
|
+
if (observationCount !== indexedCount) {
|
|
88
|
+
db.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
|
|
89
|
+
}
|
|
90
|
+
ftsAvailability.set(db, true);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
if (!isTransientSqliteError(error) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
|
|
95
|
+
// better-sqlite3 is synchronous, so there is no await-able delay here;
|
|
96
|
+
// re-entering db.exec/prepare re-enters SQLite's own busy_timeout wait
|
|
97
|
+
// (already configured in sqlite-db.ts), which is the retry itself.
|
|
74
98
|
}
|
|
75
|
-
ftsAvailability.set(db, true);
|
|
76
|
-
return true;
|
|
77
|
-
} catch (error) {
|
|
78
|
-
ftsAvailability.set(db, false);
|
|
79
|
-
console.warn(
|
|
80
|
-
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
-
);
|
|
82
|
-
return false;
|
|
83
99
|
}
|
|
100
|
+
|
|
101
|
+
ftsAvailability.set(db, false);
|
|
102
|
+
console.warn(
|
|
103
|
+
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
104
|
+
);
|
|
105
|
+
return false;
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
export function isObservationLexicalIndexEnabled(db: any): boolean {
|