ihow-memory 0.1.0-alpha.0

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/dist/core.js ADDED
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import { ensureWorkspace, resolveWorkspace } from './workspace.js';
3
+ import { readMemoryFile } from './store/files.js';
4
+ import { durablePromoteCandidate, promoteCandidate, writeCandidate } from './governance.js';
5
+ import { countIndexedDocuments } from './engine/fts.js';
6
+ import { engineStatus, indexWithEngineFallback, resolveEngineConfig, searchWithEngineFallback } from './engine/retrieval.js';
7
+ function excerpt(content, max = 300) {
8
+ const compact = content.replace(/\s+/g, ' ').trim();
9
+ return compact.length > max ? `${compact.slice(0, max - 3)}...` : compact;
10
+ }
11
+ export async function openCore(options = {}) {
12
+ const workspace = await ensureWorkspace(resolveWorkspace(options));
13
+ const engineConfig = resolveEngineConfig(options);
14
+ return {
15
+ workspace,
16
+ async search (query, opts = {}) {
17
+ if (typeof query !== 'string' || !query.trim()) return [];
18
+ return (await searchWithEngineFallback(workspace, engineConfig, query, {
19
+ limit: opts.limit
20
+ })).hits;
21
+ },
22
+ async read (ref) {
23
+ const result = await readMemoryFile(workspace, ref);
24
+ const snippet = excerpt(result.content);
25
+ return {
26
+ path: result.path,
27
+ content: result.content,
28
+ snippet,
29
+ source: 'markdown',
30
+ citation: {
31
+ path: result.path,
32
+ snippet
33
+ }
34
+ };
35
+ },
36
+ async write_candidate (payload) {
37
+ const result = await writeCandidate(workspace, payload);
38
+ await indexWithEngineFallback(workspace, engineConfig);
39
+ return result;
40
+ },
41
+ async promote (candidate, target = {}) {
42
+ const result = await promoteCandidate(workspace, candidate, target);
43
+ await indexWithEngineFallback(workspace, engineConfig);
44
+ return result;
45
+ },
46
+ async durable_promote (candidate, promoteOptions) {
47
+ const result = await durablePromoteCandidate(workspace, candidate, promoteOptions);
48
+ if (result.status === 'promoted') await indexWithEngineFallback(workspace, engineConfig);
49
+ return result;
50
+ },
51
+ async status () {
52
+ const exists = fs.existsSync(workspace.indexPath);
53
+ const documents = await countIndexedDocuments(workspace);
54
+ const providerStatus = await engineStatus(workspace, engineConfig);
55
+ return {
56
+ ok: true,
57
+ workspace: {
58
+ root: workspace.root,
59
+ space: workspace.space,
60
+ path: workspace.spaceDir,
61
+ mode: workspace.mode,
62
+ memoryRoot: workspace.memoryDir
63
+ },
64
+ index: {
65
+ path: workspace.indexPath,
66
+ manifestPath: workspace.indexManifestPath,
67
+ providerId: providerStatus.provider.id,
68
+ status: exists ? 'ready' : 'missing',
69
+ documents,
70
+ lastError: providerStatus.manifestLastError
71
+ },
72
+ provider: providerStatus.provider,
73
+ sync: {
74
+ enabled: false
75
+ }
76
+ };
77
+ },
78
+ async rebuild () {
79
+ return await indexWithEngineFallback(workspace, engineConfig);
80
+ }
81
+ };
82
+ }
83
+
84
+
85
+ //# sourceURL=core.ts
@@ -0,0 +1,210 @@
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { listMarkdownFiles } from '../store/files.js';
6
+ import { withWorkspaceLock } from '../store/lock.js';
7
+ import { relativeToSpace } from '../workspace.js';
8
+ import { defaultFtsManifest, writeProviderManifest } from './manifest.js';
9
+ const BUSY_TIMEOUT_MS = 10000;
10
+ const requireBuiltin = createRequire(import.meta.url);
11
+ let databaseSyncConstructor;
12
+ function sqliteErrorMessage(error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ return message.replace(/\s+/g, ' ').slice(0, 300);
15
+ }
16
+ export function loadDatabaseSync() {
17
+ if (databaseSyncConstructor) return databaseSyncConstructor;
18
+ try {
19
+ const sqlite = requireBuiltin('node:sqlite');
20
+ if (!sqlite.DatabaseSync) throw new Error('DatabaseSync export missing');
21
+ databaseSyncConstructor = sqlite.DatabaseSync;
22
+ return databaseSyncConstructor;
23
+ } catch (error) {
24
+ throw new Error(`sqlite_unavailable:${sqliteErrorMessage(error)}`);
25
+ }
26
+ }
27
+ export function sqliteRuntimeStatus() {
28
+ try {
29
+ loadDatabaseSync();
30
+ return {
31
+ ok: true,
32
+ detail: 'node:sqlite DatabaseSync available'
33
+ };
34
+ } catch (error) {
35
+ return {
36
+ ok: false,
37
+ detail: sqliteErrorMessage(error)
38
+ };
39
+ }
40
+ }
41
+ function openDatabase(workspace, opts = {}) {
42
+ fs.mkdirSync(path.dirname(workspace.indexPath), {
43
+ recursive: true
44
+ });
45
+ const DatabaseSync = loadDatabaseSync();
46
+ const db = new DatabaseSync(workspace.indexPath, {
47
+ timeout: BUSY_TIMEOUT_MS
48
+ });
49
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS};`);
50
+ if (opts.initialize !== false) {
51
+ db.exec('PRAGMA journal_mode = WAL;');
52
+ db.exec('PRAGMA synchronous = NORMAL;');
53
+ db.exec(`
54
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts
55
+ USING fts5(path UNINDEXED, content, tokenize = 'unicode61');
56
+ `);
57
+ }
58
+ return db;
59
+ }
60
+ function cjkSegment(text) {
61
+ if (typeof text !== 'string' || !text) return text;
62
+ return text.replace(/[㐀-鿿豈-﫿]/g, (char)=>` ${char} `);
63
+ }
64
+ function queryToFts(query) {
65
+ const terms = cjkSegment(query).match(/[\p{L}\p{N}_-]+/gu) || [];
66
+ if (terms.length === 0) return '""';
67
+ return terms.slice(0, 12).map((term)=>`"${term.replace(/"/g, '""')}"`).join(' OR ');
68
+ }
69
+ async function collectDocuments(workspace) {
70
+ const files = await listMarkdownFiles(workspace.memoryDir);
71
+ const documents = [];
72
+ for (const filePath of files){
73
+ const relative = relativeToSpace(workspace, filePath);
74
+ if (relative.startsWith('memory/candidate/')) continue;
75
+ if (relative.startsWith('memory/_mcp/_events/')) continue;
76
+ if (relative.startsWith('memory/_mcp/history/')) continue;
77
+ const content = await fsp.readFile(filePath, 'utf8');
78
+ documents.push({
79
+ path: relative,
80
+ content
81
+ });
82
+ }
83
+ return documents;
84
+ }
85
+ async function rebuildFtsIndexUnlocked(workspace) {
86
+ const documents = await collectDocuments(workspace);
87
+ const db = openDatabase(workspace);
88
+ try {
89
+ db.exec('BEGIN');
90
+ db.exec('DELETE FROM memory_fts');
91
+ const insert = db.prepare('INSERT INTO memory_fts(path, content) VALUES (?, ?)');
92
+ for (const document of documents){
93
+ insert.run(document.path, cjkSegment(document.content));
94
+ }
95
+ db.exec('COMMIT');
96
+ } catch (error) {
97
+ try {
98
+ db.exec('ROLLBACK');
99
+ } catch {}
100
+ throw error;
101
+ } finally{
102
+ db.close();
103
+ }
104
+ await writeProviderManifest(workspace, defaultFtsManifest('ready'));
105
+ return documents.length;
106
+ }
107
+ async function hasUsableIndex(workspace) {
108
+ if (!fs.existsSync(workspace.indexPath)) return false;
109
+ const db = openDatabase(workspace, {
110
+ initialize: false
111
+ });
112
+ try {
113
+ db.prepare('SELECT rowid FROM memory_fts LIMIT 1').all();
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ } finally{
118
+ db.close();
119
+ }
120
+ }
121
+ async function ensureFtsIndex(workspace) {
122
+ if (await hasUsableIndex(workspace)) return;
123
+ await withWorkspaceLock(workspace, async ()=>{
124
+ if (await hasUsableIndex(workspace)) return;
125
+ await rebuildFtsIndexUnlocked(workspace);
126
+ });
127
+ }
128
+ export async function rebuildFtsIndex(workspace) {
129
+ return await withWorkspaceLock(workspace, async ()=>await rebuildFtsIndexUnlocked(workspace));
130
+ }
131
+ export async function searchFts(workspace, query, opts = {}) {
132
+ if (opts.rebuild === true) {
133
+ await rebuildFtsIndex(workspace);
134
+ } else {
135
+ await ensureFtsIndex(workspace);
136
+ }
137
+ const limit = Math.max(1, Math.min(Number(opts.limit || 5), 25));
138
+ const db = openDatabase(workspace, {
139
+ initialize: false
140
+ });
141
+ try {
142
+ const rows = db.prepare(`
143
+ SELECT
144
+ path,
145
+ snippet(memory_fts, 1, '[', ']', '...', 24) AS snippet,
146
+ bm25(memory_fts) AS rank
147
+ FROM memory_fts
148
+ WHERE memory_fts MATCH ?
149
+ ORDER BY rank
150
+ LIMIT ?
151
+ `).all(queryToFts(query), limit);
152
+ return rows.map((row)=>({
153
+ path: row.path,
154
+ snippet: row.snippet,
155
+ score: Number(row.rank),
156
+ source: 'fts',
157
+ citation: {
158
+ path: row.path,
159
+ snippet: row.snippet
160
+ }
161
+ }));
162
+ } finally{
163
+ db.close();
164
+ }
165
+ }
166
+ export async function countIndexedDocuments(workspace) {
167
+ if (!fs.existsSync(workspace.indexPath)) return 0;
168
+ let db;
169
+ try {
170
+ db = openDatabase(workspace, {
171
+ initialize: false
172
+ });
173
+ } catch {
174
+ return 0;
175
+ }
176
+ try {
177
+ const row = db.prepare('SELECT count(*) AS count FROM memory_fts').get();
178
+ return Number(row.count || 0);
179
+ } catch {
180
+ return 0;
181
+ } finally{
182
+ db.close();
183
+ }
184
+ }
185
+ export const ftsEngine = {
186
+ id: 'fts',
187
+ capabilities: {
188
+ lexical: true,
189
+ semantic: false
190
+ },
191
+ async index (workspace) {
192
+ return {
193
+ indexed: await rebuildFtsIndex(workspace)
194
+ };
195
+ },
196
+ async search (workspace, query, opts = {}) {
197
+ return await searchFts(workspace, query, opts);
198
+ },
199
+ async status () {
200
+ return {
201
+ id: 'fts',
202
+ model: null,
203
+ ready: true,
204
+ cloud: false
205
+ };
206
+ }
207
+ };
208
+
209
+
210
+ //# sourceURL=engine/fts.ts
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs/promises';
2
+ export function defaultFtsManifest(status = 'ready') {
3
+ return {
4
+ providerId: 'fts',
5
+ modelId: null,
6
+ dims: null,
7
+ createdAt: new Date().toISOString(),
8
+ corpusFingerprint: null,
9
+ status,
10
+ ready: status === 'ready',
11
+ cloud: false,
12
+ activeProviderId: 'fts',
13
+ providers: {
14
+ fts: {
15
+ id: 'fts',
16
+ model: null,
17
+ ready: status === 'ready',
18
+ cloud: false,
19
+ capabilities: {
20
+ lexical: true,
21
+ semantic: false
22
+ }
23
+ }
24
+ }
25
+ };
26
+ }
27
+ export async function readProviderManifest(workspace) {
28
+ try {
29
+ return JSON.parse(await fs.readFile(workspace.indexManifestPath, 'utf8'));
30
+ } catch (error) {
31
+ if (error.code === 'ENOENT') return null;
32
+ return null;
33
+ }
34
+ }
35
+ export async function writeProviderManifest(workspace, manifest) {
36
+ const existing = await readProviderManifest(workspace);
37
+ await fs.writeFile(workspace.indexManifestPath, `${JSON.stringify({
38
+ createdAt: existing?.createdAt || manifest.createdAt || new Date().toISOString(),
39
+ ...manifest,
40
+ updatedAt: new Date().toISOString()
41
+ }, null, 2)}\n`, 'utf8');
42
+ }
43
+
44
+
45
+ //# sourceURL=engine/manifest.ts
@@ -0,0 +1,324 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { countIndexedDocuments, ftsEngine } from './fts.js';
3
+ import { readProviderManifest, writeProviderManifest } from './manifest.js';
4
+ const DEFAULT_VECTOR_TIMEOUT_MS = 1500;
5
+ function stringEnv(name) {
6
+ const value = process.env[name];
7
+ return value && value.trim() ? value.trim() : undefined;
8
+ }
9
+ function parseTimeout(value) {
10
+ const parsed = Number(value);
11
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_VECTOR_TIMEOUT_MS;
12
+ return Math.max(100, Math.min(parsed, 30000));
13
+ }
14
+ function requestedEngineId(options) {
15
+ const requested = options.engine || stringEnv('IHOW_MEMORY_ENGINE') || stringEnv('IHOW_MEMORY_PROVIDER') || 'fts';
16
+ const normalized = requested.trim().toLowerCase();
17
+ if ([
18
+ 'vector',
19
+ 'semantic',
20
+ 'vector-process',
21
+ 'vector-gguf'
22
+ ].includes(normalized)) return 'vector-gguf';
23
+ return normalized || 'fts';
24
+ }
25
+ export function resolveEngineConfig(options = {}) {
26
+ return {
27
+ requestedId: requestedEngineId(options),
28
+ vectorProviderCommand: options.vectorProviderCommand || stringEnv('IHOW_MEMORY_VECTOR_PROVIDER_COMMAND'),
29
+ vectorModel: options.vectorModel || stringEnv('IHOW_MEMORY_VECTOR_MODEL') || null || undefined,
30
+ vectorTimeoutMs: parseTimeout(options.vectorTimeoutMs || stringEnv('IHOW_MEMORY_VECTOR_TIMEOUT_MS'))
31
+ };
32
+ }
33
+ function safeErrorMessage(error) {
34
+ const raw = error instanceof Error ? error.message : String(error);
35
+ return raw.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, '$1[redacted]').replace(/\b(sk-[A-Za-z0-9_-]{8,})\b/g, '[redacted]').replace(/\b(token|password|secret|api[_-]?key)=\S+/gi, '$1=[redacted]').slice(0, 500);
36
+ }
37
+ function splitCommand(input) {
38
+ return input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part)=>part.replace(/^["']|["']$/g, '')) || [];
39
+ }
40
+ class VectorProcessEngine {
41
+ id = 'vector-gguf';
42
+ capabilities = {
43
+ lexical: false,
44
+ semantic: true
45
+ };
46
+ config;
47
+ constructor(config){
48
+ this.config = config;
49
+ }
50
+ async index(workspace) {
51
+ return await this.callProvider('index', workspace);
52
+ }
53
+ async search(workspace, query, opts = {}) {
54
+ const result = await this.callProvider('search', workspace, {
55
+ query,
56
+ opts
57
+ });
58
+ const hits = result.hits || result.results || [];
59
+ return hits.map((hit)=>({
60
+ ...hit,
61
+ source: hit.source || this.id,
62
+ citation: hit.citation || {
63
+ path: hit.path,
64
+ snippet: hit.snippet
65
+ }
66
+ }));
67
+ }
68
+ async status(workspace) {
69
+ const status = await this.callProvider('status', workspace);
70
+ return {
71
+ id: String(status.id || this.id),
72
+ model: typeof status.model === 'string' ? status.model : this.config.vectorModel || null,
73
+ ready: status.ready === true,
74
+ cloud: status.cloud === true,
75
+ lastError: typeof status.lastError === 'string' ? status.lastError : undefined
76
+ };
77
+ }
78
+ async callProvider(method, workspace, payload = {}) {
79
+ if (!this.config.vectorProviderCommand) throw new Error('vector_provider_unconfigured');
80
+ const parts = splitCommand(this.config.vectorProviderCommand);
81
+ const [command, ...baseArgs] = parts;
82
+ if (!command) throw new Error('vector_provider_unconfigured');
83
+ const request = {
84
+ method,
85
+ workspace: {
86
+ root: workspace.root,
87
+ space: workspace.space,
88
+ memoryDir: workspace.memoryDir,
89
+ indexPath: workspace.indexPath,
90
+ indexManifestPath: workspace.indexManifestPath
91
+ },
92
+ provider: {
93
+ id: this.id,
94
+ model: this.config.vectorModel || null
95
+ },
96
+ ...payload
97
+ };
98
+ return await new Promise((resolve, reject)=>{
99
+ const child = spawn(command, [
100
+ ...baseArgs,
101
+ method
102
+ ], {
103
+ stdio: [
104
+ 'pipe',
105
+ 'pipe',
106
+ 'pipe'
107
+ ]
108
+ });
109
+ let stdout = '';
110
+ let stderr = '';
111
+ const timer = setTimeout(()=>{
112
+ child.kill('SIGTERM');
113
+ reject(new Error(`vector_provider_timeout:${method}`));
114
+ }, this.config.vectorTimeoutMs);
115
+ child.stdout.on('data', (chunk)=>{
116
+ stdout += String(chunk);
117
+ });
118
+ child.stderr.on('data', (chunk)=>{
119
+ stderr += String(chunk);
120
+ });
121
+ child.on('error', (error)=>{
122
+ clearTimeout(timer);
123
+ reject(error);
124
+ });
125
+ child.on('close', (code)=>{
126
+ clearTimeout(timer);
127
+ if (code !== 0) {
128
+ reject(new Error(`vector_provider_exit_${code}:${stderr.trim() || method}`));
129
+ return;
130
+ }
131
+ try {
132
+ resolve(JSON.parse(stdout.trim() || '{}'));
133
+ } catch {
134
+ reject(new Error(`vector_provider_invalid_json:${method}`));
135
+ }
136
+ });
137
+ child.stdin.end(`${JSON.stringify(request)}\n`);
138
+ });
139
+ }
140
+ }
141
+ function isVectorRequested(config) {
142
+ return config.requestedId === 'vector-gguf';
143
+ }
144
+ function vectorEngine(config) {
145
+ return new VectorProcessEngine(config);
146
+ }
147
+ async function writeReadyManifest(workspace, status, documents) {
148
+ await writeProviderManifest(workspace, {
149
+ providerId: status.id,
150
+ modelId: status.model,
151
+ dims: null,
152
+ createdAt: new Date().toISOString(),
153
+ corpusFingerprint: null,
154
+ status: status.ready ? 'ready' : 'error',
155
+ ready: status.ready,
156
+ cloud: status.cloud,
157
+ activeProviderId: status.id,
158
+ lastError: status.lastError,
159
+ providers: {
160
+ fts: {
161
+ id: 'fts',
162
+ model: null,
163
+ ready: documents === undefined ? true : documents >= 0,
164
+ cloud: false,
165
+ capabilities: ftsEngine.capabilities
166
+ },
167
+ [status.id]: {
168
+ id: status.id,
169
+ model: status.model,
170
+ ready: status.ready,
171
+ cloud: status.cloud,
172
+ lastError: status.lastError,
173
+ capabilities: {
174
+ semantic: true
175
+ }
176
+ }
177
+ }
178
+ });
179
+ }
180
+ async function writeFallbackManifest(workspace, fallback) {
181
+ const documents = await countIndexedDocuments(workspace);
182
+ await writeProviderManifest(workspace, {
183
+ providerId: 'fts',
184
+ modelId: null,
185
+ dims: null,
186
+ createdAt: new Date().toISOString(),
187
+ corpusFingerprint: null,
188
+ status: 'fallback',
189
+ ready: true,
190
+ cloud: false,
191
+ activeProviderId: 'fts',
192
+ fallbackFrom: fallback.from,
193
+ fallbackTo: fallback.to,
194
+ lastError: fallback.reason,
195
+ providers: {
196
+ fts: {
197
+ id: 'fts',
198
+ model: null,
199
+ ready: true,
200
+ cloud: false,
201
+ capabilities: ftsEngine.capabilities
202
+ },
203
+ [fallback.from]: {
204
+ id: fallback.from,
205
+ model: null,
206
+ ready: false,
207
+ cloud: false,
208
+ lastError: fallback.reason,
209
+ capabilities: {
210
+ semantic: true
211
+ }
212
+ }
213
+ }
214
+ });
215
+ if (documents < 0) {
216
+ throw new Error('unreachable_index_document_count');
217
+ }
218
+ }
219
+ export async function indexWithEngineFallback(workspace, config) {
220
+ const ftsIndexed = (await ftsEngine.index(workspace)).indexed;
221
+ if (!isVectorRequested(config)) return ftsIndexed;
222
+ try {
223
+ const requested = vectorEngine(config);
224
+ const status = await requested.status(workspace);
225
+ if (!status.ready) throw new Error(status.lastError || 'vector_provider_not_ready');
226
+ await requested.index(workspace);
227
+ await writeReadyManifest(workspace, status, ftsIndexed);
228
+ return ftsIndexed;
229
+ } catch (error) {
230
+ await writeFallbackManifest(workspace, {
231
+ from: 'vector-gguf',
232
+ to: 'fts',
233
+ reason: safeErrorMessage(error)
234
+ });
235
+ return ftsIndexed;
236
+ }
237
+ }
238
+ export async function searchWithEngineFallback(workspace, config, query, opts = {}) {
239
+ if (!isVectorRequested(config)) {
240
+ return {
241
+ hits: await ftsEngine.search(workspace, query, opts)
242
+ };
243
+ }
244
+ const requested = vectorEngine(config);
245
+ try {
246
+ const status = await requested.status(workspace);
247
+ if (!status.ready) throw new Error(status.lastError || 'vector_provider_not_ready');
248
+ const hits = await requested.search(workspace, query, opts);
249
+ await writeReadyManifest(workspace, status);
250
+ return {
251
+ hits
252
+ };
253
+ } catch (error) {
254
+ const fallback = {
255
+ from: requested.id,
256
+ to: 'fts',
257
+ reason: safeErrorMessage(error)
258
+ };
259
+ const hits = await ftsEngine.search(workspace, query, opts);
260
+ await writeFallbackManifest(workspace, fallback);
261
+ return {
262
+ hits: hits.map((hit)=>({
263
+ ...hit,
264
+ fallback
265
+ })),
266
+ fallback
267
+ };
268
+ }
269
+ }
270
+ export async function engineStatus(workspace, config) {
271
+ if (!isVectorRequested(config)) {
272
+ const manifest = await readProviderManifest(workspace);
273
+ return {
274
+ provider: {
275
+ id: 'fts',
276
+ model: null,
277
+ ready: true,
278
+ cloud: false,
279
+ lastError: manifest?.providerId === 'fts' ? manifest.lastError : undefined
280
+ },
281
+ manifestLastError: manifest?.lastError
282
+ };
283
+ }
284
+ const requested = vectorEngine(config);
285
+ try {
286
+ const requestedStatus = await requested.status(workspace);
287
+ if (!requestedStatus.ready) throw new Error(requestedStatus.lastError || 'vector_provider_not_ready');
288
+ await writeReadyManifest(workspace, requestedStatus);
289
+ return {
290
+ provider: requestedStatus,
291
+ manifestLastError: requestedStatus.lastError
292
+ };
293
+ } catch (error) {
294
+ const fallback = {
295
+ from: requested.id,
296
+ to: 'fts',
297
+ reason: safeErrorMessage(error)
298
+ };
299
+ await writeFallbackManifest(workspace, fallback);
300
+ const requestedStatus = {
301
+ id: requested.id,
302
+ model: config.vectorModel || null,
303
+ ready: false,
304
+ cloud: false,
305
+ lastError: fallback.reason
306
+ };
307
+ return {
308
+ provider: {
309
+ id: 'fts',
310
+ model: null,
311
+ ready: true,
312
+ cloud: false,
313
+ lastError: fallback.reason,
314
+ fallback: true,
315
+ fallbackFrom: requested.id,
316
+ requested: requestedStatus
317
+ },
318
+ manifestLastError: fallback.reason
319
+ };
320
+ }
321
+ }
322
+
323
+
324
+ //# sourceURL=engine/retrieval.ts