@dotdotgod/context 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/package.json +2 -2
- package/src/content.mjs +112 -11
- package/src/directory-ingestion.mjs +182 -0
- package/src/doctor.mjs +14 -5
- package/src/environment-policy.mjs +85 -0
- package/src/execute.mjs +25 -6
- package/src/html-normalize.mjs +195 -0
- package/src/index.d.ts +44 -10
- package/src/index.mjs +7 -1
- package/src/jobs.mjs +62 -0
- package/src/provenance.mjs +1 -1
- package/src/server.mjs +38 -6
- package/src/session.mjs +12 -0
- package/src/store.mjs +253 -35
- package/src/tool-contracts.mjs +13 -0
package/src/store.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, rmSync } from 'node:fs';
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
4
|
import { extname } from 'node:path';
|
|
@@ -20,40 +20,174 @@ export function contextDbPath(root) {
|
|
|
20
20
|
return join(safeRoot(root), '.dotdotgod', 'context', 'context.sqlite');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const SCHEMA_VERSION = 3;
|
|
24
|
+
const MAX_PENDING_JOBS = 100;
|
|
25
|
+
|
|
26
|
+
function inspectSchema(db) {
|
|
27
|
+
const objects = new Map(db.prepare("SELECT name, type, sql FROM sqlite_master WHERE name IN ('sources', 'chunks')").all().map((row) => [row.name, row]));
|
|
28
|
+
const sourceRows = db.prepare('PRAGMA table_info(sources)').all();
|
|
29
|
+
const chunkRows = db.prepare('PRAGMA table_info(chunks)').all();
|
|
30
|
+
const requiredSources = ['id', 'scope', 'session_id', 'label', 'kind', 'metadata', 'created_at', 'expires_at'];
|
|
31
|
+
const requiredChunks = ['source_id', 'scope', 'session_id', 'ordinal', 'body'];
|
|
32
|
+
const missingSources = requiredSources.filter((name) => !sourceRows.some((row) => row.name === name));
|
|
33
|
+
const missingChunks = requiredChunks.filter((name) => !chunkRows.some((row) => row.name === name));
|
|
34
|
+
const sourceShape = sourceRows.length === requiredSources.length && sourceRows.every((row, index) => row.name === requiredSources[index])
|
|
35
|
+
&& sourceRows[0].pk === 1
|
|
36
|
+
&& sourceRows.every((row) => ['scope', 'label', 'kind', 'metadata', 'created_at'].includes(row.name) ? row.notnull === 1 : true);
|
|
37
|
+
const chunkShape = chunkRows.length === requiredChunks.length && chunkRows.every((row, index) => row.name === requiredChunks[index]);
|
|
38
|
+
const chunkSql = String(objects.get('chunks')?.sql ?? '').toLowerCase();
|
|
39
|
+
const compatible = objects.get('sources')?.type === 'table'
|
|
40
|
+
&& objects.get('chunks')?.type === 'table'
|
|
41
|
+
&& chunkSql.includes('using fts5')
|
|
42
|
+
&& chunkSql.includes("tokenize='porter unicode61'")
|
|
43
|
+
&& sourceShape
|
|
44
|
+
&& chunkShape
|
|
45
|
+
&& missingSources.length === 0
|
|
46
|
+
&& missingChunks.length === 0;
|
|
47
|
+
let version = 0;
|
|
48
|
+
if (compatible) {
|
|
49
|
+
const ledger = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'").get();
|
|
50
|
+
if (ledger) version = Number(db.prepare('SELECT coalesce(max(version), 0) AS version FROM schema_migrations').get().version);
|
|
51
|
+
else version = 1;
|
|
52
|
+
}
|
|
53
|
+
return { compatible: compatible && version <= SCHEMA_VERSION, schemaProfile: compatible ? version : null, missing: [...missingSources, ...missingChunks] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function migrate(db, fromVersion) {
|
|
57
|
+
if (fromVersion > SCHEMA_VERSION) throw new Error(`Incompatible context database schema version ${fromVersion}; runtime supports ${SCHEMA_VERSION}.`);
|
|
58
|
+
db.exec('BEGIN IMMEDIATE');
|
|
59
|
+
try {
|
|
60
|
+
db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);');
|
|
61
|
+
if (fromVersion < 2) {
|
|
62
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ingestion_jobs (
|
|
63
|
+
id TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('queued','running','completed','failed','cancelled')),
|
|
64
|
+
kind TEXT NOT NULL, input TEXT NOT NULL, result TEXT, error TEXT,
|
|
65
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
66
|
+
); CREATE INDEX IF NOT EXISTS ingestion_jobs_state_created ON ingestion_jobs(state, created_at);`);
|
|
67
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (1, ?)').run(Date.now());
|
|
68
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (2, ?)').run(Date.now());
|
|
69
|
+
}
|
|
70
|
+
if (fromVersion < 3) {
|
|
71
|
+
const columns = db.prepare('PRAGMA table_info(ingestion_jobs)').all();
|
|
72
|
+
if (!columns.some((row) => row.name === 'session_id')) db.exec('ALTER TABLE ingestion_jobs ADD COLUMN session_id TEXT;');
|
|
73
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (3, ?)').run(Date.now());
|
|
74
|
+
}
|
|
75
|
+
db.exec('COMMIT');
|
|
76
|
+
} catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function trigrams(value) {
|
|
80
|
+
const normalized = ` ${String(value).toLocaleLowerCase('en').replace(/[^\p{L}\p{N}]+/gu, ' ').trim()} `;
|
|
81
|
+
const out = new Set();
|
|
82
|
+
for (let index = 0; index + 3 <= normalized.length; index += 1) out.add(normalized.slice(index, index + 3));
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function trigramScore(left, right) {
|
|
86
|
+
const a = trigrams(left); const b = trigrams(right);
|
|
87
|
+
if (!a.size || !b.size) return 0;
|
|
88
|
+
let overlap = 0; for (const item of a) if (b.has(item)) overlap += 1;
|
|
89
|
+
return (2 * overlap) / (a.size + b.size);
|
|
90
|
+
}
|
|
91
|
+
function editDistance(left, right, ceiling = 2) {
|
|
92
|
+
if (Math.abs(left.length - right.length) > ceiling) return ceiling + 1;
|
|
93
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
94
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
95
|
+
const current = [i]; let rowMin = i;
|
|
96
|
+
for (let j = 1; j <= right.length; j += 1) { current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1)); rowMin = Math.min(rowMin, current[j]); }
|
|
97
|
+
if (rowMin > ceiling) return ceiling + 1; previous = current;
|
|
98
|
+
}
|
|
99
|
+
return previous[right.length];
|
|
100
|
+
}
|
|
101
|
+
function typoScore(query, value) {
|
|
102
|
+
const queryTerms = normalizeSearchTerms(query);
|
|
103
|
+
const valueTerms = normalizeSearchTerms(value);
|
|
104
|
+
let best = 0;
|
|
105
|
+
for (const needle of queryTerms) for (const token of valueTerms) {
|
|
106
|
+
if (needle.length >= 5 && editDistance(needle, token) <= 2) best = Math.max(best, trigramScore(needle, token));
|
|
107
|
+
}
|
|
108
|
+
return best;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function healContextDatabase(root = process.cwd()) {
|
|
112
|
+
const path = contextDbPath(root);
|
|
113
|
+
if (!existsSync(path)) throw new Error('No context database exists to heal.');
|
|
114
|
+
const stamp = Date.now();
|
|
115
|
+
const backupPath = `${path}.backup-${stamp}`;
|
|
116
|
+
const rebuiltPath = `${path}.healed-${stamp}`;
|
|
117
|
+
let inspection;
|
|
118
|
+
let replaced = false;
|
|
119
|
+
const db = new DatabaseSync(path);
|
|
120
|
+
try {
|
|
121
|
+
db.exec('PRAGMA busy_timeout=1000; PRAGMA wal_checkpoint(FULL);');
|
|
122
|
+
inspection = inspectSchema(db);
|
|
123
|
+
if (!inspection.compatible || ![1, 2, 3].includes(inspection.schemaProfile)) {
|
|
124
|
+
throw new Error('Schema is not a recognized recoverable context database; no rebuild performed.');
|
|
125
|
+
}
|
|
126
|
+
copyFileSync(path, backupPath, 0);
|
|
127
|
+
migrate(db, inspection.schemaProfile);
|
|
128
|
+
db.exec('PRAGMA wal_checkpoint(TRUNCATE);');
|
|
129
|
+
db.exec(`VACUUM INTO '${rebuiltPath.replaceAll("'", "''")}';`);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
rmSync(rebuiltPath, { force: true });
|
|
132
|
+
throw error;
|
|
133
|
+
} finally { db.close(); }
|
|
134
|
+
try {
|
|
135
|
+
renameSync(rebuiltPath, path);
|
|
136
|
+
replaced = true;
|
|
137
|
+
rmSync(`${path}-wal`, { force: true });
|
|
138
|
+
rmSync(`${path}-shm`, { force: true });
|
|
139
|
+
} finally {
|
|
140
|
+
if (!replaced) rmSync(rebuiltPath, { force: true });
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, backupPath, fromVersion: inspection.schemaProfile, toVersion: SCHEMA_VERSION, rebuilt: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
23
145
|
export class ContextStore {
|
|
24
146
|
constructor(root = process.cwd()) {
|
|
25
147
|
this.root = safeRoot(root);
|
|
26
148
|
this.path = contextDbPath(this.root);
|
|
149
|
+
const existing = existsSync(this.path);
|
|
150
|
+
let schemaVersion = 1;
|
|
151
|
+
if (existing) {
|
|
152
|
+
if (statSync(this.path).size === 0) throw new Error('Incompatible context database schema; empty database cannot be repaired automatically.');
|
|
153
|
+
const inspection = new DatabaseSync(this.path, { readOnly: true });
|
|
154
|
+
try {
|
|
155
|
+
const result = inspectSchema(inspection);
|
|
156
|
+
if (!result.compatible) throw new Error(`Incompatible context database schema; run doctor before explicit healing${result.missing.length ? ` (missing: ${result.missing.join(', ')})` : ''}.`);
|
|
157
|
+
schemaVersion = result.schemaProfile;
|
|
158
|
+
} finally {
|
|
159
|
+
inspection.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
27
162
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
28
163
|
this.db = new DatabaseSync(this.path);
|
|
29
|
-
|
|
30
|
-
PRAGMA journal_mode=WAL;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (missingSources.length || missingChunks.length) {
|
|
164
|
+
try {
|
|
165
|
+
this.db.exec('PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL;');
|
|
166
|
+
if (!existing) this.db.exec(`
|
|
167
|
+
CREATE TABLE sources (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
scope TEXT NOT NULL,
|
|
170
|
+
session_id TEXT,
|
|
171
|
+
label TEXT NOT NULL,
|
|
172
|
+
kind TEXT NOT NULL,
|
|
173
|
+
metadata TEXT NOT NULL,
|
|
174
|
+
created_at INTEGER NOT NULL,
|
|
175
|
+
expires_at INTEGER
|
|
176
|
+
);
|
|
177
|
+
CREATE VIRTUAL TABLE chunks USING fts5(
|
|
178
|
+
source_id UNINDEXED,
|
|
179
|
+
scope UNINDEXED,
|
|
180
|
+
session_id UNINDEXED,
|
|
181
|
+
ordinal UNINDEXED,
|
|
182
|
+
body,
|
|
183
|
+
tokenize='porter unicode61'
|
|
184
|
+
);
|
|
185
|
+
`);
|
|
186
|
+
migrate(this.db, schemaVersion);
|
|
187
|
+
this.db.prepare("UPDATE ingestion_jobs SET state = 'queued', updated_at = ? WHERE state = 'running'").run(Date.now());
|
|
188
|
+
} catch (error) {
|
|
55
189
|
this.db.close();
|
|
56
|
-
throw
|
|
190
|
+
throw error;
|
|
57
191
|
}
|
|
58
192
|
}
|
|
59
193
|
|
|
@@ -67,7 +201,7 @@ export class ContextStore {
|
|
|
67
201
|
const contentType = String(provenance.contentType ?? metadata.contentType ?? '').toLowerCase();
|
|
68
202
|
const extension = extname(String(metadata.path ?? '')).toLowerCase();
|
|
69
203
|
const format = provenance.format ?? (contentType.includes('json') || extension === '.json' ? 'json' : contentType.includes('markdown') || ['.md', '.mdx'].includes(extension) ? 'markdown' : 'text');
|
|
70
|
-
const extractor = format === 'json' ? 'json-v1' : format === 'markdown' ? 'markdown-v1' : 'plain';
|
|
204
|
+
const extractor = provenance.extractor ?? (format === 'json' ? 'json-v1' : format === 'markdown' ? 'markdown-v1' : 'plain');
|
|
71
205
|
const normalizedMetadata = createProvenanceMetadata(metadata, { sourceType, origin, content: text, extractor, indexedAt: new Date(createdAt).toISOString() });
|
|
72
206
|
this.db.exec('BEGIN IMMEDIATE');
|
|
73
207
|
try {
|
|
@@ -87,10 +221,18 @@ export class ContextStore {
|
|
|
87
221
|
|
|
88
222
|
expire() {
|
|
89
223
|
const ids = this.db.prepare('SELECT id FROM sources WHERE expires_at IS NOT NULL AND expires_at <= ?').all(Date.now()).map((row) => row.id);
|
|
224
|
+
if (ids.length === 0) return 0;
|
|
90
225
|
const removeChunks = this.db.prepare('DELETE FROM chunks WHERE source_id = ?');
|
|
91
226
|
const removeSource = this.db.prepare('DELETE FROM sources WHERE id = ?');
|
|
92
|
-
|
|
93
|
-
|
|
227
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
228
|
+
try {
|
|
229
|
+
for (const id of ids) { removeChunks.run(id); removeSource.run(id); }
|
|
230
|
+
this.db.exec('COMMIT');
|
|
231
|
+
return ids.length;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.db.exec('ROLLBACK');
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
94
236
|
}
|
|
95
237
|
|
|
96
238
|
search({ query, scope, sessionId, source, limit = 5 }) {
|
|
@@ -121,6 +263,27 @@ export class ContextStore {
|
|
|
121
263
|
AND (${labelClauses.join(' OR ')})${filterSql}
|
|
122
264
|
ORDER BY lower(s.label), c.ordinal LIMIT ?
|
|
123
265
|
`).all(...labelParams, ...filterParams, candidateLimit);
|
|
266
|
+
const typoWindow = Math.min(500, candidateLimit * 5);
|
|
267
|
+
const typoHalf = Math.max(1, Math.floor(typoWindow / 2));
|
|
268
|
+
const trigramRows = this.db.prepare(`
|
|
269
|
+
SELECT * FROM (
|
|
270
|
+
SELECT ${select.replace('SELECT ', '')}, 0 AS rank
|
|
271
|
+
FROM chunks c JOIN sources s ON s.id = c.source_id
|
|
272
|
+
WHERE 1 = 1${filterSql}
|
|
273
|
+
ORDER BY c.rowid ASC LIMIT ?
|
|
274
|
+
)
|
|
275
|
+
UNION ALL
|
|
276
|
+
SELECT * FROM (
|
|
277
|
+
SELECT ${select.replace('SELECT ', '')}, 0 AS rank
|
|
278
|
+
FROM chunks c JOIN sources s ON s.id = c.source_id
|
|
279
|
+
WHERE 1 = 1${filterSql}
|
|
280
|
+
ORDER BY c.rowid DESC LIMIT ?
|
|
281
|
+
)
|
|
282
|
+
`).all(...filterParams, typoHalf, ...filterParams, typoWindow - typoHalf)
|
|
283
|
+
.map((row) => ({ ...row, trigramScore: Math.max(typoScore(query, row.body), Number(row.ordinal) === 0 ? typoScore(query, row.label) : 0) }))
|
|
284
|
+
.filter((row) => row.trigramScore >= 0.5)
|
|
285
|
+
.sort((left, right) => right.trigramScore - left.trigramScore || String(left.source_id).localeCompare(String(right.source_id), 'en') || Number(left.ordinal) - Number(right.ordinal))
|
|
286
|
+
.slice(0, candidateLimit);
|
|
124
287
|
const toCandidate = (row) => {
|
|
125
288
|
let storedMetadata = {};
|
|
126
289
|
try { storedMetadata = JSON.parse(row.metadata); } catch { /* legacy invalid metadata */ }
|
|
@@ -129,6 +292,7 @@ export class ContextStore {
|
|
|
129
292
|
const fused = reciprocalRankFusion([
|
|
130
293
|
{ name: 'porter-bm25', candidates: porterRows.map(toCandidate) },
|
|
131
294
|
{ name: 'label-path', candidates: labelRows.map(toCandidate) },
|
|
295
|
+
{ name: 'trigram-v1', candidates: trigramRows.map(toCandidate) },
|
|
132
296
|
], { limit: candidateLimit });
|
|
133
297
|
return rerankCandidates(fused, query, { limit: resultLimit }).map((entry) => {
|
|
134
298
|
const row = entry.candidate;
|
|
@@ -150,6 +314,53 @@ export class ContextStore {
|
|
|
150
314
|
});
|
|
151
315
|
}
|
|
152
316
|
|
|
317
|
+
createJob({ id = crypto.randomUUID(), kind, input, sessionId = null }) {
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
const serializedInput = JSON.stringify(input);
|
|
320
|
+
if (Buffer.byteLength(serializedInput) > 64 * 1024) throw new Error('Background ingestion job input exceeds 64 KiB.');
|
|
321
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
322
|
+
try {
|
|
323
|
+
const pending = Number(this.db.prepare("SELECT count(*) AS count FROM ingestion_jobs WHERE state IN ('queued','running')").get().count);
|
|
324
|
+
if (pending >= MAX_PENDING_JOBS) throw new Error('Background ingestion queue is full.');
|
|
325
|
+
this.db.prepare('INSERT INTO ingestion_jobs(id, state, kind, input, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
326
|
+
.run(id, 'queued', kind, serializedInput, sessionId, now, now);
|
|
327
|
+
this.db.exec('COMMIT');
|
|
328
|
+
} catch (error) { this.db.exec('ROLLBACK'); throw error; }
|
|
329
|
+
return this.getJob(id);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getJob(id) {
|
|
333
|
+
const row = this.db.prepare('SELECT * FROM ingestion_jobs WHERE id = ?').get(id);
|
|
334
|
+
if (!row) return null;
|
|
335
|
+
return { id: row.id, state: row.state, kind: row.kind, input: JSON.parse(row.input), sessionId: row.session_id ?? null, result: row.result ? JSON.parse(row.result) : null, error: row.error, createdAt: row.created_at, updatedAt: row.updated_at };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
claimNextJob() {
|
|
339
|
+
const row = this.db.prepare("SELECT id FROM ingestion_jobs WHERE state = 'queued' ORDER BY created_at, id LIMIT 1").get();
|
|
340
|
+
if (!row) return null;
|
|
341
|
+
const changed = this.db.prepare("UPDATE ingestion_jobs SET state = 'running', updated_at = ? WHERE id = ? AND state = 'queued'").run(Date.now(), row.id).changes;
|
|
342
|
+
return changed ? this.getJob(row.id) : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
finishJob(id, state, value = null) {
|
|
346
|
+
if (!['completed', 'failed', 'cancelled'].includes(state)) throw new Error('Invalid terminal job state.');
|
|
347
|
+
let result = state === 'completed' ? JSON.stringify(value) : null;
|
|
348
|
+
if (result && Buffer.byteLength(result) > 256 * 1024) result = JSON.stringify({ truncated: true, message: 'Completed result exceeded the durable status limit.' });
|
|
349
|
+
const error = state === 'failed' ? String(value ?? 'Job failed').slice(0, 4096) : null;
|
|
350
|
+
this.db.prepare("UPDATE ingestion_jobs SET state = ?, result = ?, error = ?, updated_at = ? WHERE id = ? AND state IN ('queued','running')")
|
|
351
|
+
.run(state, result, error, Date.now(), id);
|
|
352
|
+
return this.getJob(id);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
cancelJob(id) {
|
|
356
|
+
const changed = this.db.prepare("UPDATE ingestion_jobs SET state = 'cancelled', updated_at = ? WHERE id = ? AND state IN ('queued','running')").run(Date.now(), id).changes;
|
|
357
|
+
return { changed: Number(changed), job: this.getJob(id) };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
listPendingJobs(limit = 20) {
|
|
361
|
+
return this.db.prepare("SELECT id FROM ingestion_jobs WHERE state IN ('queued','running') ORDER BY created_at, id LIMIT ?").all(Math.min(100, Math.max(1, limit))).map((row) => this.getJob(row.id));
|
|
362
|
+
}
|
|
363
|
+
|
|
153
364
|
stats() {
|
|
154
365
|
this.expire();
|
|
155
366
|
const sources = this.db.prepare('SELECT count(*) AS count FROM sources').get().count;
|
|
@@ -168,11 +379,18 @@ export class ContextStore {
|
|
|
168
379
|
const removeChunks = this.db.prepare('DELETE FROM chunks WHERE source_id = ?');
|
|
169
380
|
const removeSource = this.db.prepare('DELETE FROM sources WHERE id = ?');
|
|
170
381
|
let removed = 0;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
382
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
383
|
+
try {
|
|
384
|
+
for (const id of ids) {
|
|
385
|
+
removeChunks.run(id);
|
|
386
|
+
removed += Number(removeSource.run(id).changes ?? 0);
|
|
387
|
+
}
|
|
388
|
+
this.db.exec('COMMIT');
|
|
389
|
+
return { removed };
|
|
390
|
+
} catch (error) {
|
|
391
|
+
this.db.exec('ROLLBACK');
|
|
392
|
+
throw error;
|
|
174
393
|
}
|
|
175
|
-
return { removed };
|
|
176
394
|
}
|
|
177
395
|
|
|
178
396
|
destroy() {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const object = (properties, required = []) => Object.freeze({ type: 'object', properties, required, additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#' });
|
|
2
|
+
const string = (options = {}) => Object.freeze({ type: 'string', ...options });
|
|
3
|
+
|
|
4
|
+
export const PHASE3_TOOL_INPUT_SCHEMAS = Object.freeze({
|
|
5
|
+
session_resume: object({ sessionId: string({ minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$' }) }, ['sessionId']),
|
|
6
|
+
ingestion_job_start: object({
|
|
7
|
+
kind: Object.freeze({ type: 'string', enum: ['index', 'fetch'] }),
|
|
8
|
+
input: Object.freeze({ type: 'object', additionalProperties: Object.freeze({}) }),
|
|
9
|
+
}, ['kind', 'input']),
|
|
10
|
+
ingestion_job_status: object({ id: string({ format: 'uuid' }) }, ['id']),
|
|
11
|
+
ingestion_job_cancel: object({ id: string({ format: 'uuid' }) }, ['id']),
|
|
12
|
+
context_heal: object({ confirm: Object.freeze({ const: true, type: 'boolean' }) }, ['confirm']),
|
|
13
|
+
});
|