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/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +250 -0
- package/TRADEMARK.md +24 -0
- package/bin/ihow-memory.mjs +53 -0
- package/dist/cli.js +1084 -0
- package/dist/core.js +85 -0
- package/dist/engine/fts.js +210 -0
- package/dist/engine/manifest.js +45 -0
- package/dist/engine/retrieval.js +324 -0
- package/dist/governance.js +369 -0
- package/dist/http/console.js +287 -0
- package/dist/mcp/server.js +235 -0
- package/dist/store/events.js +17 -0
- package/dist/store/files.js +61 -0
- package/dist/store/lock.js +35 -0
- package/dist/telemetry.js +98 -0
- package/dist/types.js +3 -0
- package/dist/workspace.js +151 -0
- package/package.json +62 -0
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
|