@jbarket/loomai 0.5.0 → 0.7.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/CHANGELOG.md +129 -1
- package/README.md +115 -0
- package/dist/backends/dedup.d.ts +23 -0
- package/dist/backends/dedup.js +100 -0
- package/dist/backends/embedding-runtime.js +38 -9
- package/dist/backends/episodes.js +1 -1
- package/dist/backends/fastembed.js +30 -4
- package/dist/backends/migrations.js +200 -0
- package/dist/backends/mmr.d.ts +8 -0
- package/dist/backends/mmr.js +18 -4
- package/dist/backends/people.d.ts +192 -0
- package/dist/backends/people.js +824 -0
- package/dist/backends/proposals.js +1 -1
- package/dist/backends/recorder-health.d.ts +34 -0
- package/dist/backends/recorder-health.js +117 -0
- package/dist/backends/salience.d.ts +39 -0
- package/dist/backends/salience.js +52 -4
- package/dist/backends/scope.d.ts +56 -0
- package/dist/backends/scope.js +75 -0
- package/dist/backends/sqlite-knowledge.d.ts +24 -4
- package/dist/backends/sqlite-knowledge.js +253 -91
- package/dist/backends/sqlite-vec.d.ts +32 -1
- package/dist/backends/sqlite-vec.js +394 -75
- package/dist/backends/types.d.ts +203 -3
- package/dist/categories.d.ts +2 -0
- package/dist/categories.js +2 -0
- package/dist/cli/args.d.ts +16 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/knowledge.js +4 -1
- package/dist/cli/memory.js +2 -2
- package/dist/cli/person.d.ts +2 -0
- package/dist/cli/person.js +116 -0
- package/dist/cli/pieces.d.ts +24 -0
- package/dist/cli/pieces.js +139 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +10 -0
- package/dist/cli/remote.d.ts +36 -0
- package/dist/cli/remote.js +173 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +94 -5
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +2 -2
- package/dist/cli/update.js +30 -11
- package/dist/clients.js +7 -4
- package/dist/config.d.ts +14 -0
- package/dist/config.js +25 -2
- package/dist/index.js +28 -8
- package/dist/injection/harnesses.js +9 -3
- package/dist/install/harnesses.js +7 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +461 -374
- package/dist/tools/dossier.js +5 -1
- package/dist/tools/eurorack-import.js +3 -0
- package/dist/tools/identity.d.ts +2 -0
- package/dist/tools/identity.js +35 -2
- package/dist/tools/knowledge-history.d.ts +6 -1
- package/dist/tools/knowledge-history.js +26 -8
- package/dist/tools/knowledge-maintain.d.ts +5 -0
- package/dist/tools/knowledge-maintain.js +92 -5
- package/dist/tools/knowledge-recall.d.ts +7 -1
- package/dist/tools/knowledge-recall.js +53 -3
- package/dist/tools/knowledge-verify.d.ts +11 -1
- package/dist/tools/knowledge-verify.js +17 -4
- package/dist/tools/knowledge-write.d.ts +8 -1
- package/dist/tools/knowledge-write.js +27 -9
- package/dist/tools/memory-history.d.ts +5 -0
- package/dist/tools/memory-history.js +52 -0
- package/dist/tools/memory-list.js +2 -1
- package/dist/tools/memory-merge.d.ts +2 -0
- package/dist/tools/memory-merge.js +40 -0
- package/dist/tools/memory-revision-restore.d.ts +5 -0
- package/dist/tools/memory-revision-restore.js +33 -0
- package/dist/tools/person.d.ts +32 -0
- package/dist/tools/person.js +111 -0
- package/dist/tools/prune.js +1 -1
- package/dist/tools/recall.js +7 -1
- package/dist/tools/remember.js +18 -2
- package/dist/tools/update.js +5 -1
- package/dist/transport/cli-endpoint.d.ts +13 -0
- package/dist/transport/cli-endpoint.js +97 -0
- package/dist/transport/guards.d.ts +14 -0
- package/dist/transport/guards.js +26 -0
- package/dist/transport/http-server.d.ts +28 -1
- package/dist/transport/http-server.js +156 -18
- package/dist/transport/knowledge-server.d.ts +11 -0
- package/dist/transport/knowledge-server.js +30 -0
- package/dist/transport/knowledge-tools.d.ts +50 -0
- package/dist/transport/knowledge-tools.js +290 -0
- package/dist/transport/mcp-app-hello.d.ts +1 -1
- package/dist/widgets/display-modes.html +62 -56
- package/dist/widgets/hello.html +62 -56
- package/dist/widgets/proposals.html +62 -56
- package/dist/widgets/top-of-mind.html +60 -54
- package/package.json +7 -5
|
@@ -11,6 +11,8 @@ import { mkdirSync } from 'node:fs';
|
|
|
11
11
|
import { dirname } from 'node:path';
|
|
12
12
|
import BetterSqlite3 from 'better-sqlite3';
|
|
13
13
|
import { retryWrite } from './retry.js';
|
|
14
|
+
/** Identity attributed to a stamped access when the caller doesn't pass one. */
|
|
15
|
+
const UNKNOWN_IDENTITY = 'unknown';
|
|
14
16
|
/** Hard size caps — enforced at write boundary (design §A3). */
|
|
15
17
|
const MAX_PAGE_BODY_LENGTH = 64 * 1024; // 64 KB
|
|
16
18
|
const MAX_CITATION_EXCERPT_LENGTH = 4096; // 4 KB
|
|
@@ -25,10 +27,9 @@ export class SqliteKnowledgeBackend {
|
|
|
25
27
|
// Open in constructor for schema init; caller closes after use.
|
|
26
28
|
this.db = new BetterSqlite3(config.dbPath);
|
|
27
29
|
this.db.pragma('journal_mode = WAL');
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
this.db.pragma('busy_timeout = 0');
|
|
30
|
+
// busy_timeout: concurrent sleeves retry brief write-lock windows rather
|
|
31
|
+
// than failing immediately — see sqlite-vec.ts for the full rationale.
|
|
32
|
+
this.db.pragma(`busy_timeout = ${config.busyTimeoutMs ?? 5000}`);
|
|
32
33
|
this.db.pragma('user_version = 1');
|
|
33
34
|
this.initSchema();
|
|
34
35
|
}
|
|
@@ -38,63 +39,92 @@ export class SqliteKnowledgeBackend {
|
|
|
38
39
|
enforcePageBodyCap(input.body);
|
|
39
40
|
const db = this.ensureOpen();
|
|
40
41
|
const timestamp = new Date().toISOString();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const newBody = appliedMode === 'append'
|
|
56
|
-
? `${existing.body}\n\n${input.body}`
|
|
57
|
-
: input.body;
|
|
58
|
-
enforcePageBodyCap(newBody);
|
|
59
|
-
// Replace-writes destroy the stored body — snapshot it first so the
|
|
60
|
-
// page is recoverable (the 2026-06-01 verify run stomped 13 bodies
|
|
61
|
-
// with no recovery path other than transcript archaeology).
|
|
62
|
-
if (appliedMode === 'replace' && newBody !== existing.body) {
|
|
63
|
-
retryWrite(() => this.snapshotRevision(db, pageId, input.slug, existing.body, 'write-replace', timestamp));
|
|
42
|
+
// Title and domain follow the write — the page is addressed by slug,
|
|
43
|
+
// so a differing title/domain on upsert is an intentional revision.
|
|
44
|
+
const title = input.title;
|
|
45
|
+
const sourcing = input.sourcing ?? 'sourced';
|
|
46
|
+
// Set inside the transaction, read after it commits. Every one of these
|
|
47
|
+
// is derived from the existing-page read, so all of them are re-derived
|
|
48
|
+
// on a SQLITE_BUSY retry rather than carried over from the losing attempt.
|
|
49
|
+
let uuid = '';
|
|
50
|
+
let appliedMode = 'create';
|
|
51
|
+
let pageId = 0;
|
|
52
|
+
// Validate all citations before opening the transaction.
|
|
53
|
+
if (input.citations && input.citations.length > 0) {
|
|
54
|
+
for (const cit of input.citations) {
|
|
55
|
+
enforceExcerptCap(cit.excerpt);
|
|
64
56
|
}
|
|
65
|
-
// verified_at is a verification stamp, not a write stamp: an update
|
|
66
|
-
// only moves it when the writer explicitly claims verification.
|
|
67
|
-
// (Creation still stamps write time — the page was just synthesized
|
|
68
|
-
// against its sources.)
|
|
69
|
-
// created_by / version are preserved across upserts when omitted.
|
|
70
|
-
retryWrite(() => db.prepare(`UPDATE pages SET title = ?, domain = ?, body = ?, sourcing = ?, verified_at = COALESCE(?, verified_at), freshness_anchor = COALESCE(?, freshness_anchor), created_by = COALESCE(?, created_by), version = COALESCE(?, version), updated = ? WHERE id = ?`).run(title, input.domain, newBody, sourcing, input.verified_at ?? null, input.freshness_anchor ?? null, input.created_by ?? null, input.version ?? null, timestamp, pageId));
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
uuid = randomUUID();
|
|
74
|
-
title = input.title;
|
|
75
|
-
sourcing = input.sourcing ?? 'sourced';
|
|
76
|
-
appliedMode = 'create';
|
|
77
|
-
const result = retryWrite(() => db.prepare(`INSERT INTO pages (uuid, slug, title, domain, body, sourcing, provenance, verified_at, freshness_anchor, created_by, version, created, updated)
|
|
78
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid, input.slug, title, input.domain, input.body, sourcing, input.provenance ?? null, input.verified_at ?? timestamp, input.freshness_anchor ?? null, input.created_by ?? null, input.version ?? null, timestamp, timestamp));
|
|
79
|
-
pageId = Number(result.lastInsertRowid);
|
|
80
57
|
}
|
|
81
58
|
let citationsAdded = 0;
|
|
82
59
|
let citationsDeduped = 0;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
// Pre-prepare citation statements — compiled once, safe to reuse on retry.
|
|
61
|
+
// Exact-duplicate guard: re-sending a page's citations on upsert must not
|
|
62
|
+
// multiply them. A citation is a duplicate when claim, kind, locator, and
|
|
63
|
+
// excerpt all match an existing row on the same page.
|
|
64
|
+
const dupCheck = db.prepare(`SELECT 1 FROM citations
|
|
65
|
+
WHERE page_id = ? AND claim = ? AND source_kind = ?
|
|
66
|
+
AND COALESCE(source_locator, '') = COALESCE(?, '')
|
|
67
|
+
AND excerpt = ?`);
|
|
68
|
+
const insertCit = db.prepare(`INSERT INTO citations (page_id, claim, source_kind, source_locator, excerpt, retrieved_at, created)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
70
|
+
// ── Single transaction: read + snapshot + page write + citations ─────────
|
|
71
|
+
// All sub-operations must commit atomically — a crash between any two
|
|
72
|
+
// leaves a partial state: a page with no citations, or a snapshot with no
|
|
73
|
+
// corresponding page update. Every other mutating method uses db.transaction;
|
|
74
|
+
// writePage — the hottest path — now does too (t-329).
|
|
75
|
+
//
|
|
76
|
+
// The existing-page read is INSIDE the span, and the span is IMMEDIATE:
|
|
77
|
+
// writePage is state-dependent (mode, body, uuid, and the snapshot's
|
|
78
|
+
// pre-image all follow from what it read), so an atomic write over a read
|
|
79
|
+
// taken outside the lock protects nothing. A second process committing in
|
|
80
|
+
// that window used to be silently overwritten — an append dropped the
|
|
81
|
+
// contribution it never saw, and a replace snapshotted a body that was no
|
|
82
|
+
// longer the one it displaced, losing it with no revision to recover from.
|
|
83
|
+
// BEGIN IMMEDIATE takes the write lock before the read, so the read/write
|
|
84
|
+
// sequence is serialized: the loser is told SQLITE_BUSY and retries the
|
|
85
|
+
// whole thing — re-reading — instead of writing over what it missed.
|
|
86
|
+
// (Mark, 2026-09-06, reviewing #92.)
|
|
87
|
+
//
|
|
88
|
+
// Counter variables (citationsAdded, citationsDeduped) are reset at the top
|
|
89
|
+
// of the callback so a SQLITE_BUSY retry never double-counts.
|
|
90
|
+
const tx = db.transaction(() => {
|
|
91
|
+
citationsAdded = 0;
|
|
92
|
+
citationsDeduped = 0;
|
|
93
|
+
const existing = db.prepare('SELECT id, uuid, title, body, freshness_anchor FROM pages WHERE slug = ?').get(input.slug);
|
|
94
|
+
uuid = existing?.uuid ?? randomUUID();
|
|
95
|
+
pageId = existing?.id ?? 0;
|
|
96
|
+
appliedMode = existing ? (input.bodyMode ?? 'replace') : 'create';
|
|
97
|
+
const newBody = (existing && appliedMode === 'append')
|
|
98
|
+
? `${existing.body}\n\n${input.body}`
|
|
99
|
+
: input.body;
|
|
100
|
+
if (existing)
|
|
101
|
+
enforcePageBodyCap(newBody);
|
|
102
|
+
// Derive sla_class from the final anchor + domain; caller may override.
|
|
103
|
+
const finalAnchor = input.freshness_anchor ?? existing?.freshness_anchor ?? null;
|
|
104
|
+
const slaClass = input.sla_class ?? deriveSlaClass(finalAnchor, input.domain);
|
|
105
|
+
if (existing) {
|
|
106
|
+
// Replace-writes destroy the stored body — snapshot it first so the
|
|
107
|
+
// page is recoverable (the 2026-06-01 verify run stomped 13 bodies
|
|
108
|
+
// with no recovery path other than transcript archaeology).
|
|
109
|
+
if (appliedMode === 'replace' && newBody !== existing.body) {
|
|
110
|
+
this.snapshotRevision(db, pageId, input.slug, existing.body, 'write-replace', timestamp, input.actor ?? null);
|
|
111
|
+
}
|
|
112
|
+
// verified_at is a verification stamp, not a write stamp: an update
|
|
113
|
+
// only moves it when the writer explicitly claims verification.
|
|
114
|
+
// (Creation still stamps write time — the page was just synthesized
|
|
115
|
+
// against its sources.)
|
|
116
|
+
// created_by / version are preserved across upserts when omitted.
|
|
117
|
+
// author is deliberately absent from this UPDATE — it is the
|
|
118
|
+
// identity that wrote v1 and is never changed by a later upsert.
|
|
119
|
+
// sla_class is always re-derived from the final anchor + domain.
|
|
120
|
+
db.prepare(`UPDATE pages SET title = ?, domain = ?, body = ?, sourcing = ?, verified_at = COALESCE(?, verified_at), freshness_anchor = COALESCE(?, freshness_anchor), created_by = COALESCE(?, created_by), version = COALESCE(?, version), sla_class = ?, updated = ? WHERE id = ?`).run(title, input.domain, newBody, sourcing, input.verified_at ?? null, input.freshness_anchor ?? null, input.created_by ?? null, input.version ?? null, slaClass, timestamp, pageId);
|
|
86
121
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
AND COALESCE(source_locator, '') = COALESCE(?, '')
|
|
94
|
-
AND excerpt = ?`);
|
|
95
|
-
const insertCit = db.prepare(`INSERT INTO citations (page_id, claim, source_kind, source_locator, excerpt, retrieved_at, created)
|
|
96
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
97
|
-
const tx = db.transaction(() => {
|
|
122
|
+
else {
|
|
123
|
+
const result = db.prepare(`INSERT INTO pages (uuid, slug, title, domain, body, sourcing, provenance, verified_at, freshness_anchor, sla_class, created_by, version, author, created, updated)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid, input.slug, title, input.domain, newBody, sourcing, input.provenance ?? null, input.verified_at ?? timestamp, input.freshness_anchor ?? null, slaClass, input.created_by ?? null, input.version ?? null, input.actor ?? null, timestamp, timestamp);
|
|
125
|
+
pageId = Number(result.lastInsertRowid);
|
|
126
|
+
}
|
|
127
|
+
if (input.citations && input.citations.length > 0) {
|
|
98
128
|
for (const cit of input.citations) {
|
|
99
129
|
const dup = dupCheck.get(pageId, cit.claim, cit.source_kind, cit.source_locator ?? null, cit.excerpt);
|
|
100
130
|
if (dup) {
|
|
@@ -104,9 +134,9 @@ export class SqliteKnowledgeBackend {
|
|
|
104
134
|
insertCit.run(pageId, cit.claim, cit.source_kind, cit.source_locator ?? null, cit.excerpt, timestamp, timestamp);
|
|
105
135
|
citationsAdded++;
|
|
106
136
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
retryWrite(() => tx.immediate());
|
|
110
140
|
return Promise.resolve({
|
|
111
141
|
uuid,
|
|
112
142
|
slug: input.slug,
|
|
@@ -129,7 +159,7 @@ export class SqliteKnowledgeBackend {
|
|
|
129
159
|
return Promise.resolve(null);
|
|
130
160
|
if (opts?.stampAccess) {
|
|
131
161
|
const now = new Date().toISOString();
|
|
132
|
-
|
|
162
|
+
this.recordAccess(db, [page.id], opts.identity ?? UNKNOWN_IDENTITY, now);
|
|
133
163
|
page.last_accessed = now;
|
|
134
164
|
page.hit_count += 1;
|
|
135
165
|
}
|
|
@@ -210,12 +240,7 @@ export class SqliteKnowledgeBackend {
|
|
|
210
240
|
// appearing in a listing is not a read.
|
|
211
241
|
if (rows.length > 0 && input.stampAccess !== false) {
|
|
212
242
|
const now = new Date().toISOString();
|
|
213
|
-
|
|
214
|
-
const tx = db.transaction((ids) => {
|
|
215
|
-
for (const id of ids)
|
|
216
|
-
stamp.run(now, id);
|
|
217
|
-
});
|
|
218
|
-
retryWrite(() => tx(rows.map((r) => r.id)));
|
|
243
|
+
this.recordAccess(db, rows.map((r) => r.id), input.identity ?? UNKNOWN_IDENTITY, now);
|
|
219
244
|
}
|
|
220
245
|
return Promise.resolve(rows.map((page) => ({
|
|
221
246
|
...page,
|
|
@@ -280,7 +305,7 @@ export class SqliteKnowledgeBackend {
|
|
|
280
305
|
// Record the supersession.
|
|
281
306
|
db.prepare("INSERT INTO supersessions (old_slug, new_slug, note, created) VALUES (?, ?, ?, ?)").run(input.old_slug, input.new_slug, input.note ?? null, timestamp);
|
|
282
307
|
});
|
|
283
|
-
retryWrite(() => tx());
|
|
308
|
+
retryWrite(() => tx.immediate());
|
|
284
309
|
return Promise.resolve({
|
|
285
310
|
old_slug: input.old_slug,
|
|
286
311
|
new_slug: input.new_slug,
|
|
@@ -312,7 +337,7 @@ export class SqliteKnowledgeBackend {
|
|
|
312
337
|
pages.push({ slug: p.slug, old_domain: p.domain, new_domain: newDomain });
|
|
313
338
|
}
|
|
314
339
|
});
|
|
315
|
-
retryWrite(() => tx());
|
|
340
|
+
retryWrite(() => tx.immediate());
|
|
316
341
|
return Promise.resolve({ moved: pages.length, pages, pointers_written: 0 });
|
|
317
342
|
}
|
|
318
343
|
// ── Batch by explicit slug list (re-domain only) ──────────────────────────
|
|
@@ -331,7 +356,7 @@ export class SqliteKnowledgeBackend {
|
|
|
331
356
|
pages.push({ slug, old_domain: page.domain, new_domain: input.new_domain });
|
|
332
357
|
}
|
|
333
358
|
});
|
|
334
|
-
retryWrite(() => tx());
|
|
359
|
+
retryWrite(() => tx.immediate());
|
|
335
360
|
return Promise.resolve({ moved: pages.length, pages, pointers_written: 0 });
|
|
336
361
|
}
|
|
337
362
|
// ── Single-page mode ──────────────────────────────────────────────────────
|
|
@@ -373,7 +398,7 @@ export class SqliteKnowledgeBackend {
|
|
|
373
398
|
db.prepare('UPDATE pages SET domain = ?, updated = ? WHERE slug = ?').run(input.new_domain, timestamp, currentSlug);
|
|
374
399
|
}
|
|
375
400
|
});
|
|
376
|
-
retryWrite(() => tx());
|
|
401
|
+
retryWrite(() => tx.immediate());
|
|
377
402
|
return Promise.resolve({
|
|
378
403
|
moved: 1,
|
|
379
404
|
pages: [{
|
|
@@ -395,7 +420,7 @@ export class SqliteKnowledgeBackend {
|
|
|
395
420
|
if (!input.source_slugs || input.source_slugs.length === 0) {
|
|
396
421
|
return Promise.reject(new Error('mergePages: source_slugs must not be empty'));
|
|
397
422
|
}
|
|
398
|
-
const targetRow = db.prepare('SELECT id, body, verified_at, freshness_anchor FROM pages WHERE slug = ?').get(input.target_slug);
|
|
423
|
+
const targetRow = db.prepare('SELECT id, body, verified_at, freshness_anchor, domain FROM pages WHERE slug = ?').get(input.target_slug);
|
|
399
424
|
if (!targetRow) {
|
|
400
425
|
return Promise.reject(new Error(`mergePages: target page not found: ${input.target_slug}`));
|
|
401
426
|
}
|
|
@@ -455,8 +480,9 @@ export class SqliteKnowledgeBackend {
|
|
|
455
480
|
db.prepare('UPDATE citations SET page_id = ? WHERE page_id = ?').run(targetRow.id, source.id);
|
|
456
481
|
citationsMoved += after;
|
|
457
482
|
}
|
|
458
|
-
// Update target: verified_at, freshness_anchor, updated.
|
|
459
|
-
|
|
483
|
+
// Update target: verified_at, freshness_anchor, sla_class, updated.
|
|
484
|
+
const mergedSlaClass = deriveSlaClass(freshnessAnchor ?? null, targetRow.domain);
|
|
485
|
+
db.prepare('UPDATE pages SET verified_at = ?, freshness_anchor = ?, sla_class = ?, updated = ? WHERE id = ?').run(maxVerifiedAt, freshnessAnchor ?? null, mergedSlaClass, timestamp, targetRow.id);
|
|
460
486
|
// Optionally append loser bodies to target body.
|
|
461
487
|
if (input.append_loser_bodies) {
|
|
462
488
|
let appendedBody = targetRow.body;
|
|
@@ -482,7 +508,7 @@ export class SqliteKnowledgeBackend {
|
|
|
482
508
|
}
|
|
483
509
|
}
|
|
484
510
|
});
|
|
485
|
-
retryWrite(() => tx());
|
|
511
|
+
retryWrite(() => tx.immediate());
|
|
486
512
|
return Promise.resolve({
|
|
487
513
|
target_slug: input.target_slug,
|
|
488
514
|
sources_merged: sourceRows.length,
|
|
@@ -524,7 +550,7 @@ export class SqliteKnowledgeBackend {
|
|
|
524
550
|
db.prepare('DELETE FROM pages WHERE id = ?').run(row.id);
|
|
525
551
|
}
|
|
526
552
|
});
|
|
527
|
-
retryWrite(() => tx());
|
|
553
|
+
retryWrite(() => tx.immediate());
|
|
528
554
|
return Promise.resolve({ purged: input.slugs.length, slugs: input.slugs });
|
|
529
555
|
}
|
|
530
556
|
catch (e) {
|
|
@@ -550,19 +576,29 @@ export class SqliteKnowledgeBackend {
|
|
|
550
576
|
}
|
|
551
577
|
const rows = [];
|
|
552
578
|
for (const slug of slugs) {
|
|
553
|
-
const page = db.prepare('SELECT id, status, body FROM pages WHERE slug = ?').get(slug);
|
|
579
|
+
const page = db.prepare('SELECT id, status, body, freshness_anchor, domain FROM pages WHERE slug = ?').get(slug);
|
|
554
580
|
if (!page) {
|
|
555
581
|
return Promise.reject(new Error(`verifyPages: page not found: ${slug}`));
|
|
556
582
|
}
|
|
557
583
|
if (page.status === 'archived') {
|
|
558
584
|
return Promise.reject(new Error(`verifyPages: page '${slug}' is archived — restore it before verifying`));
|
|
559
585
|
}
|
|
560
|
-
rows.push({ id: page.id, slug, body: page.body });
|
|
561
|
-
}
|
|
586
|
+
rows.push({ id: page.id, slug, body: page.body, freshness_anchor: page.freshness_anchor, domain: page.domain });
|
|
587
|
+
}
|
|
588
|
+
const outcome = input.outcome ?? 'confirmed';
|
|
589
|
+
const citationChecked = input.citation_checked ? 1 : 0;
|
|
590
|
+
const insertVerification = db.prepare('INSERT INTO verifications (page_id, verifier, verified_at, outcome, citation_checked) VALUES (?, ?, ?, ?, ?)');
|
|
591
|
+
// verified_at means "last SUCCESSFUL verification" — it drives the
|
|
592
|
+
// stale-first queue and SLA checks. A 'stale' outcome is a failed
|
|
593
|
+
// verification: it goes in the audit trail but must not renew the
|
|
594
|
+
// stamp, or a known-wrong page drops to the back of the queue.
|
|
595
|
+
const stamped = outcome !== 'stale';
|
|
562
596
|
let noted = false;
|
|
563
597
|
const tx = db.transaction(() => {
|
|
564
598
|
for (const row of rows) {
|
|
565
|
-
|
|
599
|
+
const finalAnchor = input.freshness_anchor ?? row.freshness_anchor;
|
|
600
|
+
const slaClass = deriveSlaClass(finalAnchor, row.domain);
|
|
601
|
+
db.prepare('UPDATE pages SET verified_at = CASE WHEN ? THEN ? ELSE verified_at END, freshness_anchor = COALESCE(?, freshness_anchor), sla_class = ? WHERE id = ?').run(stamped ? 1 : 0, verifiedAt, input.freshness_anchor ?? null, slaClass, row.id);
|
|
566
602
|
if (input.note) {
|
|
567
603
|
const dateLabel = verifiedAt.slice(0, 10);
|
|
568
604
|
const newBody = `${row.body}\n\n## Verification — ${dateLabel}\n\n${input.note}`;
|
|
@@ -572,13 +608,18 @@ export class SqliteKnowledgeBackend {
|
|
|
572
608
|
db.prepare('UPDATE pages SET body = ?, updated = ? WHERE id = ?').run(newBody, timestamp, row.id);
|
|
573
609
|
noted = true;
|
|
574
610
|
}
|
|
611
|
+
// The attributed audit trail (t-675) — recorded for every outcome,
|
|
612
|
+
// additive to the verified_at stamp above (which stays for
|
|
613
|
+
// freshness sort/SLA and only moves on success).
|
|
614
|
+
insertVerification.run(row.id, input.verifier ?? null, verifiedAt, outcome, citationChecked);
|
|
575
615
|
}
|
|
576
616
|
});
|
|
577
|
-
retryWrite(() => tx());
|
|
617
|
+
retryWrite(() => tx.immediate());
|
|
578
618
|
return Promise.resolve({
|
|
579
619
|
verified: rows.length,
|
|
580
620
|
slugs: rows.map((r) => r.slug),
|
|
581
621
|
verified_at: verifiedAt,
|
|
622
|
+
stamped,
|
|
582
623
|
noted,
|
|
583
624
|
});
|
|
584
625
|
}
|
|
@@ -586,6 +627,21 @@ export class SqliteKnowledgeBackend {
|
|
|
586
627
|
return Promise.reject(e);
|
|
587
628
|
}
|
|
588
629
|
}
|
|
630
|
+
getVerifications(slug) {
|
|
631
|
+
try {
|
|
632
|
+
const db = this.ensureOpen();
|
|
633
|
+
const page = db.prepare('SELECT id FROM pages WHERE slug = ?').get(slug);
|
|
634
|
+
if (!page) {
|
|
635
|
+
return Promise.reject(new Error(`getVerifications: page not found: ${slug}`));
|
|
636
|
+
}
|
|
637
|
+
const rows = db.prepare(`SELECT id, page_id, verifier, verified_at, outcome, citation_checked
|
|
638
|
+
FROM verifications WHERE page_id = ? ORDER BY id DESC`).all(page.id);
|
|
639
|
+
return Promise.resolve(rows.map((r) => ({ ...r, citation_checked: r.citation_checked !== 0 })));
|
|
640
|
+
}
|
|
641
|
+
catch (e) {
|
|
642
|
+
return Promise.reject(e);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
589
645
|
listRevisions(slug) {
|
|
590
646
|
try {
|
|
591
647
|
const db = this.ensureOpen();
|
|
@@ -593,7 +649,7 @@ export class SqliteKnowledgeBackend {
|
|
|
593
649
|
if (!page) {
|
|
594
650
|
return Promise.reject(new Error(`listRevisions: page not found: ${slug}`));
|
|
595
651
|
}
|
|
596
|
-
const rows = db.prepare(`SELECT id, page_id, slug, op, replaced_at, LENGTH(body) AS body_length
|
|
652
|
+
const rows = db.prepare(`SELECT id, page_id, slug, op, replaced_at, actor, LENGTH(body) AS body_length
|
|
597
653
|
FROM page_revisions WHERE page_id = ? ORDER BY id DESC`).all(page.id);
|
|
598
654
|
return Promise.resolve(rows);
|
|
599
655
|
}
|
|
@@ -604,7 +660,7 @@ export class SqliteKnowledgeBackend {
|
|
|
604
660
|
getRevision(revisionId) {
|
|
605
661
|
try {
|
|
606
662
|
const db = this.ensureOpen();
|
|
607
|
-
const row = db.prepare('SELECT id, page_id, slug, op, replaced_at, body FROM page_revisions WHERE id = ?').get(revisionId);
|
|
663
|
+
const row = db.prepare('SELECT id, page_id, slug, op, replaced_at, actor, body FROM page_revisions WHERE id = ?').get(revisionId);
|
|
608
664
|
return Promise.resolve(row ?? null);
|
|
609
665
|
}
|
|
610
666
|
catch (e) {
|
|
@@ -630,10 +686,10 @@ export class SqliteKnowledgeBackend {
|
|
|
630
686
|
const tx = db.transaction(() => {
|
|
631
687
|
// The body being displaced is itself preserved — a restore must
|
|
632
688
|
// never be the second irreversible overwrite in the story.
|
|
633
|
-
snapshotId = this.snapshotRevision(db, page.id, input.slug, page.body, 'history-restore', timestamp);
|
|
689
|
+
snapshotId = this.snapshotRevision(db, page.id, input.slug, page.body, 'history-restore', timestamp, input.actor ?? null);
|
|
634
690
|
db.prepare('UPDATE pages SET body = ?, updated = ? WHERE id = ?').run(revision.body, timestamp, page.id);
|
|
635
691
|
});
|
|
636
|
-
retryWrite(() => tx());
|
|
692
|
+
retryWrite(() => tx.immediate());
|
|
637
693
|
return Promise.resolve({
|
|
638
694
|
slug: input.slug,
|
|
639
695
|
revision_id: input.revision_id,
|
|
@@ -646,12 +702,13 @@ export class SqliteKnowledgeBackend {
|
|
|
646
702
|
}
|
|
647
703
|
}
|
|
648
704
|
/**
|
|
649
|
-
* Insert a body snapshot and prune beyond the per-page cap.
|
|
650
|
-
* transaction
|
|
651
|
-
*
|
|
705
|
+
* Insert a body snapshot and prune beyond the per-page cap. Always called
|
|
706
|
+
* inside a transaction owned by the caller (writePage or restoreRevision).
|
|
707
|
+
* The inner retryWrite calls run bare statements — safe inside an outer
|
|
708
|
+
* transaction; the outer transaction is the atomicity boundary.
|
|
652
709
|
*/
|
|
653
|
-
snapshotRevision(db, pageId, slug, body, op, timestamp) {
|
|
654
|
-
const result = retryWrite(() => db.prepare('INSERT INTO page_revisions (page_id, slug, body, op, replaced_at) VALUES (?, ?, ?, ?, ?)').run(pageId, slug, body, op, timestamp));
|
|
710
|
+
snapshotRevision(db, pageId, slug, body, op, timestamp, actor = null) {
|
|
711
|
+
const result = retryWrite(() => db.prepare('INSERT INTO page_revisions (page_id, slug, body, op, replaced_at, actor) VALUES (?, ?, ?, ?, ?, ?)').run(pageId, slug, body, op, timestamp, actor));
|
|
655
712
|
retryWrite(() => db.prepare(`DELETE FROM page_revisions WHERE page_id = ? AND id NOT IN (
|
|
656
713
|
SELECT id FROM page_revisions WHERE page_id = ? ORDER BY id DESC LIMIT ?
|
|
657
714
|
)`).run(pageId, pageId, MAX_REVISIONS_PER_PAGE));
|
|
@@ -675,13 +732,24 @@ export class SqliteKnowledgeBackend {
|
|
|
675
732
|
added++;
|
|
676
733
|
}
|
|
677
734
|
});
|
|
678
|
-
retryWrite(() => tx());
|
|
735
|
+
retryWrite(() => tx.immediate());
|
|
679
736
|
return Promise.resolve(added);
|
|
680
737
|
}
|
|
681
738
|
catch (e) {
|
|
682
739
|
return Promise.reject(e);
|
|
683
740
|
}
|
|
684
741
|
}
|
|
742
|
+
getAccessStats(pageId) {
|
|
743
|
+
try {
|
|
744
|
+
const db = this.ensureOpen();
|
|
745
|
+
const rows = db.prepare(`SELECT identity, hit_count, last_accessed FROM knowledge_access
|
|
746
|
+
WHERE page_id = ? ORDER BY last_accessed DESC`).all(pageId);
|
|
747
|
+
return Promise.resolve(rows);
|
|
748
|
+
}
|
|
749
|
+
catch (e) {
|
|
750
|
+
return Promise.reject(e);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
685
753
|
close() {
|
|
686
754
|
if (this.db) {
|
|
687
755
|
this.db.close();
|
|
@@ -689,6 +757,29 @@ export class SqliteKnowledgeBackend {
|
|
|
689
757
|
}
|
|
690
758
|
}
|
|
691
759
|
// ── Internals ──
|
|
760
|
+
/**
|
|
761
|
+
* Stamp a real read on each page id: bump the shared pages.hit_count /
|
|
762
|
+
* last_accessed aggregate (unchanged legacy behavior — this is what
|
|
763
|
+
* knowledge_maintain's cold/expansion ranking reads) AND upsert the
|
|
764
|
+
* per-identity row in knowledge_access (t-677 attribution). Both updates
|
|
765
|
+
* happen in one transaction so a crash between them can't desync the
|
|
766
|
+
* aggregate from the per-reader breakdown.
|
|
767
|
+
*/
|
|
768
|
+
recordAccess(db, pageIds, identity, now) {
|
|
769
|
+
const bumpPage = db.prepare('UPDATE pages SET last_accessed = ?, hit_count = hit_count + 1 WHERE id = ?');
|
|
770
|
+
const upsertAccess = db.prepare(`INSERT INTO knowledge_access (page_id, identity, hit_count, last_accessed)
|
|
771
|
+
VALUES (?, ?, 1, ?)
|
|
772
|
+
ON CONFLICT(page_id, identity) DO UPDATE SET
|
|
773
|
+
hit_count = hit_count + 1,
|
|
774
|
+
last_accessed = excluded.last_accessed`);
|
|
775
|
+
const tx = db.transaction((ids) => {
|
|
776
|
+
for (const id of ids) {
|
|
777
|
+
bumpPage.run(now, id);
|
|
778
|
+
upsertAccess.run(id, identity, now);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
retryWrite(() => tx(pageIds));
|
|
782
|
+
}
|
|
692
783
|
ensureOpen() {
|
|
693
784
|
if (!this.db) {
|
|
694
785
|
throw new Error('KnowledgeBackend is already closed');
|
|
@@ -715,7 +806,8 @@ export class SqliteKnowledgeBackend {
|
|
|
715
806
|
last_accessed TEXT,
|
|
716
807
|
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
717
808
|
verified_at TEXT,
|
|
718
|
-
freshness_anchor TEXT
|
|
809
|
+
freshness_anchor TEXT,
|
|
810
|
+
sla_class TEXT NOT NULL DEFAULT 'default'
|
|
719
811
|
)`,
|
|
720
812
|
`CREATE INDEX IF NOT EXISTS idx_pages_domain ON pages(domain)`,
|
|
721
813
|
`CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug)`,
|
|
@@ -749,6 +841,29 @@ export class SqliteKnowledgeBackend {
|
|
|
749
841
|
replaced_at TEXT NOT NULL
|
|
750
842
|
)`,
|
|
751
843
|
`CREATE INDEX IF NOT EXISTS idx_page_revisions_page ON page_revisions(page_id)`,
|
|
844
|
+
// Actor attribution (t-675): who verified a page, when, and what they
|
|
845
|
+
// found. Additive to pages.verified_at (which stays the fast single-
|
|
846
|
+
// column freshness stamp everything else queries/sorts on) — this
|
|
847
|
+
// table is the attributed audit trail underneath it.
|
|
848
|
+
`CREATE TABLE IF NOT EXISTS verifications (
|
|
849
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
850
|
+
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
851
|
+
verifier TEXT,
|
|
852
|
+
verified_at TEXT NOT NULL,
|
|
853
|
+
outcome TEXT NOT NULL DEFAULT 'confirmed',
|
|
854
|
+
citation_checked INTEGER NOT NULL DEFAULT 0
|
|
855
|
+
)`,
|
|
856
|
+
`CREATE INDEX IF NOT EXISTS idx_verifications_page ON verifications(page_id)`,
|
|
857
|
+
// Per-identity read attribution (t-677) — additive to pages.hit_count/
|
|
858
|
+
// last_accessed, never a replacement. See getAccessStats/recordAccess.
|
|
859
|
+
`CREATE TABLE IF NOT EXISTS knowledge_access (
|
|
860
|
+
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
861
|
+
identity TEXT NOT NULL,
|
|
862
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
863
|
+
last_accessed TEXT,
|
|
864
|
+
PRIMARY KEY (page_id, identity)
|
|
865
|
+
)`,
|
|
866
|
+
`CREATE INDEX IF NOT EXISTS idx_knowledge_access_page ON knowledge_access(page_id)`,
|
|
752
867
|
];
|
|
753
868
|
for (const sql of statements) {
|
|
754
869
|
this.db.prepare(sql).run();
|
|
@@ -771,9 +886,56 @@ export class SqliteKnowledgeBackend {
|
|
|
771
886
|
if (!pageCols.includes('version')) {
|
|
772
887
|
this.db.prepare('ALTER TABLE pages ADD COLUMN version TEXT').run();
|
|
773
888
|
}
|
|
889
|
+
// sla_class: machine-readable freshness SLA class (t-434, 2026-09-25).
|
|
890
|
+
// DEFAULT 'default' covers all rows created before this migration.
|
|
891
|
+
// Backfill then upgrades rows whose anchor or domain implies a non-default class.
|
|
892
|
+
if (!pageCols.includes('sla_class')) {
|
|
893
|
+
this.db.prepare("ALTER TABLE pages ADD COLUMN sla_class TEXT NOT NULL DEFAULT 'default'").run();
|
|
894
|
+
// Backfill: re-derive for every existing row.
|
|
895
|
+
const rows = this.db.prepare("SELECT id, freshness_anchor, domain FROM pages").all();
|
|
896
|
+
const update = this.db.prepare("UPDATE pages SET sla_class = ? WHERE id = ?");
|
|
897
|
+
const backfill = this.db.transaction(() => {
|
|
898
|
+
for (const row of rows) {
|
|
899
|
+
const klass = deriveSlaClass(row.freshness_anchor, row.domain);
|
|
900
|
+
if (klass !== 'default') {
|
|
901
|
+
update.run(klass, row.id);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
backfill();
|
|
906
|
+
}
|
|
907
|
+
// Actor attribution (t-675): identity that wrote v1 of the page. Set
|
|
908
|
+
// only at creation (writePage) — never backfilled, since there is no
|
|
909
|
+
// way to recover who actually wrote pre-existing rows.
|
|
910
|
+
if (!pageCols.includes('author')) {
|
|
911
|
+
this.db.prepare('ALTER TABLE pages ADD COLUMN author TEXT').run();
|
|
912
|
+
}
|
|
913
|
+
const revisionCols = this.db.prepare('PRAGMA table_info(page_revisions)').all().map((c) => c.name);
|
|
914
|
+
if (!revisionCols.includes('actor')) {
|
|
915
|
+
this.db.prepare('ALTER TABLE page_revisions ADD COLUMN actor TEXT').run();
|
|
916
|
+
}
|
|
774
917
|
}
|
|
775
918
|
}
|
|
776
919
|
// ── Helpers ──
|
|
920
|
+
/**
|
|
921
|
+
* SLA class derivation — deterministic, no model required (t-434).
|
|
922
|
+
*
|
|
923
|
+
* Priority:
|
|
924
|
+
* 1. version-anchored: freshness_anchor matches /v?\d+\.\d+/ → 'software' (14d)
|
|
925
|
+
* 2. device-domain: domain starts with a known device-domain prefix → 'device' (7d)
|
|
926
|
+
* 3. else → 'default' (30d)
|
|
927
|
+
*
|
|
928
|
+
* Callers may override with KnowledgePageInput.sla_class.
|
|
929
|
+
*/
|
|
930
|
+
const DEVICE_DOMAIN_PREFIXES = ['music/gear', 'monitor/hardware'];
|
|
931
|
+
const RE_SLA_VERSION = /v?\d+\.\d+/i;
|
|
932
|
+
export function deriveSlaClass(anchor, domain) {
|
|
933
|
+
if (anchor && RE_SLA_VERSION.test(anchor))
|
|
934
|
+
return 'software';
|
|
935
|
+
if (DEVICE_DOMAIN_PREFIXES.some((p) => domain === p || domain.startsWith(`${p}/`)))
|
|
936
|
+
return 'device';
|
|
937
|
+
return 'default';
|
|
938
|
+
}
|
|
777
939
|
function enforcePageBodyCap(body) {
|
|
778
940
|
if (body.length > MAX_PAGE_BODY_LENGTH) {
|
|
779
941
|
throw new Error(`Page body exceeds hard cap of ${MAX_PAGE_BODY_LENGTH} characters ` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Database } from 'better-sqlite3';
|
|
2
|
-
import type { MemoryBackend, MemoryInput, MemoryRef, RecallInput, MemoryMatch, ForgetInput, ForgetResult, UpdateInput, UpdateResult, PruneResult, ListInput, MemoryEntry, EmbeddingProvider, FindSimilarInput, AuditOptions, AuditReport, ArchiveInput, ArchiveResult, RestoreInput, RestoreResult, RecallObservation, MemoryRevisionMeta, MemoryRevision, MemoryRevisionRestoreInput, MemoryRevisionRestoreResult, MemorySupersededInput, MemorySupersededResult } from './types.js';
|
|
2
|
+
import type { MemoryBackend, MemoryInput, MemoryRef, RecallInput, MemoryMatch, ForgetInput, ForgetResult, UpdateInput, UpdateResult, PruneResult, ListInput, MemoryEntry, EmbeddingProvider, FindSimilarInput, AuditOptions, AuditReport, ArchiveInput, ArchiveResult, RestoreInput, RestoreResult, RecallObservation, MemoryRevisionMeta, MemoryRevision, MemoryRevisionRestoreInput, MemoryRevisionRestoreResult, MemorySupersededInput, MemorySupersededResult, MemoryMergeInput, MemoryMergeResult } from './types.js';
|
|
3
3
|
export interface SqliteVecConfig {
|
|
4
4
|
/** Absolute path to the SQLite database file */
|
|
5
5
|
dbPath: string;
|
|
@@ -32,8 +32,24 @@ export declare class SqliteVecBackend implements MemoryBackend {
|
|
|
32
32
|
getRevision(revisionId: number): Promise<MemoryRevision | null>;
|
|
33
33
|
restoreRevision(input: MemoryRevisionRestoreInput): Promise<MemoryRevisionRestoreResult>;
|
|
34
34
|
supersede(input: MemorySupersededInput): Promise<MemorySupersededResult>;
|
|
35
|
+
mergeMemories(input: MemoryMergeInput): Promise<MemoryMergeResult>;
|
|
35
36
|
close(): void;
|
|
36
37
|
getDatabase(): Database;
|
|
38
|
+
/**
|
|
39
|
+
* A transaction that reheats the active row holding `uniqKey` (times_seen
|
|
40
|
+
* + 1, updated = timestamp) and returns its ref, or returns undefined if no
|
|
41
|
+
* active row holds it. Run it with .immediate() on its own, or call it
|
|
42
|
+
* inside another transaction (better-sqlite3 nests it as a savepoint).
|
|
43
|
+
*/
|
|
44
|
+
private reheatTx;
|
|
45
|
+
/**
|
|
46
|
+
* The dedup key a row should hold for the given body, or NULL when another
|
|
47
|
+
* ACTIVE row already holds that key — the existing holder keeps it, same
|
|
48
|
+
* policy as the migration backfill (rebuildUniqKeys). Call inside the
|
|
49
|
+
* write transaction that changes the body, so the check and the write see
|
|
50
|
+
* the same state.
|
|
51
|
+
*/
|
|
52
|
+
private claimableUniq;
|
|
37
53
|
/**
|
|
38
54
|
* Insert a content snapshot into memory_revisions and prune beyond the per-memory
|
|
39
55
|
* cap. Mirrors the knowledge wing's snapshotRevision() in sqlite-knowledge.ts.
|
|
@@ -60,5 +76,20 @@ export declare class SqliteVecBackend implements MemoryBackend {
|
|
|
60
76
|
/** Stored embeddings for a set of memory ids (point lookups; pools are small). */
|
|
61
77
|
private loadVectors;
|
|
62
78
|
private initSchema;
|
|
79
|
+
/**
|
|
80
|
+
* Hard-delete rows by id. Every call is journaled to memory_deletions first
|
|
81
|
+
* (t-436) — this is the ONLY path that permanently destroys a memory, so it
|
|
82
|
+
* is the one place that must record what it destroyed. The journal table is
|
|
83
|
+
* standalone (keyed by ref, no FK to memories) so it survives the delete it
|
|
84
|
+
* describes.
|
|
85
|
+
*/
|
|
63
86
|
private deleteById;
|
|
87
|
+
/**
|
|
88
|
+
* The journal-then-delete transaction behind deleteById. Callers already
|
|
89
|
+
* inside a transaction (mergeMemories with hard_delete_losers) invoke it
|
|
90
|
+
* directly; better-sqlite3 nests it as a savepoint.
|
|
91
|
+
*/
|
|
92
|
+
private deleteByIdTx;
|
|
64
93
|
}
|
|
94
|
+
/** Float32Array view of a Buffer; copies only when alignment forces it. */
|
|
95
|
+
export declare function bufToF32(buf: Buffer): Float32Array;
|