@aionis/substrate 0.1.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 +29 -0
- package/LICENSE +201 -0
- package/README.md +222 -0
- package/dist/backup.d.ts +38 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +203 -0
- package/dist/backup.js.map +1 -0
- package/dist/event-log.d.ts +18 -0
- package/dist/event-log.d.ts.map +1 -0
- package/dist/event-log.js +157 -0
- package/dist/event-log.js.map +1 -0
- package/dist/file-substrate.d.ts +7 -0
- package/dist/file-substrate.d.ts.map +1 -0
- package/dist/file-substrate.js +409 -0
- package/dist/file-substrate.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-reference-corpus.d.ts +76 -0
- package/dist/runtime-reference-corpus.d.ts.map +1 -0
- package/dist/runtime-reference-corpus.js +308 -0
- package/dist/runtime-reference-corpus.js.map +1 -0
- package/dist/runtime-snapshot-corpus.d.ts +53 -0
- package/dist/runtime-snapshot-corpus.d.ts.map +1 -0
- package/dist/runtime-snapshot-corpus.js +176 -0
- package/dist/runtime-snapshot-corpus.js.map +1 -0
- package/dist/runtime-snapshot-importer.d.ts +27 -0
- package/dist/runtime-snapshot-importer.d.ts.map +1 -0
- package/dist/runtime-snapshot-importer.js +571 -0
- package/dist/runtime-snapshot-importer.js.map +1 -0
- package/dist/runtime-snapshot-parity.d.ts +40 -0
- package/dist/runtime-snapshot-parity.d.ts.map +1 -0
- package/dist/runtime-snapshot-parity.js +217 -0
- package/dist/runtime-snapshot-parity.js.map +1 -0
- package/dist/search.d.ts +3 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +157 -0
- package/dist/search.js.map +1 -0
- package/dist/sqlite-substrate.d.ts +7 -0
- package/dist/sqlite-substrate.d.ts.map +1 -0
- package/dist/sqlite-substrate.js +666 -0
- package/dist/sqlite-substrate.js.map +1 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/ADAPTER_CONTRACT.md +155 -0
- package/docs/API_USAGE.md +225 -0
- package/docs/BACKUP_RESTORE.md +108 -0
- package/docs/CHECKPOINT_COMPACTION.md +75 -0
- package/docs/EXTERNAL_ADMISSION_PARITY.md +98 -0
- package/docs/RUNTIME_DUAL_WRITE_EXPERIMENT.md +132 -0
- package/docs/RUNTIME_MAPPING.md +69 -0
- package/docs/RUNTIME_REFERENCE_CORPUS.md +129 -0
- package/docs/RUNTIME_SNAPSHOT_CORPUS.md +81 -0
- package/docs/RUNTIME_SNAPSHOT_IMPORT.md +120 -0
- package/docs/STORE_CONTRACT.md +181 -0
- package/examples/basic/README.md +19 -0
- package/examples/basic/index.mjs +112 -0
- package/package.json +65 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { applyAionisEvent, checksumAionisEvents, emptyReplayState } from "./event-log.js";
|
|
6
|
+
import { searchMemoryNodes } from "./search.js";
|
|
7
|
+
import { AIONIS_SUBSTRATE_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION } from "./types.js";
|
|
8
|
+
function requireNonEmpty(value, label) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
throw new Error(`${label} is required`);
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
function clampConfidence(value) {
|
|
15
|
+
if (!Number.isFinite(value))
|
|
16
|
+
throw new Error("confidence must be finite");
|
|
17
|
+
if (value < 0)
|
|
18
|
+
return 0;
|
|
19
|
+
if (value > 1)
|
|
20
|
+
return 1;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function normalizeStrings(values) {
|
|
24
|
+
return Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean)));
|
|
25
|
+
}
|
|
26
|
+
function parseJsonObject(raw) {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return {};
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
31
|
+
}
|
|
32
|
+
function parseJsonArray(raw) {
|
|
33
|
+
if (!raw)
|
|
34
|
+
return [];
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
37
|
+
}
|
|
38
|
+
function stringify(value) {
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
}
|
|
41
|
+
function rowToNode(row) {
|
|
42
|
+
return {
|
|
43
|
+
id: row.id,
|
|
44
|
+
scope: row.scope,
|
|
45
|
+
kind: row.kind,
|
|
46
|
+
title: row.title,
|
|
47
|
+
summary: row.summary,
|
|
48
|
+
lifecycle: row.lifecycle,
|
|
49
|
+
authority: row.authority,
|
|
50
|
+
confidence: row.confidence,
|
|
51
|
+
targetFiles: parseJsonArray(row.target_files_json).filter((item) => typeof item === "string"),
|
|
52
|
+
payloadRef: row.payload_ref,
|
|
53
|
+
agentId: row.agent_id,
|
|
54
|
+
teamId: row.team_id,
|
|
55
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
56
|
+
createdAt: row.created_at,
|
|
57
|
+
updatedAt: row.updated_at,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function rowToRelation(row) {
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
scope: row.scope,
|
|
64
|
+
kind: row.kind,
|
|
65
|
+
sourceId: row.source_id,
|
|
66
|
+
targetId: row.target_id,
|
|
67
|
+
confidence: row.confidence,
|
|
68
|
+
reasons: parseJsonArray(row.reasons_json).filter((item) => typeof item === "string"),
|
|
69
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
70
|
+
createdAt: row.created_at,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function rowToFeedback(row) {
|
|
74
|
+
return {
|
|
75
|
+
id: row.id,
|
|
76
|
+
scope: row.scope,
|
|
77
|
+
memoryId: row.memory_id,
|
|
78
|
+
outcome: row.outcome,
|
|
79
|
+
strength: row.strength,
|
|
80
|
+
runId: row.run_id,
|
|
81
|
+
evidenceRef: row.evidence_ref,
|
|
82
|
+
createdAt: row.created_at,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function rowToDecision(row) {
|
|
86
|
+
return {
|
|
87
|
+
id: row.id,
|
|
88
|
+
scope: row.scope,
|
|
89
|
+
query: row.query,
|
|
90
|
+
decisions: parseJsonArray(row.decisions_json),
|
|
91
|
+
createdAt: row.created_at,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function rowToEvent(row) {
|
|
95
|
+
return {
|
|
96
|
+
id: row.id,
|
|
97
|
+
sequence: row.sequence,
|
|
98
|
+
type: row.type,
|
|
99
|
+
createdAt: row.created_at,
|
|
100
|
+
payload: JSON.parse(row.payload_json),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function relationBlocksDirectUse(kind) {
|
|
104
|
+
return kind === "supersedes" || kind === "contradicts" || kind === "invalidates";
|
|
105
|
+
}
|
|
106
|
+
function relationRequiresRehydrate(kind) {
|
|
107
|
+
return kind === "requires_payload";
|
|
108
|
+
}
|
|
109
|
+
function sortNodes(nodes) {
|
|
110
|
+
return [...nodes].sort((a, b) => {
|
|
111
|
+
const byTime = b.updatedAt.localeCompare(a.updatedAt);
|
|
112
|
+
if (byTime !== 0)
|
|
113
|
+
return byTime;
|
|
114
|
+
return a.id.localeCompare(b.id);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function reasonsFor(node, relations) {
|
|
118
|
+
const incoming = relations.filter((relation) => relation.scope === node.scope && relation.targetId === node.id);
|
|
119
|
+
const blockingRelation = incoming.find((relation) => relation.confidence >= 0.65 && relationBlocksDirectUse(relation.kind));
|
|
120
|
+
if (blockingRelation) {
|
|
121
|
+
return {
|
|
122
|
+
action: "do_not_use",
|
|
123
|
+
reasons: [{
|
|
124
|
+
code: "blocked_by_relation",
|
|
125
|
+
detail: `${blockingRelation.kind} relation reached confidence ${blockingRelation.confidence}`,
|
|
126
|
+
relationId: blockingRelation.id,
|
|
127
|
+
}],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const rehydrateRelation = incoming.find((relation) => relation.confidence >= 0.55 && relationRequiresRehydrate(relation.kind));
|
|
131
|
+
if (node.lifecycle === "archived" || node.lifecycle === "rehydrate_required" || rehydrateRelation) {
|
|
132
|
+
return {
|
|
133
|
+
action: "rehydrate",
|
|
134
|
+
reasons: [{
|
|
135
|
+
code: rehydrateRelation ? "relation_requires_payload" : "payload_required",
|
|
136
|
+
detail: rehydrateRelation
|
|
137
|
+
? `${rehydrateRelation.kind} relation requests payload recovery`
|
|
138
|
+
: "memory is archived or marked as requiring payload rehydration",
|
|
139
|
+
relationId: rehydrateRelation?.id,
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (node.lifecycle === "suppressed" || node.lifecycle === "retired" || node.lifecycle === "blocked" || node.authority === "rejected") {
|
|
144
|
+
return {
|
|
145
|
+
action: "do_not_use",
|
|
146
|
+
reasons: [{ code: "lifecycle_blocks_direct_use", detail: `lifecycle=${node.lifecycle}, authority=${node.authority}` }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (node.lifecycle === "active" && (node.authority === "trusted" || node.authority === "verified")) {
|
|
150
|
+
return {
|
|
151
|
+
action: "use_now",
|
|
152
|
+
reasons: [{ code: "active_authoritative_memory", detail: `lifecycle=${node.lifecycle}, authority=${node.authority}` }],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
action: "inspect_before_use",
|
|
157
|
+
reasons: [{ code: "insufficient_authority", detail: `lifecycle=${node.lifecycle}, authority=${node.authority}` }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function readMetadataValue(db, key) {
|
|
161
|
+
const row = db.prepare("SELECT value FROM substrate_metadata WHERE key = ?").get(key);
|
|
162
|
+
return row?.value ?? null;
|
|
163
|
+
}
|
|
164
|
+
function writeMetadataValue(db, key, value) {
|
|
165
|
+
db.prepare(`
|
|
166
|
+
INSERT INTO substrate_metadata (key, value)
|
|
167
|
+
VALUES (?, ?)
|
|
168
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
169
|
+
`).run(key, value);
|
|
170
|
+
}
|
|
171
|
+
function migrateSchema(db, migratedAt) {
|
|
172
|
+
db.exec(`
|
|
173
|
+
PRAGMA journal_mode = WAL;
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS substrate_metadata (
|
|
176
|
+
key TEXT PRIMARY KEY,
|
|
177
|
+
value TEXT NOT NULL
|
|
178
|
+
);
|
|
179
|
+
`);
|
|
180
|
+
const existingVersionRaw = readMetadataValue(db, "schema_version");
|
|
181
|
+
const existingVersion = existingVersionRaw === null ? null : Number(existingVersionRaw);
|
|
182
|
+
if (existingVersion !== null && (!Number.isInteger(existingVersion) || existingVersion < 1)) {
|
|
183
|
+
throw new Error(`invalid Aionis Substrate schema version: ${existingVersionRaw}`);
|
|
184
|
+
}
|
|
185
|
+
if (existingVersion !== null && existingVersion > CURRENT_SCHEMA_VERSION) {
|
|
186
|
+
throw new Error(`unsupported Aionis Substrate schema version ${existingVersion}; current runtime supports ${CURRENT_SCHEMA_VERSION}`);
|
|
187
|
+
}
|
|
188
|
+
const userVersion = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
189
|
+
if (userVersion > CURRENT_SCHEMA_VERSION) {
|
|
190
|
+
throw new Error(`unsupported SQLite user_version ${userVersion}; current runtime supports ${CURRENT_SCHEMA_VERSION}`);
|
|
191
|
+
}
|
|
192
|
+
db.exec(`
|
|
193
|
+
CREATE TABLE IF NOT EXISTS substrate_events (
|
|
194
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
195
|
+
id TEXT NOT NULL UNIQUE,
|
|
196
|
+
type TEXT NOT NULL,
|
|
197
|
+
created_at TEXT NOT NULL,
|
|
198
|
+
payload_json TEXT NOT NULL
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
CREATE TABLE IF NOT EXISTS memory_nodes (
|
|
202
|
+
scope TEXT NOT NULL,
|
|
203
|
+
id TEXT NOT NULL,
|
|
204
|
+
kind TEXT NOT NULL,
|
|
205
|
+
title TEXT,
|
|
206
|
+
summary TEXT NOT NULL,
|
|
207
|
+
lifecycle TEXT NOT NULL,
|
|
208
|
+
authority TEXT NOT NULL,
|
|
209
|
+
confidence REAL NOT NULL,
|
|
210
|
+
target_files_json TEXT NOT NULL,
|
|
211
|
+
payload_ref TEXT,
|
|
212
|
+
agent_id TEXT,
|
|
213
|
+
team_id TEXT,
|
|
214
|
+
metadata_json TEXT NOT NULL,
|
|
215
|
+
created_at TEXT NOT NULL,
|
|
216
|
+
updated_at TEXT NOT NULL,
|
|
217
|
+
PRIMARY KEY(scope, id)
|
|
218
|
+
);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_memory_nodes_scope_updated ON memory_nodes(scope, updated_at DESC, id);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_memory_nodes_scope_lifecycle ON memory_nodes(scope, lifecycle, authority);
|
|
221
|
+
|
|
222
|
+
CREATE TABLE IF NOT EXISTS memory_relations (
|
|
223
|
+
scope TEXT NOT NULL,
|
|
224
|
+
id TEXT NOT NULL,
|
|
225
|
+
kind TEXT NOT NULL,
|
|
226
|
+
source_id TEXT NOT NULL,
|
|
227
|
+
target_id TEXT NOT NULL,
|
|
228
|
+
confidence REAL NOT NULL,
|
|
229
|
+
reasons_json TEXT NOT NULL,
|
|
230
|
+
metadata_json TEXT NOT NULL,
|
|
231
|
+
created_at TEXT NOT NULL,
|
|
232
|
+
PRIMARY KEY(scope, id)
|
|
233
|
+
);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_memory_relations_target ON memory_relations(scope, target_id, kind);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_memory_relations_source ON memory_relations(scope, source_id, kind);
|
|
236
|
+
|
|
237
|
+
CREATE TABLE IF NOT EXISTS memory_feedback (
|
|
238
|
+
scope TEXT NOT NULL,
|
|
239
|
+
id TEXT NOT NULL,
|
|
240
|
+
memory_id TEXT NOT NULL,
|
|
241
|
+
outcome TEXT NOT NULL,
|
|
242
|
+
strength TEXT NOT NULL,
|
|
243
|
+
run_id TEXT,
|
|
244
|
+
evidence_ref TEXT,
|
|
245
|
+
created_at TEXT NOT NULL,
|
|
246
|
+
PRIMARY KEY(scope, id)
|
|
247
|
+
);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_memory_feedback_memory ON memory_feedback(scope, memory_id, created_at DESC);
|
|
249
|
+
|
|
250
|
+
CREATE TABLE IF NOT EXISTS decision_traces (
|
|
251
|
+
scope TEXT NOT NULL,
|
|
252
|
+
id TEXT NOT NULL,
|
|
253
|
+
query TEXT,
|
|
254
|
+
decisions_json TEXT NOT NULL,
|
|
255
|
+
created_at TEXT NOT NULL,
|
|
256
|
+
PRIMARY KEY(scope, id)
|
|
257
|
+
);
|
|
258
|
+
CREATE INDEX IF NOT EXISTS idx_decision_traces_scope_created ON decision_traces(scope, created_at DESC, id);
|
|
259
|
+
`);
|
|
260
|
+
if (existingVersion === null) {
|
|
261
|
+
writeMetadataValue(db, "created_at", migratedAt);
|
|
262
|
+
}
|
|
263
|
+
writeMetadataValue(db, "adapter", "sqlite");
|
|
264
|
+
writeMetadataValue(db, "schema_version", String(CURRENT_SCHEMA_VERSION));
|
|
265
|
+
writeMetadataValue(db, "last_migrated_at", migratedAt);
|
|
266
|
+
db.exec(`PRAGMA user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
267
|
+
}
|
|
268
|
+
export async function openSqliteAionisSubstrate(options) {
|
|
269
|
+
await mkdir(dirname(options.path), { recursive: true });
|
|
270
|
+
const db = new DatabaseSync(options.path);
|
|
271
|
+
const now = options.now ?? (() => new Date());
|
|
272
|
+
let tail = Promise.resolve();
|
|
273
|
+
migrateSchema(db, now().toISOString());
|
|
274
|
+
function isoNow() {
|
|
275
|
+
return now().toISOString();
|
|
276
|
+
}
|
|
277
|
+
function enqueue(fn) {
|
|
278
|
+
const next = tail.then(fn, fn);
|
|
279
|
+
tail = next.then(() => undefined, () => undefined);
|
|
280
|
+
return next;
|
|
281
|
+
}
|
|
282
|
+
function transaction(fn) {
|
|
283
|
+
db.exec("BEGIN IMMEDIATE");
|
|
284
|
+
try {
|
|
285
|
+
const out = fn();
|
|
286
|
+
db.exec("COMMIT");
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
db.exec("ROLLBACK");
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function appendEvent(type, createdAt, payload) {
|
|
295
|
+
const id = randomUUID();
|
|
296
|
+
db.prepare("INSERT INTO substrate_events (id, type, created_at, payload_json) VALUES (?, ?, ?, ?)")
|
|
297
|
+
.run(id, type, createdAt, stringify(payload));
|
|
298
|
+
const sequence = Number(db.prepare("SELECT last_insert_rowid() AS sequence").get().sequence);
|
|
299
|
+
return { id, sequence, type, createdAt, payload };
|
|
300
|
+
}
|
|
301
|
+
function listEventsSync() {
|
|
302
|
+
return db.prepare(`
|
|
303
|
+
SELECT sequence, id, type, created_at, payload_json
|
|
304
|
+
FROM substrate_events
|
|
305
|
+
ORDER BY sequence ASC
|
|
306
|
+
`).all().map(rowToEvent);
|
|
307
|
+
}
|
|
308
|
+
function listAllNodesSync() {
|
|
309
|
+
return db.prepare(`
|
|
310
|
+
SELECT *
|
|
311
|
+
FROM memory_nodes
|
|
312
|
+
ORDER BY scope ASC, id ASC
|
|
313
|
+
`).all().map(rowToNode);
|
|
314
|
+
}
|
|
315
|
+
function listAllRelationsSync() {
|
|
316
|
+
return db.prepare(`
|
|
317
|
+
SELECT *
|
|
318
|
+
FROM memory_relations
|
|
319
|
+
ORDER BY scope ASC, id ASC
|
|
320
|
+
`).all().map(rowToRelation);
|
|
321
|
+
}
|
|
322
|
+
function listAllFeedbackSync() {
|
|
323
|
+
return db.prepare(`
|
|
324
|
+
SELECT *
|
|
325
|
+
FROM memory_feedback
|
|
326
|
+
ORDER BY scope ASC, id ASC
|
|
327
|
+
`).all().map(rowToFeedback);
|
|
328
|
+
}
|
|
329
|
+
function listAllDecisionsSync() {
|
|
330
|
+
return db.prepare(`
|
|
331
|
+
SELECT *
|
|
332
|
+
FROM decision_traces
|
|
333
|
+
ORDER BY scope ASC, id ASC
|
|
334
|
+
`).all().map(rowToDecision);
|
|
335
|
+
}
|
|
336
|
+
function getNodeSync(scope, id) {
|
|
337
|
+
const row = db.prepare("SELECT * FROM memory_nodes WHERE scope = ? AND id = ?").get(scope, id);
|
|
338
|
+
return row ? rowToNode(row) : null;
|
|
339
|
+
}
|
|
340
|
+
function insertNodeRow(node) {
|
|
341
|
+
db.prepare(`
|
|
342
|
+
INSERT INTO memory_nodes (
|
|
343
|
+
scope, id, kind, title, summary, lifecycle, authority, confidence, target_files_json,
|
|
344
|
+
payload_ref, agent_id, team_id, metadata_json, created_at, updated_at
|
|
345
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
346
|
+
ON CONFLICT(scope, id) DO UPDATE SET
|
|
347
|
+
kind = excluded.kind,
|
|
348
|
+
title = excluded.title,
|
|
349
|
+
summary = excluded.summary,
|
|
350
|
+
lifecycle = excluded.lifecycle,
|
|
351
|
+
authority = excluded.authority,
|
|
352
|
+
confidence = excluded.confidence,
|
|
353
|
+
target_files_json = excluded.target_files_json,
|
|
354
|
+
payload_ref = excluded.payload_ref,
|
|
355
|
+
agent_id = excluded.agent_id,
|
|
356
|
+
team_id = excluded.team_id,
|
|
357
|
+
metadata_json = excluded.metadata_json,
|
|
358
|
+
updated_at = excluded.updated_at
|
|
359
|
+
`).run(node.scope, node.id, node.kind, node.title ?? null, node.summary, node.lifecycle, node.authority, node.confidence, stringify(node.targetFiles ?? []), node.payloadRef ?? null, node.agentId ?? null, node.teamId ?? null, stringify(node.metadata ?? {}), node.createdAt, node.updatedAt);
|
|
360
|
+
}
|
|
361
|
+
function insertRelationRow(relation) {
|
|
362
|
+
db.prepare(`
|
|
363
|
+
INSERT INTO memory_relations (
|
|
364
|
+
scope, id, kind, source_id, target_id, confidence, reasons_json, metadata_json, created_at
|
|
365
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
366
|
+
ON CONFLICT(scope, id) DO UPDATE SET
|
|
367
|
+
kind = excluded.kind,
|
|
368
|
+
source_id = excluded.source_id,
|
|
369
|
+
target_id = excluded.target_id,
|
|
370
|
+
confidence = excluded.confidence,
|
|
371
|
+
reasons_json = excluded.reasons_json,
|
|
372
|
+
metadata_json = excluded.metadata_json
|
|
373
|
+
`).run(relation.scope, relation.id, relation.kind, relation.sourceId, relation.targetId, relation.confidence, stringify(relation.reasons), stringify(relation.metadata ?? {}), relation.createdAt);
|
|
374
|
+
}
|
|
375
|
+
function insertFeedbackRow(feedback) {
|
|
376
|
+
db.prepare(`
|
|
377
|
+
INSERT INTO memory_feedback (
|
|
378
|
+
scope, id, memory_id, outcome, strength, run_id, evidence_ref, created_at
|
|
379
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
380
|
+
ON CONFLICT(scope, id) DO UPDATE SET
|
|
381
|
+
memory_id = excluded.memory_id,
|
|
382
|
+
outcome = excluded.outcome,
|
|
383
|
+
strength = excluded.strength,
|
|
384
|
+
run_id = excluded.run_id,
|
|
385
|
+
evidence_ref = excluded.evidence_ref
|
|
386
|
+
`).run(feedback.scope, feedback.id, feedback.memoryId, feedback.outcome, feedback.strength, feedback.runId ?? null, feedback.evidenceRef ?? null, feedback.createdAt);
|
|
387
|
+
}
|
|
388
|
+
function insertDecisionRow(trace) {
|
|
389
|
+
db.prepare(`
|
|
390
|
+
INSERT INTO decision_traces (scope, id, query, decisions_json, created_at)
|
|
391
|
+
VALUES (?, ?, ?, ?, ?)
|
|
392
|
+
ON CONFLICT(scope, id) DO UPDATE SET
|
|
393
|
+
query = excluded.query,
|
|
394
|
+
decisions_json = excluded.decisions_json
|
|
395
|
+
`).run(trace.scope, trace.id, trace.query ?? null, stringify(trace.decisions), trace.createdAt);
|
|
396
|
+
}
|
|
397
|
+
function buildContext(input) {
|
|
398
|
+
const maxPerBucket = input.maxPerBucket ?? Number.POSITIVE_INFINITY;
|
|
399
|
+
const nodes = sortNodes(db.prepare("SELECT * FROM memory_nodes WHERE scope = ?").all(input.scope).map(rowToNode));
|
|
400
|
+
const relations = db.prepare("SELECT * FROM memory_relations WHERE scope = ?").all(input.scope).map(rowToRelation);
|
|
401
|
+
const buckets = {
|
|
402
|
+
use_now: [],
|
|
403
|
+
inspect_before_use: [],
|
|
404
|
+
do_not_use: [],
|
|
405
|
+
rehydrate: [],
|
|
406
|
+
};
|
|
407
|
+
const decisions = [];
|
|
408
|
+
for (const node of nodes) {
|
|
409
|
+
const decision = reasonsFor(node, relations);
|
|
410
|
+
decisions.push({ memoryId: node.id, action: decision.action, reasons: decision.reasons });
|
|
411
|
+
buckets[decision.action].push(node);
|
|
412
|
+
}
|
|
413
|
+
for (const action of Object.keys(buckets)) {
|
|
414
|
+
buckets[action] = buckets[action].slice(0, maxPerBucket);
|
|
415
|
+
}
|
|
416
|
+
const trace = {
|
|
417
|
+
id: randomUUID(),
|
|
418
|
+
scope: input.scope,
|
|
419
|
+
query: input.query ?? null,
|
|
420
|
+
decisions,
|
|
421
|
+
createdAt: isoNow(),
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
scope: input.scope,
|
|
425
|
+
use_now: buckets.use_now,
|
|
426
|
+
inspect_before_use: buckets.inspect_before_use,
|
|
427
|
+
do_not_use: buckets.do_not_use,
|
|
428
|
+
rehydrate: buckets.rehydrate,
|
|
429
|
+
decision_trace: trace,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
async getStoreInfo() {
|
|
434
|
+
return await enqueue(() => {
|
|
435
|
+
const eventCount = Number(db.prepare("SELECT count(*) AS count FROM substrate_events").get().count);
|
|
436
|
+
const lastSequence = Number(db.prepare("SELECT coalesce(max(sequence), 0) AS sequence FROM substrate_events").get().sequence);
|
|
437
|
+
const schemaVersion = Number(readMetadataValue(db, "schema_version") ?? CURRENT_SCHEMA_VERSION);
|
|
438
|
+
return {
|
|
439
|
+
adapter: "sqlite",
|
|
440
|
+
schemaVersion,
|
|
441
|
+
lastSequence,
|
|
442
|
+
eventCount,
|
|
443
|
+
};
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
async compact() {
|
|
447
|
+
return await enqueue(() => transaction(() => {
|
|
448
|
+
const beforeEvents = listEventsSync();
|
|
449
|
+
const before = {
|
|
450
|
+
eventCount: beforeEvents.length,
|
|
451
|
+
lastSequence: Number(db.prepare("SELECT coalesce(max(sequence), 0) AS sequence FROM substrate_events").get().sequence),
|
|
452
|
+
eventsSha256: checksumAionisEvents(beforeEvents),
|
|
453
|
+
};
|
|
454
|
+
if (beforeEvents.length === 0) {
|
|
455
|
+
return {
|
|
456
|
+
adapter: "sqlite",
|
|
457
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
458
|
+
compacted: false,
|
|
459
|
+
before,
|
|
460
|
+
after: {
|
|
461
|
+
eventCount: 0,
|
|
462
|
+
lastSequence: 0,
|
|
463
|
+
checkpointEventId: null,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const checkpoint = {
|
|
468
|
+
id: randomUUID(),
|
|
469
|
+
sequence: 1,
|
|
470
|
+
type: "substrate.checkpoint.created",
|
|
471
|
+
createdAt: isoNow(),
|
|
472
|
+
payload: {
|
|
473
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
474
|
+
coveredEventCount: beforeEvents.length,
|
|
475
|
+
coveredLastSequence: before.lastSequence,
|
|
476
|
+
coveredEventsSha256: before.eventsSha256,
|
|
477
|
+
state: {
|
|
478
|
+
nodes: listAllNodesSync(),
|
|
479
|
+
relations: listAllRelationsSync(),
|
|
480
|
+
feedback: listAllFeedbackSync(),
|
|
481
|
+
decisions: listAllDecisionsSync(),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
applyAionisEvent(emptyReplayState(), checkpoint);
|
|
486
|
+
db.exec(`
|
|
487
|
+
DELETE FROM substrate_events;
|
|
488
|
+
DELETE FROM sqlite_sequence WHERE name = 'substrate_events';
|
|
489
|
+
`);
|
|
490
|
+
db.prepare(`
|
|
491
|
+
INSERT INTO substrate_events (sequence, id, type, created_at, payload_json)
|
|
492
|
+
VALUES (?, ?, ?, ?, ?)
|
|
493
|
+
`).run(checkpoint.sequence, checkpoint.id, checkpoint.type, checkpoint.createdAt, stringify(checkpoint.payload));
|
|
494
|
+
writeMetadataValue(db, "last_compacted_at", checkpoint.createdAt);
|
|
495
|
+
writeMetadataValue(db, "last_compacted_event_count", String(before.eventCount));
|
|
496
|
+
writeMetadataValue(db, "last_compacted_last_sequence", String(before.lastSequence));
|
|
497
|
+
writeMetadataValue(db, "last_compacted_events_sha256", before.eventsSha256);
|
|
498
|
+
return {
|
|
499
|
+
adapter: "sqlite",
|
|
500
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
501
|
+
compacted: true,
|
|
502
|
+
before,
|
|
503
|
+
after: {
|
|
504
|
+
eventCount: 1,
|
|
505
|
+
lastSequence: 1,
|
|
506
|
+
checkpointEventId: checkpoint.id,
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}));
|
|
510
|
+
},
|
|
511
|
+
async putNode(input) {
|
|
512
|
+
return await enqueue(() => transaction(() => {
|
|
513
|
+
const ts = isoNow();
|
|
514
|
+
const id = requireNonEmpty(input.id ?? randomUUID(), "memory id");
|
|
515
|
+
const existing = getNodeSync(input.scope, id);
|
|
516
|
+
const createdAt = existing?.createdAt ?? input.createdAt ?? ts;
|
|
517
|
+
const updatedAt = input.updatedAt ?? ts;
|
|
518
|
+
const node = {
|
|
519
|
+
id,
|
|
520
|
+
scope: requireNonEmpty(input.scope, "scope"),
|
|
521
|
+
kind: input.kind,
|
|
522
|
+
title: input.title ?? existing?.title ?? null,
|
|
523
|
+
summary: requireNonEmpty(input.summary, "summary"),
|
|
524
|
+
lifecycle: input.lifecycle ?? existing?.lifecycle ?? "candidate",
|
|
525
|
+
authority: input.authority ?? existing?.authority ?? "unknown",
|
|
526
|
+
confidence: clampConfidence(input.confidence ?? existing?.confidence ?? 0.5),
|
|
527
|
+
targetFiles: normalizeStrings(input.targetFiles ?? existing?.targetFiles),
|
|
528
|
+
payloadRef: input.payloadRef ?? existing?.payloadRef ?? null,
|
|
529
|
+
agentId: input.agentId ?? existing?.agentId ?? null,
|
|
530
|
+
teamId: input.teamId ?? existing?.teamId ?? null,
|
|
531
|
+
metadata: input.metadata ?? existing?.metadata ?? {},
|
|
532
|
+
createdAt,
|
|
533
|
+
updatedAt,
|
|
534
|
+
};
|
|
535
|
+
appendEvent("memory.node.upsert", ts, node);
|
|
536
|
+
insertNodeRow(node);
|
|
537
|
+
return node;
|
|
538
|
+
}));
|
|
539
|
+
},
|
|
540
|
+
async transitionLifecycle(input) {
|
|
541
|
+
return await enqueue(() => transaction(() => {
|
|
542
|
+
const ts = isoNow();
|
|
543
|
+
const current = getNodeSync(input.scope, input.memoryId);
|
|
544
|
+
if (!current)
|
|
545
|
+
throw new Error(`cannot transition missing memory node: ${input.memoryId}`);
|
|
546
|
+
const node = {
|
|
547
|
+
...current,
|
|
548
|
+
lifecycle: input.lifecycle,
|
|
549
|
+
authority: input.authority ?? current.authority,
|
|
550
|
+
confidence: input.confidence === undefined ? current.confidence : clampConfidence(input.confidence),
|
|
551
|
+
updatedAt: ts,
|
|
552
|
+
metadata: {
|
|
553
|
+
...(current.metadata ?? {}),
|
|
554
|
+
last_lifecycle_transition_reason: requireNonEmpty(input.reason, "reason"),
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
appendEvent("memory.lifecycle.transition", ts, {
|
|
558
|
+
scope: requireNonEmpty(input.scope, "scope"),
|
|
559
|
+
memoryId: requireNonEmpty(input.memoryId, "memoryId"),
|
|
560
|
+
lifecycle: input.lifecycle,
|
|
561
|
+
authority: input.authority,
|
|
562
|
+
confidence: input.confidence,
|
|
563
|
+
reason: requireNonEmpty(input.reason, "reason"),
|
|
564
|
+
});
|
|
565
|
+
insertNodeRow(node);
|
|
566
|
+
return node;
|
|
567
|
+
}));
|
|
568
|
+
},
|
|
569
|
+
async putRelation(input) {
|
|
570
|
+
return await enqueue(() => transaction(() => {
|
|
571
|
+
const ts = isoNow();
|
|
572
|
+
const scope = requireNonEmpty(input.scope, "scope");
|
|
573
|
+
const sourceId = requireNonEmpty(input.sourceId, "sourceId");
|
|
574
|
+
const targetId = requireNonEmpty(input.targetId, "targetId");
|
|
575
|
+
if (!getNodeSync(scope, sourceId))
|
|
576
|
+
throw new Error(`cannot relate missing source memory node: ${sourceId}`);
|
|
577
|
+
if (!getNodeSync(scope, targetId))
|
|
578
|
+
throw new Error(`cannot relate missing target memory node: ${targetId}`);
|
|
579
|
+
const relation = {
|
|
580
|
+
id: requireNonEmpty(input.id ?? randomUUID(), "relation id"),
|
|
581
|
+
scope,
|
|
582
|
+
kind: input.kind,
|
|
583
|
+
sourceId,
|
|
584
|
+
targetId,
|
|
585
|
+
confidence: clampConfidence(input.confidence ?? 0.7),
|
|
586
|
+
reasons: normalizeStrings(input.reasons),
|
|
587
|
+
metadata: input.metadata ?? {},
|
|
588
|
+
createdAt: input.createdAt ?? ts,
|
|
589
|
+
};
|
|
590
|
+
appendEvent("memory.relation.upsert", ts, relation);
|
|
591
|
+
insertRelationRow(relation);
|
|
592
|
+
return relation;
|
|
593
|
+
}));
|
|
594
|
+
},
|
|
595
|
+
async recordFeedback(input) {
|
|
596
|
+
return await enqueue(() => transaction(() => {
|
|
597
|
+
const ts = isoNow();
|
|
598
|
+
const scope = requireNonEmpty(input.scope, "scope");
|
|
599
|
+
const memoryId = requireNonEmpty(input.memoryId, "memoryId");
|
|
600
|
+
if (!getNodeSync(scope, memoryId))
|
|
601
|
+
throw new Error(`cannot record feedback for missing memory node: ${memoryId}`);
|
|
602
|
+
const feedback = {
|
|
603
|
+
id: requireNonEmpty(input.id ?? randomUUID(), "feedback id"),
|
|
604
|
+
scope,
|
|
605
|
+
memoryId,
|
|
606
|
+
outcome: input.outcome,
|
|
607
|
+
strength: input.strength,
|
|
608
|
+
runId: input.runId ?? null,
|
|
609
|
+
evidenceRef: input.evidenceRef ?? null,
|
|
610
|
+
createdAt: input.createdAt ?? ts,
|
|
611
|
+
};
|
|
612
|
+
appendEvent("memory.feedback.recorded", ts, feedback);
|
|
613
|
+
insertFeedbackRow(feedback);
|
|
614
|
+
return feedback;
|
|
615
|
+
}));
|
|
616
|
+
},
|
|
617
|
+
async recordDecision(input) {
|
|
618
|
+
return await enqueue(() => transaction(() => {
|
|
619
|
+
const trace = {
|
|
620
|
+
id: requireNonEmpty(input.id ?? randomUUID(), "decision trace id"),
|
|
621
|
+
scope: requireNonEmpty(input.scope, "scope"),
|
|
622
|
+
query: input.query ?? null,
|
|
623
|
+
decisions: input.decisions,
|
|
624
|
+
createdAt: input.createdAt ?? isoNow(),
|
|
625
|
+
};
|
|
626
|
+
appendEvent("memory.decision.recorded", trace.createdAt, trace);
|
|
627
|
+
insertDecisionRow(trace);
|
|
628
|
+
return trace;
|
|
629
|
+
}));
|
|
630
|
+
},
|
|
631
|
+
async previewContext(input) {
|
|
632
|
+
return await enqueue(() => buildContext(input));
|
|
633
|
+
},
|
|
634
|
+
async compileContext(input) {
|
|
635
|
+
return await enqueue(() => transaction(() => {
|
|
636
|
+
const context = buildContext(input);
|
|
637
|
+
appendEvent("memory.decision.recorded", context.decision_trace.createdAt, context.decision_trace);
|
|
638
|
+
insertDecisionRow(context.decision_trace);
|
|
639
|
+
return context;
|
|
640
|
+
}));
|
|
641
|
+
},
|
|
642
|
+
async getNode(scope, id) {
|
|
643
|
+
return await enqueue(() => getNodeSync(scope, id));
|
|
644
|
+
},
|
|
645
|
+
async listNodes(scope) {
|
|
646
|
+
return await enqueue(() => sortNodes(db.prepare("SELECT * FROM memory_nodes WHERE scope = ?").all(scope).map(rowToNode)));
|
|
647
|
+
},
|
|
648
|
+
async searchNodes(input) {
|
|
649
|
+
return await enqueue(() => {
|
|
650
|
+
const nodes = db.prepare("SELECT * FROM memory_nodes WHERE scope = ?").all(input.scope).map(rowToNode);
|
|
651
|
+
return searchMemoryNodes(nodes, input);
|
|
652
|
+
});
|
|
653
|
+
},
|
|
654
|
+
async listRelations(scope) {
|
|
655
|
+
return await enqueue(() => db.prepare("SELECT * FROM memory_relations WHERE scope = ?").all(scope).map(rowToRelation));
|
|
656
|
+
},
|
|
657
|
+
async listEvents() {
|
|
658
|
+
return await enqueue(() => listEventsSync());
|
|
659
|
+
},
|
|
660
|
+
async close() {
|
|
661
|
+
await tail.catch(() => undefined);
|
|
662
|
+
db.close();
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
//# sourceMappingURL=sqlite-substrate.js.map
|