memorix 1.9.6 → 1.9.8
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 +18 -0
- package/dist/cli/index.js +254 -87
- package/dist/index.js +254 -87
- 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 +18 -0
- package/dist/sdk.js +253 -86
- package/dist/vector-backfill-runner.js +96 -84
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -1
- package/docs/README.md +1 -1
- package/docs/SETUP.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/hooks/installers/index.ts +160 -3
- package/src/store/orama-store.ts +16 -1
- package/src/store/sqlite-fts.ts +38 -16
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -55,7 +55,7 @@ Generated guidance also has scope:
|
|
|
55
55
|
| Cursor | `memorix setup --agent cursor --global` | Cursor MCP and rules config | Cursor MCP config, `.cursor/rules/memorix.mdc`, skills, and hook guidance | Reload Cursor after setup so it can pick up project config changes. |
|
|
56
56
|
| Grok Build | `memorix setup --agent grok --global` | `~/.grok/hooks/memorix.json` + `~/.grok/AGENTS.md` | Lifecycle hooks on SessionStart, SessionEnd, UserPromptSubmit, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, Stop, and StopFailure, plus Grok project/global guidance | Uses Grok's own JSON hook directory. MCP stays host-owned unless you pass `--mcp http`, which writes `[mcp_servers.memorix]` with `url = "http://localhost:3211/mcp"` in Grok `config.toml`. Global hooks are trusted; project hooks require Grok `/hooks-trust`. Set `GROK_HOME` to use a custom Grok home. The command is `memorix hook --agent grok`. |
|
|
57
57
|
| Gemini CLI | `memorix setup --agent gemini-cli --global` | Gemini CLI extension | Extension under `~/.gemini/extensions/memorix` with MCP, `GEMINI.md` context, hooks, commands, and skills | Antigravity CLI has an official Gemini CLI migration path, but Gemini CLI remains an active standalone Google CLI target. |
|
|
58
|
-
| OpenCode | `memorix setup --agent opencode --global` | OpenCode local plugin file | Global setup writes `~/.config/opencode/plugins/memorix.js`, `~/.config/opencode/opencode.json`, skills, and `AGENTS.md` guidance; repo-local setup writes the `.opencode/` equivalents. |
|
|
58
|
+
| OpenCode | `memorix setup --agent opencode --global` | OpenCode local plugin file | Global setup writes `~/.config/opencode/plugins/memorix.js`, `~/.config/opencode/opencode.json`, skills, and `AGENTS.md` guidance; repo-local setup writes the `.opencode/` equivalents. | The generated plugin keeps the v1 named export and also exposes the v2 default `id`/`setup` contract. v2 event subscriptions use the host session ID when forwarding hooks. |
|
|
59
59
|
| OpenClaw | `memorix setup --agent openclaw --global` | OpenClaw compatible bundle | `~/.openclaw/extensions/memorix` with `.mcp.json`, skills, and OpenClaw `HOOK.md`/`handler.ts` hook pack | Setup attempts `openclaw plugins install <path> --force` and `openclaw hooks enable memorix` when OpenClaw is available. |
|
|
60
60
|
| Hermes Agent | `memorix setup --agent hermes --global` | Hermes plugin | Hermes home (`%LOCALAPPDATA%\hermes` on native Windows, `~/.hermes` elsewhere, or `HERMES_HOME`), plugin enablement in `config.yaml`, and `mcp_servers.memorix` | The plugin registers hooks, a slash command, a CLI command, and skills through Hermes' plugin context. Existing YAML keys are preserved. |
|
|
61
61
|
| Oh-my-Pi | `memorix setup --agent omp --global` | Oh-my-Pi `omp.extensions` package | `.omp/packages/memorix` or `~/.omp/agent/packages/memorix`, plus `.omp/mcp.json` or `~/.omp/agent/mcp.json` | Setup attempts `omp plugin link <path>`. This target uses Oh-my-Pi's package/extension manifest, not Pi's legacy package manifest. |
|
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.8**. 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/docs/SETUP.md
CHANGED
|
@@ -502,7 +502,7 @@ OpenCode can also use a local Memorix plugin generated by setup:
|
|
|
502
502
|
memorix setup --agent opencode --global
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
-
This writes the OpenCode plugin, skills, MCP config, and `AGENTS.md` guidance in the selected scope.
|
|
505
|
+
This writes the OpenCode plugin, skills, MCP config, and `AGENTS.md` guidance in the selected scope. The generated plugin exports both the legacy v1 hook function and the v2 default `id`/`setup` contract, so OpenCode v1 and v2 can load the same file during the migration window.
|
|
506
506
|
|
|
507
507
|
Antigravity setup installs a native plugin at `~/.gemini/config/plugins/memorix` for global setup or `.agents/plugins/memorix` for workspace setup. The plugin includes stdio MCP, official hooks, rules, and skills:
|
|
508
508
|
|
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.8",
|
|
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.8",
|
|
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.8",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
|
@@ -449,8 +449,10 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
|
|
|
449
449
|
* Format: .opencode/plugins/memorix.js — Bun-compatible JS module
|
|
450
450
|
* See: https://opencode.ai/docs/plugins/
|
|
451
451
|
*
|
|
452
|
-
* Plugin
|
|
453
|
-
* -
|
|
452
|
+
* Plugin contracts:
|
|
453
|
+
* - OpenCode v1: named export `MemorixPlugin` returns an event-hook map.
|
|
454
|
+
* - OpenCode v2: default export contains `id` and `setup(ctx)`, with event
|
|
455
|
+
* subscriptions and tool hooks registered through the context.
|
|
454
456
|
* - Return object keys are EVENT NAMES (e.g. "session.created", "file.edited")
|
|
455
457
|
* - Each key maps to an async handler: (input, output) => { ... }
|
|
456
458
|
* - Session/file/command events: handler receives ({ event }) for event-style hooks
|
|
@@ -464,7 +466,7 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
|
|
|
464
466
|
* protocol used by all agents. spawnSync works in both Node.js and Bun
|
|
465
467
|
* runtimes (OpenCode may fall back to Node.js on Windows).
|
|
466
468
|
*/
|
|
467
|
-
const OPENCODE_PLUGIN_VERSION =
|
|
469
|
+
const OPENCODE_PLUGIN_VERSION = 8;
|
|
468
470
|
|
|
469
471
|
const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: string }>> = {
|
|
470
472
|
cursor: { project: path.join('.cursor', 'skills'), global: path.join('.cursor', 'skills') },
|
|
@@ -716,6 +718,161 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
|
|
|
716
718
|
},
|
|
717
719
|
};
|
|
718
720
|
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* OpenCode v2 compatibility layer. Keep this beside the v1 export so one
|
|
724
|
+
* generated file can be loaded by both host generations during migration.
|
|
725
|
+
*/
|
|
726
|
+
const v2HookCommand = ${hookCommand};
|
|
727
|
+
|
|
728
|
+
function v2ReportHookFailure(eventName, detail) {
|
|
729
|
+
if (process.env.MEMORIX_HOOK_DEBUG !== '1') return;
|
|
730
|
+
console.error('[memorix-plugin] v2 hook delivery failed:', eventName, detail);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function v2EventData(event) {
|
|
734
|
+
return event && typeof event.data === 'object' && event.data !== null ? event.data : {};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function v2SessionId(event) {
|
|
738
|
+
const data = v2EventData(event);
|
|
739
|
+
const value = data.sessionID ?? data.sessionId ?? data.session_id
|
|
740
|
+
?? event?.sessionID ?? event?.sessionId;
|
|
741
|
+
return typeof value === 'string' ? value : '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function v2RunHook(payload, directory, sessionId) {
|
|
745
|
+
const nextPayload = { agent: 'opencode', cwd: directory, ...payload };
|
|
746
|
+
if (sessionId) nextPayload.session_id = sessionId;
|
|
747
|
+
const eventName = nextPayload.hook_event_name || 'unknown';
|
|
748
|
+
try {
|
|
749
|
+
const result = spawnSync(v2HookCommand, ['hook'], {
|
|
750
|
+
input: JSON.stringify(nextPayload),
|
|
751
|
+
timeout: 10_000,
|
|
752
|
+
encoding: 'utf-8',
|
|
753
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
754
|
+
shell: process.platform === 'win32',
|
|
755
|
+
windowsHide: true,
|
|
756
|
+
});
|
|
757
|
+
if (result.status !== 0) {
|
|
758
|
+
v2ReportHookFailure(eventName, {
|
|
759
|
+
exit: result.status,
|
|
760
|
+
stderr: (result.stderr || '').slice(0, 200),
|
|
761
|
+
error: result.error?.message,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
v2ReportHookFailure(eventName, error?.message ?? error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function v2SessionState(states, sessionId) {
|
|
770
|
+
const key = sessionId || '__unknown__';
|
|
771
|
+
let state = states.get(key);
|
|
772
|
+
if (!state) {
|
|
773
|
+
state = { pendingText: '', lastDeliveredKey: '' };
|
|
774
|
+
states.set(key, state);
|
|
775
|
+
}
|
|
776
|
+
return state;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function v2FlushAssistant(state, directory, sessionId) {
|
|
780
|
+
const text = state.pendingText.trim();
|
|
781
|
+
if (!text || text === state.lastDeliveredKey) return;
|
|
782
|
+
v2RunHook({ hook_event_name: 'message.updated', ai_response: text }, directory, sessionId);
|
|
783
|
+
state.lastDeliveredKey = text;
|
|
784
|
+
state.pendingText = '';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function v2FilePath(file) {
|
|
788
|
+
if (typeof file === 'string') return file;
|
|
789
|
+
if (!file || typeof file !== 'object') return '';
|
|
790
|
+
return file.path ?? file.file ?? file.name ?? '';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function v2HandleEvent(event, directory, states) {
|
|
794
|
+
if (!event || typeof event !== 'object') return;
|
|
795
|
+
const type = typeof event.type === 'string' ? event.type : '';
|
|
796
|
+
const data = v2EventData(event);
|
|
797
|
+
const sessionId = v2SessionId(event);
|
|
798
|
+
const state = v2SessionState(states, sessionId);
|
|
799
|
+
const run = (payload) => v2RunHook(payload, directory, sessionId);
|
|
800
|
+
|
|
801
|
+
switch (type) {
|
|
802
|
+
case 'session.created':
|
|
803
|
+
run({ hook_event_name: 'session.created' });
|
|
804
|
+
break;
|
|
805
|
+
case 'session.text.delta':
|
|
806
|
+
state.pendingText += typeof data.delta === 'string' ? data.delta : (typeof data.text === 'string' ? data.text : '');
|
|
807
|
+
break;
|
|
808
|
+
case 'session.message.updated':
|
|
809
|
+
case 'message.updated':
|
|
810
|
+
if (data.role === 'assistant' && typeof data.text === 'string') state.pendingText = data.text;
|
|
811
|
+
break;
|
|
812
|
+
case 'session.step.ended': {
|
|
813
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
814
|
+
for (const file of files) {
|
|
815
|
+
const filePath = v2FilePath(file);
|
|
816
|
+
if (filePath) run({ hook_event_name: 'file.edited', file_path: filePath });
|
|
817
|
+
}
|
|
818
|
+
if (typeof data.text === 'string') state.pendingText += data.text;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
case 'session.idle':
|
|
822
|
+
v2FlushAssistant(state, directory, sessionId);
|
|
823
|
+
run({ hook_event_name: 'session.idle' });
|
|
824
|
+
break;
|
|
825
|
+
case 'session.compacted':
|
|
826
|
+
run({ hook_event_name: 'session.compacted' });
|
|
827
|
+
break;
|
|
828
|
+
default:
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function setup(ctx) {
|
|
834
|
+
const directory = ctx?.location?.directory || ctx?.location?.worktree || process.cwd();
|
|
835
|
+
const controller = new AbortController();
|
|
836
|
+
const states = new Map();
|
|
837
|
+
|
|
838
|
+
if (ctx?.tool?.hook) {
|
|
839
|
+
try {
|
|
840
|
+
Promise.resolve(ctx.tool.hook('execute.after', async (event) => {
|
|
841
|
+
const sessionId = v2SessionId(event);
|
|
842
|
+
const data = event && typeof event.input === 'object' ? event.input : {};
|
|
843
|
+
const tool = event?.tool ?? event?.name ?? '';
|
|
844
|
+
v2RunHook({
|
|
845
|
+
hook_event_name: 'tool.execute.after',
|
|
846
|
+
tool_name: tool,
|
|
847
|
+
tool_input: data,
|
|
848
|
+
}, directory, sessionId);
|
|
849
|
+
if (tool === 'shell') {
|
|
850
|
+
v2RunHook({
|
|
851
|
+
hook_event_name: 'command.executed',
|
|
852
|
+
command: data.command ?? data.cmd ?? '',
|
|
853
|
+
}, directory, sessionId);
|
|
854
|
+
}
|
|
855
|
+
})).catch((error) => v2ReportHookFailure('execute.after', error?.message ?? error));
|
|
856
|
+
} catch (error) {
|
|
857
|
+
v2ReportHookFailure('execute.after', error?.message ?? error);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (ctx?.event?.subscribe) {
|
|
862
|
+
void (async () => {
|
|
863
|
+
try {
|
|
864
|
+
const stream = ctx.event.subscribe({ signal: controller.signal });
|
|
865
|
+
for await (const event of stream) v2HandleEvent(event, directory, states);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (!controller.signal.aborted) v2ReportHookFailure('event.subscribe', error?.message ?? error);
|
|
868
|
+
}
|
|
869
|
+
})();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return () => controller.abort();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export default { id: 'memorix', setup };
|
|
719
876
|
`;
|
|
720
877
|
}
|
|
721
878
|
|
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 {
|