kongbrain 0.5.0 → 0.5.2
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/.clawhubignore +0 -1
- package/.kongcode-handoff.json +8 -0
- package/README.github.md +3 -1
- package/SKILL.md +6 -1
- package/dist/causal-CZ62YZ2J.js +11 -0
- package/dist/chunk-6NWMZY3J.js +194 -0
- package/dist/chunk-B3QUPMCI.js +419 -0
- package/dist/chunk-XSIONAGJ.js +1364 -0
- package/dist/index.js +6674 -0
- package/dist/memory-daemon-MJRXOBXU.js +11 -0
- package/openclaw.plugin.json +9 -1
- package/package.json +27 -3
- package/src/config.ts +9 -1
- package/src/context-engine.ts +4 -2
- package/src/index.ts +52 -20
- package/src/model-resolution.ts +98 -0
- package/src/schema-loader.ts +21 -3
- package/src/schema.surql +8 -8
- package/src/surreal.ts +10 -2
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/log.ts
|
|
23
|
+
var LEVELS, currentLevel, log;
|
|
24
|
+
var init_log = __esm({
|
|
25
|
+
"src/log.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
28
|
+
currentLevel = process.env.KONGBRAIN_LOG_LEVEL ?? "warn";
|
|
29
|
+
log = {
|
|
30
|
+
error: (...args) => {
|
|
31
|
+
if (LEVELS[currentLevel] >= LEVELS.error) console.error("[kongbrain]", ...args);
|
|
32
|
+
},
|
|
33
|
+
warn: (...args) => {
|
|
34
|
+
if (LEVELS[currentLevel] >= LEVELS.warn) console.warn("[kongbrain]", ...args);
|
|
35
|
+
},
|
|
36
|
+
info: (...args) => {
|
|
37
|
+
if (LEVELS[currentLevel] >= LEVELS.info) console.info("[kongbrain]", ...args);
|
|
38
|
+
},
|
|
39
|
+
debug: (...args) => {
|
|
40
|
+
if (LEVELS[currentLevel] >= LEVELS.debug) console.debug("[kongbrain]", ...args);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/errors.ts
|
|
47
|
+
function swallow(context, err) {
|
|
48
|
+
if (!DEBUG) return;
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err ?? "unknown");
|
|
50
|
+
log.debug(`[swallow] ${context}: ${msg}`);
|
|
51
|
+
}
|
|
52
|
+
var DEBUG;
|
|
53
|
+
var init_errors = __esm({
|
|
54
|
+
"src/errors.ts"() {
|
|
55
|
+
"use strict";
|
|
56
|
+
init_log();
|
|
57
|
+
DEBUG = process.env.KONGBRAIN_DEBUG === "1";
|
|
58
|
+
swallow.warn = function swallowWarn(context, err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err ?? "unknown");
|
|
60
|
+
log.warn(`${context}: ${msg}`);
|
|
61
|
+
};
|
|
62
|
+
swallow.error = function swallowError(context, err) {
|
|
63
|
+
const msg = err instanceof Error ? err.message : String(err ?? "unknown");
|
|
64
|
+
const stack = err instanceof Error ? `
|
|
65
|
+
${err.stack}` : "";
|
|
66
|
+
log.error(`${context}: ${msg}${stack}`);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/surreal.ts
|
|
72
|
+
init_errors();
|
|
73
|
+
init_log();
|
|
74
|
+
import { Surreal } from "surrealdb";
|
|
75
|
+
|
|
76
|
+
// src/schema-loader.ts
|
|
77
|
+
import { readFileSync } from "fs";
|
|
78
|
+
import { join, dirname } from "path";
|
|
79
|
+
import { fileURLToPath } from "url";
|
|
80
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
var DEFAULT_EMBEDDING_DIMENSIONS = 1024;
|
|
82
|
+
var DIMENSION_PLACEHOLDER = "__KONGBRAIN_EMBEDDING_DIMENSIONS__";
|
|
83
|
+
function normalizeDimensions(value) {
|
|
84
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : DEFAULT_EMBEDDING_DIMENSIONS;
|
|
85
|
+
}
|
|
86
|
+
function loadSchema(options = {}) {
|
|
87
|
+
const primary = join(__dirname, "schema.surql");
|
|
88
|
+
let schema;
|
|
89
|
+
try {
|
|
90
|
+
schema = readFileSync(primary, "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
schema = readFileSync(join(__dirname, "..", "src", "schema.surql"), "utf-8");
|
|
93
|
+
}
|
|
94
|
+
return schema.replaceAll(
|
|
95
|
+
DIMENSION_PLACEHOLDER,
|
|
96
|
+
String(normalizeDimensions(options.embeddingDimensions))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/surreal.ts
|
|
101
|
+
var RECORD_ID_RE = /^[a-zA-Z_][a-zA-Z0-9_]*:[a-zA-Z0-9_]+$/;
|
|
102
|
+
function assertRecordId(id) {
|
|
103
|
+
if (!RECORD_ID_RE.test(id)) {
|
|
104
|
+
throw new Error(`Invalid record ID format: ${id.slice(0, 40)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var VALID_EDGES = /* @__PURE__ */ new Set([
|
|
108
|
+
// Semantic edges
|
|
109
|
+
"responds_to",
|
|
110
|
+
"tool_result_of",
|
|
111
|
+
"summarizes",
|
|
112
|
+
"mentions",
|
|
113
|
+
"related_to",
|
|
114
|
+
"narrower",
|
|
115
|
+
"broader",
|
|
116
|
+
"about_concept",
|
|
117
|
+
"reflects_on",
|
|
118
|
+
// Skill edges
|
|
119
|
+
"skill_from_task",
|
|
120
|
+
"skill_uses_concept",
|
|
121
|
+
// Structural pillar edges
|
|
122
|
+
"owns",
|
|
123
|
+
"performed",
|
|
124
|
+
"task_part_of",
|
|
125
|
+
"session_task",
|
|
126
|
+
"produced",
|
|
127
|
+
"derived_from",
|
|
128
|
+
"relevant_to",
|
|
129
|
+
"used_in",
|
|
130
|
+
"artifact_mentions",
|
|
131
|
+
// Causal edges
|
|
132
|
+
"caused_by",
|
|
133
|
+
"supports",
|
|
134
|
+
"contradicts",
|
|
135
|
+
"describes",
|
|
136
|
+
// Evolution edges
|
|
137
|
+
"supersedes",
|
|
138
|
+
// Session edges
|
|
139
|
+
"part_of"
|
|
140
|
+
]);
|
|
141
|
+
function assertValidEdge(edge) {
|
|
142
|
+
if (!VALID_EDGES.has(edge)) throw new Error(`Invalid edge name: ${edge}`);
|
|
143
|
+
}
|
|
144
|
+
function patchOrderByFields(sql) {
|
|
145
|
+
const s = sql.trim();
|
|
146
|
+
if (!/^\s*SELECT\b/i.test(s) || !/\bORDER\s+BY\b/i.test(s)) return sql;
|
|
147
|
+
if (/^\s*SELECT\s+\*/i.test(s)) return sql;
|
|
148
|
+
const selectMatch = s.match(/^\s*SELECT\s+([\s\S]+?)\s+FROM\b/i);
|
|
149
|
+
if (!selectMatch) return sql;
|
|
150
|
+
const selectClause = selectMatch[1];
|
|
151
|
+
const orderMatch = s.match(
|
|
152
|
+
/\bORDER\s+BY\s+([\s\S]+?)(?=\s+LIMIT\b|\s+GROUP\b|\s+HAVING\b|$)/i
|
|
153
|
+
);
|
|
154
|
+
if (!orderMatch) return sql;
|
|
155
|
+
const orderFields = orderMatch[1].split(",").map((f) => f.trim().replace(/\s+(ASC|DESC)\s*$/i, "").trim()).filter(Boolean);
|
|
156
|
+
const selectedFields = selectClause.split(",").map((f) => f.trim().split(/\s+AS\s+/i)[0].trim()).map((f) => f.split(".").pop()).filter(Boolean).map((f) => f.toLowerCase());
|
|
157
|
+
const missing = orderFields.filter(
|
|
158
|
+
(f) => !selectedFields.includes(f.split(".").pop().toLowerCase())
|
|
159
|
+
);
|
|
160
|
+
if (missing.length === 0) return sql;
|
|
161
|
+
return sql.replace(
|
|
162
|
+
/(\bSELECT\s+)([\s\S]+?)(\s+FROM\b)/i,
|
|
163
|
+
(_, pre, fields, post) => `${pre}${fields}, ${missing.join(", ")}${post}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
var SurrealStore = class _SurrealStore {
|
|
167
|
+
constructor(config, options = {}) {
|
|
168
|
+
this.reconnecting = null;
|
|
169
|
+
this.shutdownFlag = false;
|
|
170
|
+
this.initialized = false;
|
|
171
|
+
/**
|
|
172
|
+
* The embedding provider tag used to stamp writes and filter searches.
|
|
173
|
+
* Set once at startup via setActiveProvider() after the EmbeddingService
|
|
174
|
+
* is constructed. Falls back to "local-bge-m3" so existing single-provider
|
|
175
|
+
* deployments keep working if the wire-up step is ever skipped.
|
|
176
|
+
*/
|
|
177
|
+
this.activeProvider = "local-bge-m3";
|
|
178
|
+
// ── Reflection session lookup ─────────────────────────────────────────
|
|
179
|
+
this._reflectionSessions = null;
|
|
180
|
+
this.config = config;
|
|
181
|
+
this.schemaOptions = options;
|
|
182
|
+
this.db = new Surreal();
|
|
183
|
+
}
|
|
184
|
+
/** Set the embedding provider id used to stamp writes and filter searches. */
|
|
185
|
+
setActiveProvider(providerId) {
|
|
186
|
+
this.activeProvider = providerId;
|
|
187
|
+
}
|
|
188
|
+
/** Get the active provider id (for callers writing records via direct CREATE). */
|
|
189
|
+
getActiveProvider() {
|
|
190
|
+
return this.activeProvider;
|
|
191
|
+
}
|
|
192
|
+
/** Connect and run schema. Returns true if a new connection was made, false if already initialized. */
|
|
193
|
+
async initialize() {
|
|
194
|
+
if (this.initialized) return false;
|
|
195
|
+
await this.db.connect(this.config.url, {
|
|
196
|
+
namespace: this.config.ns,
|
|
197
|
+
database: this.config.db,
|
|
198
|
+
authentication: { username: this.config.user, password: this.config.pass }
|
|
199
|
+
});
|
|
200
|
+
await this.runSchema();
|
|
201
|
+
this.initialized = true;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
markShutdown() {
|
|
205
|
+
this.shutdownFlag = true;
|
|
206
|
+
}
|
|
207
|
+
async ensureConnected() {
|
|
208
|
+
if (this.shutdownFlag) return;
|
|
209
|
+
if (this.db.isConnected) return;
|
|
210
|
+
if (this.reconnecting) return this.reconnecting;
|
|
211
|
+
this.reconnecting = (async () => {
|
|
212
|
+
const MAX_ATTEMPTS = 3;
|
|
213
|
+
const BACKOFF_MS = [500, 1500, 4e3];
|
|
214
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
215
|
+
try {
|
|
216
|
+
log.warn(
|
|
217
|
+
`SurrealDB disconnected \u2014 reconnecting (attempt ${attempt}/${MAX_ATTEMPTS})...`
|
|
218
|
+
);
|
|
219
|
+
this.db = new Surreal();
|
|
220
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
221
|
+
await Promise.race([
|
|
222
|
+
this.db.connect(this.config.url, {
|
|
223
|
+
namespace: this.config.ns,
|
|
224
|
+
database: this.config.db,
|
|
225
|
+
authentication: { username: this.config.user, password: this.config.pass }
|
|
226
|
+
}),
|
|
227
|
+
new Promise(
|
|
228
|
+
(_, reject) => setTimeout(
|
|
229
|
+
() => reject(new Error(`SurrealDB connect timed out after ${CONNECT_TIMEOUT_MS}ms`)),
|
|
230
|
+
CONNECT_TIMEOUT_MS
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
]);
|
|
234
|
+
log.warn("SurrealDB reconnected successfully.");
|
|
235
|
+
return;
|
|
236
|
+
} catch (e) {
|
|
237
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
238
|
+
await new Promise((r) => setTimeout(r, BACKOFF_MS[attempt - 1]));
|
|
239
|
+
} else {
|
|
240
|
+
log.error(`SurrealDB reconnection failed after ${MAX_ATTEMPTS} attempts.`);
|
|
241
|
+
throw new Error("SurrealDB reconnection failed");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})().finally(() => {
|
|
246
|
+
this.reconnecting = null;
|
|
247
|
+
});
|
|
248
|
+
return this.reconnecting;
|
|
249
|
+
}
|
|
250
|
+
async runSchema() {
|
|
251
|
+
const schema = loadSchema({
|
|
252
|
+
embeddingDimensions: this.schemaOptions.embeddingDimensions
|
|
253
|
+
});
|
|
254
|
+
await this.db.query(schema);
|
|
255
|
+
}
|
|
256
|
+
getConnection() {
|
|
257
|
+
return this.db;
|
|
258
|
+
}
|
|
259
|
+
isConnected() {
|
|
260
|
+
return this.db?.isConnected ?? false;
|
|
261
|
+
}
|
|
262
|
+
getInfo() {
|
|
263
|
+
return {
|
|
264
|
+
url: this.config.url,
|
|
265
|
+
ns: this.config.ns,
|
|
266
|
+
db: this.config.db,
|
|
267
|
+
connected: this.db?.isConnected ?? false
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async ping() {
|
|
271
|
+
try {
|
|
272
|
+
await this.ensureConnected();
|
|
273
|
+
await this.db.query("RETURN 'ok'");
|
|
274
|
+
return true;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async close() {
|
|
280
|
+
try {
|
|
281
|
+
this.markShutdown();
|
|
282
|
+
await this.db?.close();
|
|
283
|
+
} catch (e) {
|
|
284
|
+
swallow("surreal:close", e);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Returns true if an error is a connection-level failure worth retrying. */
|
|
288
|
+
isConnectionError(e) {
|
|
289
|
+
const msg = String(e?.message ?? e);
|
|
290
|
+
return msg.includes("must be connected") || msg.includes("ConnectionUnavailable");
|
|
291
|
+
}
|
|
292
|
+
/** Run a query function with one retry on connection errors. */
|
|
293
|
+
async withRetry(fn) {
|
|
294
|
+
try {
|
|
295
|
+
return await fn();
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (!this.isConnectionError(e)) throw e;
|
|
298
|
+
this.initialized = false;
|
|
299
|
+
try {
|
|
300
|
+
await this.db?.close();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
this.db = new Surreal();
|
|
304
|
+
await this.db.connect(this.config.url, {
|
|
305
|
+
namespace: this.config.ns,
|
|
306
|
+
database: this.config.db,
|
|
307
|
+
authentication: { username: this.config.user, password: this.config.pass }
|
|
308
|
+
});
|
|
309
|
+
return await fn();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
313
|
+
async queryFirst(sql, bindings) {
|
|
314
|
+
await this.ensureConnected();
|
|
315
|
+
return this.withRetry(async () => {
|
|
316
|
+
const ns = this.config.ns;
|
|
317
|
+
const dbName = this.config.db;
|
|
318
|
+
const fullSql = `USE NS ${ns} DB ${dbName}; ${patchOrderByFields(sql)}`;
|
|
319
|
+
const result = await this.db.query(fullSql, bindings);
|
|
320
|
+
const rows = Array.isArray(result) ? result[result.length - 1] : result;
|
|
321
|
+
return (Array.isArray(rows) ? rows : []).filter(Boolean);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
async queryMulti(sql, bindings) {
|
|
325
|
+
await this.ensureConnected();
|
|
326
|
+
return this.withRetry(async () => {
|
|
327
|
+
const ns = this.config.ns;
|
|
328
|
+
const dbName = this.config.db;
|
|
329
|
+
const fullSql = `USE NS ${ns} DB ${dbName}; ${patchOrderByFields(sql)}`;
|
|
330
|
+
const raw = await this.db.query(fullSql, bindings);
|
|
331
|
+
const flat = raw.flat();
|
|
332
|
+
return flat[flat.length - 1];
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async queryExec(sql, bindings) {
|
|
336
|
+
await this.ensureConnected();
|
|
337
|
+
return this.withRetry(async () => {
|
|
338
|
+
const ns = this.config.ns;
|
|
339
|
+
const dbName = this.config.db;
|
|
340
|
+
const fullSql = `USE NS ${ns} DB ${dbName}; ${patchOrderByFields(sql)}`;
|
|
341
|
+
await this.db.query(fullSql, bindings);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Execute N SQL statements in a single SurrealDB round-trip.
|
|
346
|
+
* Returns one result array per statement; bindings are shared across all statements.
|
|
347
|
+
*/
|
|
348
|
+
async queryBatch(statements, bindings) {
|
|
349
|
+
if (statements.length === 0) return [];
|
|
350
|
+
await this.ensureConnected();
|
|
351
|
+
return this.withRetry(async () => {
|
|
352
|
+
const ns = this.config.ns;
|
|
353
|
+
const dbName = this.config.db;
|
|
354
|
+
const joined = statements.map((s) => patchOrderByFields(s)).join(";\n");
|
|
355
|
+
const fullSql = `USE NS ${ns} DB ${dbName};
|
|
356
|
+
${joined}`;
|
|
357
|
+
const raw = await this.db.query(fullSql, bindings);
|
|
358
|
+
return raw.slice(1).map((r) => (Array.isArray(r) ? r : []).filter(Boolean));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async safeQuery(sql, bindings) {
|
|
362
|
+
try {
|
|
363
|
+
return await this.queryFirst(sql, bindings);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
swallow.warn("surreal:safeQuery", e);
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ── Vector search ──────────────────────────────────────────────────────
|
|
370
|
+
/** Multi-table cosine similarity search across turns, concepts, memories, artifacts, monologues, and identity chunks. Returns merged results sorted by score. */
|
|
371
|
+
async vectorSearch(vec, sessionId, limits = {}, withEmbeddings = false) {
|
|
372
|
+
const lim = {
|
|
373
|
+
turn: limits.turn ?? 20,
|
|
374
|
+
identity: limits.identity ?? 10,
|
|
375
|
+
concept: limits.concept ?? 15,
|
|
376
|
+
memory: limits.memory ?? 15,
|
|
377
|
+
artifact: limits.artifact ?? 10,
|
|
378
|
+
monologue: limits.monologue ?? 8
|
|
379
|
+
};
|
|
380
|
+
const sessionTurnLim = Math.ceil(lim.turn / 2);
|
|
381
|
+
const crossTurnLim = lim.turn - sessionTurnLim;
|
|
382
|
+
const emb = withEmbeddings ? ", embedding" : "";
|
|
383
|
+
const stmts = [
|
|
384
|
+
`SELECT id, text, role, timestamp, 0 AS accessCount, 'turn' AS table,
|
|
385
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
386
|
+
FROM turn WHERE embedding != NONE AND array::len(embedding) > 0
|
|
387
|
+
AND embedding_provider = $provider
|
|
388
|
+
AND session_id = $sid ORDER BY score DESC LIMIT ${sessionTurnLim}`,
|
|
389
|
+
`SELECT id, text, role, timestamp, 0 AS accessCount, 'turn' AS table,
|
|
390
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
391
|
+
FROM turn WHERE embedding != NONE AND array::len(embedding) > 0
|
|
392
|
+
AND embedding_provider = $provider
|
|
393
|
+
AND session_id != $sid ORDER BY score DESC LIMIT ${crossTurnLim}`,
|
|
394
|
+
`SELECT id, content AS text, stability AS importance, access_count AS accessCount,
|
|
395
|
+
created_at AS timestamp, 'concept' AS table,
|
|
396
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
397
|
+
FROM concept WHERE embedding != NONE AND array::len(embedding) > 0
|
|
398
|
+
AND embedding_provider = $provider
|
|
399
|
+
ORDER BY score DESC LIMIT ${lim.concept}`,
|
|
400
|
+
`SELECT id, text, importance, access_count AS accessCount,
|
|
401
|
+
created_at AS timestamp, session_id AS sessionId, 'memory' AS table,
|
|
402
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
403
|
+
FROM memory WHERE embedding != NONE AND array::len(embedding) > 0
|
|
404
|
+
AND embedding_provider = $provider
|
|
405
|
+
AND (status = 'active' OR status IS NONE) ORDER BY score DESC LIMIT ${lim.memory}`,
|
|
406
|
+
`SELECT id, description AS text, 0 AS accessCount,
|
|
407
|
+
created_at AS timestamp, 'artifact' AS table,
|
|
408
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
409
|
+
FROM artifact WHERE embedding != NONE AND array::len(embedding) > 0
|
|
410
|
+
AND embedding_provider = $provider
|
|
411
|
+
ORDER BY score DESC LIMIT ${lim.artifact}`,
|
|
412
|
+
`SELECT id, content AS text, category AS source, 0.5 AS importance, 0 AS accessCount,
|
|
413
|
+
timestamp, 'monologue' AS table,
|
|
414
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
415
|
+
FROM monologue WHERE embedding != NONE AND array::len(embedding) > 0
|
|
416
|
+
AND embedding_provider = $provider
|
|
417
|
+
ORDER BY score DESC LIMIT ${lim.monologue}`,
|
|
418
|
+
`SELECT id, text, importance, 0 AS accessCount,
|
|
419
|
+
'identity_chunk' AS table,
|
|
420
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
421
|
+
FROM identity_chunk WHERE embedding != NONE AND array::len(embedding) > 0
|
|
422
|
+
AND embedding_provider = $provider
|
|
423
|
+
ORDER BY score DESC LIMIT ${lim.identity}`
|
|
424
|
+
];
|
|
425
|
+
let batchResults;
|
|
426
|
+
try {
|
|
427
|
+
batchResults = await this.queryBatch(stmts, { vec, sid: sessionId, provider: this.activeProvider });
|
|
428
|
+
} catch (e) {
|
|
429
|
+
swallow.warn("surreal:vectorSearch:batch", e);
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
const [sessionTurns = [], crossTurns = [], concepts = [], memories = [], artifacts = [], monologues = [], identityChunks = []] = batchResults;
|
|
433
|
+
return [
|
|
434
|
+
...sessionTurns,
|
|
435
|
+
...crossTurns,
|
|
436
|
+
...concepts,
|
|
437
|
+
...memories,
|
|
438
|
+
...artifacts,
|
|
439
|
+
...monologues,
|
|
440
|
+
...identityChunks
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
// ── Turn operations ────────────────────────────────────────────────────
|
|
444
|
+
async upsertTurn(turn) {
|
|
445
|
+
const { embedding, ...rest } = turn;
|
|
446
|
+
const record = embedding?.length ? { ...rest, embedding, embedding_provider: this.activeProvider } : rest;
|
|
447
|
+
const rows = await this.queryFirst(
|
|
448
|
+
`CREATE turn CONTENT $turn RETURN id`,
|
|
449
|
+
{ turn: record }
|
|
450
|
+
);
|
|
451
|
+
return String(rows[0]?.id ?? "");
|
|
452
|
+
}
|
|
453
|
+
async getSessionTurns(sessionId, limit = 50) {
|
|
454
|
+
return this.queryFirst(
|
|
455
|
+
`SELECT role, text, timestamp FROM turn WHERE session_id = $sid ORDER BY timestamp ASC LIMIT $lim`,
|
|
456
|
+
{ sid: sessionId, lim: limit }
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
async getSessionTurnsRich(sessionId, limit = 20) {
|
|
460
|
+
return this.queryFirst(
|
|
461
|
+
`SELECT role, text, tool_name, timestamp FROM turn WHERE session_id = $sid ORDER BY timestamp ASC LIMIT $lim`,
|
|
462
|
+
{ sid: sessionId, lim: limit }
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
// ── Relation helpers ───────────────────────────────────────────────────
|
|
466
|
+
async relate(fromId, edge, toId) {
|
|
467
|
+
assertRecordId(fromId);
|
|
468
|
+
assertRecordId(toId);
|
|
469
|
+
const safeName = edge.replace(/[^a-zA-Z0-9_]/g, "");
|
|
470
|
+
await this.queryExec(`RELATE ${fromId}->${safeName}->${toId}`);
|
|
471
|
+
}
|
|
472
|
+
// ── 5-Pillar entity operations ─────────────────────────────────────────
|
|
473
|
+
async ensureAgent(name, model) {
|
|
474
|
+
const rows = await this.queryFirst(
|
|
475
|
+
`SELECT id FROM agent WHERE name = $name LIMIT 1`,
|
|
476
|
+
{ name }
|
|
477
|
+
);
|
|
478
|
+
if (rows.length > 0) return String(rows[0].id);
|
|
479
|
+
const created = await this.queryFirst(
|
|
480
|
+
`CREATE agent CONTENT { name: $name, model: $model } RETURN id`,
|
|
481
|
+
{ name, ...model != null ? { model } : {} }
|
|
482
|
+
);
|
|
483
|
+
return String(created[0]?.id ?? "");
|
|
484
|
+
}
|
|
485
|
+
async ensureProject(name) {
|
|
486
|
+
const rows = await this.queryFirst(
|
|
487
|
+
`SELECT id FROM project WHERE name = $name LIMIT 1`,
|
|
488
|
+
{ name }
|
|
489
|
+
);
|
|
490
|
+
if (rows.length > 0) return String(rows[0].id);
|
|
491
|
+
const created = await this.queryFirst(
|
|
492
|
+
`CREATE project CONTENT { name: $name } RETURN id`,
|
|
493
|
+
{ name }
|
|
494
|
+
);
|
|
495
|
+
return String(created[0]?.id ?? "");
|
|
496
|
+
}
|
|
497
|
+
async createTask(description) {
|
|
498
|
+
const rows = await this.queryFirst(
|
|
499
|
+
`CREATE task CONTENT { description: $desc, status: "in_progress" } RETURN id`,
|
|
500
|
+
{ desc: description }
|
|
501
|
+
);
|
|
502
|
+
return String(rows[0]?.id ?? "");
|
|
503
|
+
}
|
|
504
|
+
async createSession(agentId = "default") {
|
|
505
|
+
const rows = await this.queryFirst(
|
|
506
|
+
`CREATE session CONTENT { agent_id: $agent_id } RETURN id`,
|
|
507
|
+
{ agent_id: agentId }
|
|
508
|
+
);
|
|
509
|
+
return String(rows[0]?.id ?? "");
|
|
510
|
+
}
|
|
511
|
+
async updateSessionStats(sessionId, inputTokens, outputTokens) {
|
|
512
|
+
assertRecordId(sessionId);
|
|
513
|
+
await this.queryExec(
|
|
514
|
+
`UPDATE ${sessionId} SET
|
|
515
|
+
turn_count += 1,
|
|
516
|
+
total_input_tokens += $input,
|
|
517
|
+
total_output_tokens += $output,
|
|
518
|
+
last_active = time::now()`,
|
|
519
|
+
{ input: inputTokens, output: outputTokens }
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
async endSession(sessionId, summary) {
|
|
523
|
+
assertRecordId(sessionId);
|
|
524
|
+
if (summary) {
|
|
525
|
+
await this.queryExec(
|
|
526
|
+
`UPDATE ${sessionId} SET ended_at = time::now(), summary = $summary`,
|
|
527
|
+
{ summary }
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
await this.queryExec(`UPDATE ${sessionId} SET ended_at = time::now()`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async markSessionActive(sessionId) {
|
|
534
|
+
assertRecordId(sessionId);
|
|
535
|
+
await this.queryExec(
|
|
536
|
+
`UPDATE ${sessionId} SET cleanup_completed = false, last_active = time::now()`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
async markSessionEnded(sessionId) {
|
|
540
|
+
assertRecordId(sessionId);
|
|
541
|
+
await this.queryExec(
|
|
542
|
+
`UPDATE ${sessionId} SET ended_at = time::now(), cleanup_completed = true`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
async getOrphanedSessions(limit = 3) {
|
|
546
|
+
return this.queryFirst(
|
|
547
|
+
`SELECT id, started_at FROM session
|
|
548
|
+
WHERE cleanup_completed != true
|
|
549
|
+
AND started_at < time::now() - 2m
|
|
550
|
+
ORDER BY started_at DESC LIMIT $lim`,
|
|
551
|
+
{ lim: limit }
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
async linkSessionToTask(sessionId, taskId) {
|
|
555
|
+
assertRecordId(sessionId);
|
|
556
|
+
assertRecordId(taskId);
|
|
557
|
+
await this.queryExec(
|
|
558
|
+
`RELATE ${sessionId}->session_task->${taskId}`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
async linkTaskToProject(taskId, projectId) {
|
|
562
|
+
assertRecordId(taskId);
|
|
563
|
+
assertRecordId(projectId);
|
|
564
|
+
await this.queryExec(
|
|
565
|
+
`RELATE ${taskId}->task_part_of->${projectId}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
async linkAgentToTask(agentId, taskId) {
|
|
569
|
+
assertRecordId(agentId);
|
|
570
|
+
assertRecordId(taskId);
|
|
571
|
+
await this.queryExec(
|
|
572
|
+
`RELATE ${agentId}->performed->${taskId}`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
async linkAgentToProject(agentId, projectId) {
|
|
576
|
+
assertRecordId(agentId);
|
|
577
|
+
assertRecordId(projectId);
|
|
578
|
+
await this.queryExec(
|
|
579
|
+
`RELATE ${agentId}->owns->${projectId}`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
// ── Graph traversal ────────────────────────────────────────────────────
|
|
583
|
+
/**
|
|
584
|
+
* BFS expansion from seed nodes along typed edges, with batched per-hop queries.
|
|
585
|
+
* Each edge query is LIMIT 3 (EDGE_NEIGHBOR_LIMIT) to bound fan-out per node.
|
|
586
|
+
*/
|
|
587
|
+
/**
|
|
588
|
+
* Tag-boosted concept retrieval: extract keywords from query text,
|
|
589
|
+
* find concepts tagged with matching terms, score by cosine similarity.
|
|
590
|
+
* Returns concepts that pure vector search might miss due to embedding mismatch.
|
|
591
|
+
*/
|
|
592
|
+
async tagBoostedConcepts(queryText, queryVec, limit = 10) {
|
|
593
|
+
const stopwords = /* @__PURE__ */ new Set(["the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", "shall", "to", "of", "in", "for", "on", "with", "at", "by", "from", "as", "into", "about", "between", "through", "during", "it", "its", "this", "that", "these", "those", "i", "you", "we", "they", "my", "your", "our", "their", "what", "which", "who", "how", "when", "where", "why", "not", "no", "and", "or", "but", "if", "so", "any", "all", "some", "more", "just", "also", "than", "very", "too", "much", "many"]);
|
|
594
|
+
const words = queryText.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopwords.has(w));
|
|
595
|
+
if (words.length === 0) return [];
|
|
596
|
+
const tagConditions = words.slice(0, 8).map((w) => `tags CONTAINS '${w.replace(/'/g, "")}'`).join(" OR ");
|
|
597
|
+
try {
|
|
598
|
+
const rows = await this.queryFirst(
|
|
599
|
+
`SELECT id, content AS text, stability AS importance, access_count AS accessCount,
|
|
600
|
+
created_at AS timestamp, 'concept' AS table,
|
|
601
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
602
|
+
FROM concept
|
|
603
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
604
|
+
AND embedding_provider = $provider
|
|
605
|
+
AND (${tagConditions})
|
|
606
|
+
ORDER BY score DESC
|
|
607
|
+
LIMIT $limit`,
|
|
608
|
+
{ vec: queryVec, limit, provider: this.activeProvider }
|
|
609
|
+
);
|
|
610
|
+
return rows;
|
|
611
|
+
} catch (e) {
|
|
612
|
+
swallow.warn("surreal:tagBoostedConcepts", e);
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async graphExpand(nodeIds, queryVec, hops = 1) {
|
|
617
|
+
if (nodeIds.length === 0) return [];
|
|
618
|
+
const MAX_FRONTIER_SEEDS = 5;
|
|
619
|
+
const MAX_FRONTIER_PER_HOP = 3;
|
|
620
|
+
const EDGE_NEIGHBOR_LIMIT = 3;
|
|
621
|
+
const forwardEdges = [
|
|
622
|
+
// Semantic edges
|
|
623
|
+
"responds_to",
|
|
624
|
+
"tool_result_of",
|
|
625
|
+
"summarizes",
|
|
626
|
+
"mentions",
|
|
627
|
+
"related_to",
|
|
628
|
+
"narrower",
|
|
629
|
+
"broader",
|
|
630
|
+
"about_concept",
|
|
631
|
+
"reflects_on",
|
|
632
|
+
"skill_from_task",
|
|
633
|
+
"skill_uses_concept",
|
|
634
|
+
// Structural pillar edges (Agent→Project→Task→Artifact→Concept)
|
|
635
|
+
"owns",
|
|
636
|
+
"performed",
|
|
637
|
+
"task_part_of",
|
|
638
|
+
"session_task",
|
|
639
|
+
"produced",
|
|
640
|
+
"derived_from",
|
|
641
|
+
"relevant_to",
|
|
642
|
+
"used_in",
|
|
643
|
+
"artifact_mentions"
|
|
644
|
+
];
|
|
645
|
+
const reverseEdges = [
|
|
646
|
+
"reflects_on",
|
|
647
|
+
"skill_from_task",
|
|
648
|
+
// Reverse pillar traversal (find what produced an artifact, what task a concept came from)
|
|
649
|
+
"produced",
|
|
650
|
+
"derived_from",
|
|
651
|
+
"performed",
|
|
652
|
+
"owns"
|
|
653
|
+
];
|
|
654
|
+
const scoreExpr = ", IF embedding != NONE AND array::len(embedding) > 0 AND embedding_provider = $provider THEN vector::similarity::cosine(embedding, $vec) ELSE 0 END AS score";
|
|
655
|
+
const bindings = { vec: queryVec, provider: this.activeProvider };
|
|
656
|
+
const selectFields = `SELECT id, text, content, description, importance, stability,
|
|
657
|
+
access_count AS accessCount, created_at AS timestamp,
|
|
658
|
+
meta::tb(id) AS table${scoreExpr}`;
|
|
659
|
+
const seen = new Set(nodeIds);
|
|
660
|
+
const allNeighbors = [];
|
|
661
|
+
let frontier = nodeIds.slice(0, MAX_FRONTIER_SEEDS).filter((id) => RECORD_ID_RE.test(id));
|
|
662
|
+
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
663
|
+
const stmts = [];
|
|
664
|
+
for (const id of frontier) {
|
|
665
|
+
for (const edge of forwardEdges) {
|
|
666
|
+
assertValidEdge(edge);
|
|
667
|
+
stmts.push(`${selectFields} FROM ${id}->${edge}->? LIMIT ${EDGE_NEIGHBOR_LIMIT}`);
|
|
668
|
+
}
|
|
669
|
+
for (const edge of reverseEdges) {
|
|
670
|
+
assertValidEdge(edge);
|
|
671
|
+
stmts.push(`${selectFields} FROM ${id}<-${edge}<-? LIMIT ${EDGE_NEIGHBOR_LIMIT}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
let queryResults;
|
|
675
|
+
try {
|
|
676
|
+
queryResults = await this.queryBatch(stmts, bindings);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
swallow.warn("surreal:graphExpand:batch", e);
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
const nextFrontier = [];
|
|
682
|
+
for (const rows of queryResults) {
|
|
683
|
+
for (const row of rows) {
|
|
684
|
+
const nodeId = String(row.id);
|
|
685
|
+
if (seen.has(nodeId)) continue;
|
|
686
|
+
seen.add(nodeId);
|
|
687
|
+
const text = row.text ?? row.content ?? row.description ?? null;
|
|
688
|
+
if (text) {
|
|
689
|
+
const score = row.score ?? 0;
|
|
690
|
+
allNeighbors.push({
|
|
691
|
+
text,
|
|
692
|
+
importance: row.importance ?? row.stability,
|
|
693
|
+
accessCount: row.accessCount,
|
|
694
|
+
timestamp: row.timestamp,
|
|
695
|
+
table: String(row.table ?? "unknown"),
|
|
696
|
+
id: nodeId,
|
|
697
|
+
score
|
|
698
|
+
});
|
|
699
|
+
if (RECORD_ID_RE.test(nodeId)) {
|
|
700
|
+
nextFrontier.push({ id: nodeId, score });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
frontier = nextFrontier.sort((a, b) => b.score - a.score).slice(0, MAX_FRONTIER_PER_HOP).map((n) => n.id);
|
|
706
|
+
}
|
|
707
|
+
return allNeighbors;
|
|
708
|
+
}
|
|
709
|
+
async bumpAccessCounts(ids) {
|
|
710
|
+
const validated = ids.filter((id) => {
|
|
711
|
+
try {
|
|
712
|
+
assertRecordId(id);
|
|
713
|
+
return true;
|
|
714
|
+
} catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
if (validated.length === 0) return;
|
|
719
|
+
try {
|
|
720
|
+
const stmts = validated.map(
|
|
721
|
+
(id) => `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`
|
|
722
|
+
);
|
|
723
|
+
await this.queryBatch(stmts);
|
|
724
|
+
} catch (e) {
|
|
725
|
+
swallow.warn("surreal:bumpAccessCounts", e);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// ── Concept / Memory / Artifact CRUD ───────────────────────────────────
|
|
729
|
+
async upsertConcept(content, embedding, source) {
|
|
730
|
+
if (!content?.trim()) return "";
|
|
731
|
+
content = content.trim();
|
|
732
|
+
const rows = await this.queryFirst(
|
|
733
|
+
`SELECT id FROM concept WHERE string::lowercase(content) = string::lowercase($content) LIMIT 1`,
|
|
734
|
+
{ content }
|
|
735
|
+
);
|
|
736
|
+
if (rows.length > 0) {
|
|
737
|
+
const id = String(rows[0].id);
|
|
738
|
+
if (embedding?.length) {
|
|
739
|
+
await this.queryExec(
|
|
740
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now(),
|
|
741
|
+
embedding = IF embedding IS NONE OR array::len(embedding) = 0 THEN $emb ELSE embedding END,
|
|
742
|
+
embedding_provider = IF embedding IS NONE OR array::len(embedding) = 0 THEN $provider ELSE embedding_provider END`,
|
|
743
|
+
{ emb: embedding, provider: this.activeProvider }
|
|
744
|
+
);
|
|
745
|
+
} else {
|
|
746
|
+
await this.queryExec(
|
|
747
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return id;
|
|
751
|
+
}
|
|
752
|
+
const emb = embedding?.length ? embedding : void 0;
|
|
753
|
+
const record = { content, source: source ?? void 0 };
|
|
754
|
+
if (emb) {
|
|
755
|
+
record.embedding = emb;
|
|
756
|
+
record.embedding_provider = this.activeProvider;
|
|
757
|
+
}
|
|
758
|
+
const created = await this.queryFirst(
|
|
759
|
+
`CREATE concept CONTENT $record RETURN id`,
|
|
760
|
+
{ record }
|
|
761
|
+
);
|
|
762
|
+
return String(created[0]?.id ?? "");
|
|
763
|
+
}
|
|
764
|
+
async createArtifact(path, type, description, embedding) {
|
|
765
|
+
const record = { path, type, description };
|
|
766
|
+
if (embedding?.length) {
|
|
767
|
+
record.embedding = embedding;
|
|
768
|
+
record.embedding_provider = this.activeProvider;
|
|
769
|
+
}
|
|
770
|
+
const rows = await this.queryFirst(
|
|
771
|
+
`CREATE artifact CONTENT $record RETURN id`,
|
|
772
|
+
{ record }
|
|
773
|
+
);
|
|
774
|
+
return String(rows[0]?.id ?? "");
|
|
775
|
+
}
|
|
776
|
+
async createMemory(text, embedding, importance, category, sessionId) {
|
|
777
|
+
const source = category ?? "general";
|
|
778
|
+
if (embedding?.length) {
|
|
779
|
+
const dupes = await this.queryFirst(
|
|
780
|
+
`SELECT id, importance,
|
|
781
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
782
|
+
FROM memory
|
|
783
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
784
|
+
AND embedding_provider = $provider
|
|
785
|
+
AND category = $cat
|
|
786
|
+
ORDER BY score DESC
|
|
787
|
+
LIMIT 1`,
|
|
788
|
+
{ vec: embedding, cat: source, provider: this.activeProvider }
|
|
789
|
+
);
|
|
790
|
+
if (dupes.length > 0 && dupes[0].score > 0.92) {
|
|
791
|
+
const existing = dupes[0];
|
|
792
|
+
const newImp = Math.max(existing.importance ?? 0, importance);
|
|
793
|
+
await this.queryExec(
|
|
794
|
+
`UPDATE ${String(existing.id)} SET access_count += 1, importance = $imp, last_accessed = time::now()`,
|
|
795
|
+
{ imp: newImp }
|
|
796
|
+
);
|
|
797
|
+
return String(existing.id);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const record = { text, importance, category: source, source };
|
|
801
|
+
if (embedding?.length) {
|
|
802
|
+
record.embedding = embedding;
|
|
803
|
+
record.embedding_provider = this.activeProvider;
|
|
804
|
+
}
|
|
805
|
+
if (sessionId) record.session_id = sessionId;
|
|
806
|
+
const rows = await this.queryFirst(
|
|
807
|
+
`CREATE memory CONTENT $record RETURN id`,
|
|
808
|
+
{ record }
|
|
809
|
+
);
|
|
810
|
+
return String(rows[0]?.id ?? "");
|
|
811
|
+
}
|
|
812
|
+
async createMonologue(sessionId, category, content, embedding) {
|
|
813
|
+
const record = { session_id: sessionId, category, content };
|
|
814
|
+
if (embedding?.length) {
|
|
815
|
+
record.embedding = embedding;
|
|
816
|
+
record.embedding_provider = this.activeProvider;
|
|
817
|
+
}
|
|
818
|
+
const rows = await this.queryFirst(
|
|
819
|
+
`CREATE monologue CONTENT $record RETURN id`,
|
|
820
|
+
{ record }
|
|
821
|
+
);
|
|
822
|
+
return String(rows[0]?.id ?? "");
|
|
823
|
+
}
|
|
824
|
+
// ── Core Memory (Tier 0/1) ─────────────────────────────────────────────
|
|
825
|
+
async getAllCoreMemory(tier) {
|
|
826
|
+
try {
|
|
827
|
+
if (tier != null) {
|
|
828
|
+
return await this.queryFirst(
|
|
829
|
+
`SELECT * FROM core_memory WHERE active = true AND tier = $tier ORDER BY priority DESC`,
|
|
830
|
+
{ tier }
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
return await this.queryFirst(
|
|
834
|
+
`SELECT * FROM core_memory WHERE active = true ORDER BY tier ASC, priority DESC`
|
|
835
|
+
);
|
|
836
|
+
} catch (e) {
|
|
837
|
+
swallow.warn("surreal:getAllCoreMemory", e);
|
|
838
|
+
return [];
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async createCoreMemory(text, category, priority, tier, sessionId) {
|
|
842
|
+
const record = { text, category, priority, tier, active: true };
|
|
843
|
+
if (sessionId) record.session_id = sessionId;
|
|
844
|
+
const rows = await this.queryFirst(
|
|
845
|
+
`CREATE core_memory CONTENT $record RETURN id`,
|
|
846
|
+
{ record }
|
|
847
|
+
);
|
|
848
|
+
const id = String(rows[0]?.id ?? "");
|
|
849
|
+
if (!id) throw new Error("createCoreMemory: CREATE returned no ID");
|
|
850
|
+
return id;
|
|
851
|
+
}
|
|
852
|
+
async updateCoreMemory(id, fields) {
|
|
853
|
+
assertRecordId(id);
|
|
854
|
+
const ALLOWED_FIELDS = /* @__PURE__ */ new Set(["text", "category", "priority", "tier", "active"]);
|
|
855
|
+
const sets = [];
|
|
856
|
+
const bindings = {};
|
|
857
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
858
|
+
if (val !== void 0 && ALLOWED_FIELDS.has(key)) {
|
|
859
|
+
sets.push(`${key} = $${key}`);
|
|
860
|
+
bindings[key] = val;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (sets.length === 0) return false;
|
|
864
|
+
sets.push("updated_at = time::now()");
|
|
865
|
+
const rows = await this.queryFirst(
|
|
866
|
+
`UPDATE ${id} SET ${sets.join(", ")} RETURN id`,
|
|
867
|
+
bindings
|
|
868
|
+
);
|
|
869
|
+
return rows.length > 0;
|
|
870
|
+
}
|
|
871
|
+
async deleteCoreMemory(id) {
|
|
872
|
+
assertRecordId(id);
|
|
873
|
+
await this.queryExec(
|
|
874
|
+
`UPDATE ${id} SET active = false, updated_at = time::now()`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
async deactivateSessionMemories(sessionId) {
|
|
878
|
+
try {
|
|
879
|
+
await this.queryExec(
|
|
880
|
+
`UPDATE core_memory SET active = false, updated_at = time::now() WHERE session_id = $sid AND tier = 1`,
|
|
881
|
+
{ sid: sessionId }
|
|
882
|
+
);
|
|
883
|
+
} catch (e) {
|
|
884
|
+
swallow.warn("surreal:deactivateSessionMemories", e);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// ── Wakeup & lifecycle queries ─────────────────────────────────────────
|
|
888
|
+
async getLatestHandoff() {
|
|
889
|
+
try {
|
|
890
|
+
const rows = await this.queryFirst(
|
|
891
|
+
`SELECT text, created_at FROM memory WHERE category = "handoff" ORDER BY created_at DESC LIMIT 1`
|
|
892
|
+
);
|
|
893
|
+
return rows[0] ?? null;
|
|
894
|
+
} catch (e) {
|
|
895
|
+
swallow.warn("surreal:getLatestHandoff", e);
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async countResolvedSinceHandoff(handoffCreatedAt) {
|
|
900
|
+
try {
|
|
901
|
+
const rows = await this.queryFirst(
|
|
902
|
+
`SELECT count() AS count FROM memory WHERE status = 'resolved' AND resolved_at > $ts GROUP ALL`,
|
|
903
|
+
{ ts: handoffCreatedAt }
|
|
904
|
+
);
|
|
905
|
+
return rows[0]?.count ?? 0;
|
|
906
|
+
} catch (e) {
|
|
907
|
+
swallow.warn("surreal:countResolvedSinceHandoff", e);
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async getAllIdentityChunks() {
|
|
912
|
+
try {
|
|
913
|
+
return await this.queryFirst(
|
|
914
|
+
`SELECT text, chunk_index FROM identity_chunk ORDER BY chunk_index ASC`
|
|
915
|
+
);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
swallow.warn("surreal:getAllIdentityChunks", e);
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async getRecentMonologues(limit = 5) {
|
|
922
|
+
try {
|
|
923
|
+
return await this.queryFirst(
|
|
924
|
+
`SELECT category, content, timestamp FROM monologue ORDER BY timestamp DESC LIMIT $lim`,
|
|
925
|
+
{ lim: limit }
|
|
926
|
+
);
|
|
927
|
+
} catch (e) {
|
|
928
|
+
swallow.warn("surreal:getRecentMonologues", e);
|
|
929
|
+
return [];
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async getPreviousSessionTurns(currentSessionId, limit = 10) {
|
|
933
|
+
try {
|
|
934
|
+
let prevSessionQuery;
|
|
935
|
+
const bindings = { lim: limit };
|
|
936
|
+
if (currentSessionId) {
|
|
937
|
+
prevSessionQuery = `SELECT id, started_at FROM session WHERE id != $current ORDER BY started_at DESC LIMIT 1`;
|
|
938
|
+
bindings.current = currentSessionId;
|
|
939
|
+
} else {
|
|
940
|
+
prevSessionQuery = `SELECT id, started_at FROM session ORDER BY started_at DESC LIMIT 1`;
|
|
941
|
+
}
|
|
942
|
+
const sessionRows = await this.queryFirst(prevSessionQuery, bindings);
|
|
943
|
+
if (sessionRows.length === 0) return [];
|
|
944
|
+
const prevSessionId = String(sessionRows[0].id);
|
|
945
|
+
const turns = await this.queryFirst(
|
|
946
|
+
`SELECT role, text, tool_name, timestamp FROM turn
|
|
947
|
+
WHERE id IN (SELECT VALUE in FROM part_of WHERE out = $sid)
|
|
948
|
+
AND text != NONE AND text != ""
|
|
949
|
+
ORDER BY timestamp DESC LIMIT $lim`,
|
|
950
|
+
{ sid: prevSessionId, lim: limit }
|
|
951
|
+
);
|
|
952
|
+
return turns.reverse();
|
|
953
|
+
} catch (e) {
|
|
954
|
+
swallow.warn("surreal:getPreviousSessionTurns", e);
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async getUnresolvedMemories(limit = 5) {
|
|
959
|
+
try {
|
|
960
|
+
return await this.queryFirst(
|
|
961
|
+
`SELECT id, text,
|
|
962
|
+
math::max([importance - math::min([math::floor(duration::days(time::now() - created_at) / 7), 3]), 0]) AS importance,
|
|
963
|
+
category
|
|
964
|
+
FROM memory
|
|
965
|
+
WHERE (status IS NONE OR status != 'resolved')
|
|
966
|
+
AND category NOT IN ['handoff', 'monologue', 'reflection', 'compaction', 'consolidation']
|
|
967
|
+
AND importance >= 6
|
|
968
|
+
ORDER BY importance DESC
|
|
969
|
+
LIMIT $lim`,
|
|
970
|
+
{ lim: limit }
|
|
971
|
+
);
|
|
972
|
+
} catch (e) {
|
|
973
|
+
swallow.warn("surreal:getUnresolvedMemories", e);
|
|
974
|
+
return [];
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async getRecentFailedCausal(limit = 3) {
|
|
978
|
+
try {
|
|
979
|
+
return await this.queryFirst(
|
|
980
|
+
`SELECT description, chain_type, created_at FROM causal_chain WHERE success = false ORDER BY created_at DESC LIMIT $lim`,
|
|
981
|
+
{ lim: limit }
|
|
982
|
+
);
|
|
983
|
+
} catch (e) {
|
|
984
|
+
swallow.warn("surreal:getRecentFailedCausal", e);
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
async resolveMemory(memoryId) {
|
|
989
|
+
try {
|
|
990
|
+
assertRecordId(memoryId);
|
|
991
|
+
await this.queryFirst(
|
|
992
|
+
`UPDATE ${memoryId} SET status = 'resolved', resolved_at = time::now()`
|
|
993
|
+
);
|
|
994
|
+
return true;
|
|
995
|
+
} catch (e) {
|
|
996
|
+
swallow.warn("surreal:resolveMemory", e);
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// ── Utility cache ──────────────────────────────────────────────────────
|
|
1001
|
+
async updateUtilityCache(memoryId, utilization) {
|
|
1002
|
+
try {
|
|
1003
|
+
await this.queryExec(
|
|
1004
|
+
`UPSERT memory_utility_cache SET
|
|
1005
|
+
memory_id = $mid,
|
|
1006
|
+
retrieval_count += 1,
|
|
1007
|
+
avg_utilization = IF retrieval_count > 1
|
|
1008
|
+
THEN (avg_utilization * (retrieval_count - 1) + $util) / retrieval_count
|
|
1009
|
+
ELSE $util
|
|
1010
|
+
END,
|
|
1011
|
+
last_updated = time::now()
|
|
1012
|
+
WHERE memory_id = $mid`,
|
|
1013
|
+
{ mid: memoryId, util: utilization }
|
|
1014
|
+
);
|
|
1015
|
+
} catch (e) {
|
|
1016
|
+
swallow.warn("surreal:updateUtilityCache", e);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async getUtilityFromCache(ids) {
|
|
1020
|
+
const result = /* @__PURE__ */ new Map();
|
|
1021
|
+
if (ids.length === 0) return result;
|
|
1022
|
+
try {
|
|
1023
|
+
const rows = await this.queryFirst(
|
|
1024
|
+
`SELECT memory_id, avg_utilization FROM memory_utility_cache WHERE memory_id IN $ids`,
|
|
1025
|
+
{ ids }
|
|
1026
|
+
);
|
|
1027
|
+
for (const row of rows) {
|
|
1028
|
+
if (row.avg_utilization != null) result.set(String(row.memory_id), row.avg_utilization);
|
|
1029
|
+
}
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
swallow.warn("surreal:getUtilityFromCache", e);
|
|
1032
|
+
}
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
async getUtilityCacheEntries(ids) {
|
|
1036
|
+
const result = /* @__PURE__ */ new Map();
|
|
1037
|
+
if (ids.length === 0) return result;
|
|
1038
|
+
try {
|
|
1039
|
+
const rows = await this.queryFirst(
|
|
1040
|
+
`SELECT memory_id, avg_utilization, retrieval_count FROM memory_utility_cache WHERE memory_id IN $ids`,
|
|
1041
|
+
{ ids }
|
|
1042
|
+
);
|
|
1043
|
+
for (const row of rows) {
|
|
1044
|
+
if (row.avg_utilization != null) {
|
|
1045
|
+
result.set(String(row.memory_id), {
|
|
1046
|
+
avg_utilization: row.avg_utilization,
|
|
1047
|
+
retrieval_count: row.retrieval_count ?? 0
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
swallow.warn("surreal:getUtilityCacheEntries", e);
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
// ── Maintenance operations ─────────────────────────────────────────────
|
|
1057
|
+
async runMemoryMaintenance() {
|
|
1058
|
+
try {
|
|
1059
|
+
await this.queryExec(`
|
|
1060
|
+
UPDATE memory SET importance = math::max([importance * 0.95, 2.0]) WHERE importance > 2.0;
|
|
1061
|
+
UPDATE memory SET importance = math::max([importance, 3 + ((
|
|
1062
|
+
SELECT VALUE avg_utilization FROM memory_utility_cache WHERE memory_id = string::concat(meta::tb(id), ":", meta::id(id)) LIMIT 1
|
|
1063
|
+
)[0] ?? 0) * 4]) WHERE importance < 7;
|
|
1064
|
+
`);
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
swallow("surreal:runMemoryMaintenance", e);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async garbageCollectMemories() {
|
|
1070
|
+
try {
|
|
1071
|
+
const countRows = await this.queryFirst(
|
|
1072
|
+
`SELECT count() AS count FROM memory GROUP ALL`
|
|
1073
|
+
);
|
|
1074
|
+
const count = countRows[0]?.count ?? 0;
|
|
1075
|
+
if (count <= 200) return 0;
|
|
1076
|
+
const pruned = await this.db.query(
|
|
1077
|
+
`LET $stale = (
|
|
1078
|
+
SELECT id FROM memory
|
|
1079
|
+
WHERE created_at < time::now() - 14d
|
|
1080
|
+
AND importance <= 2.0
|
|
1081
|
+
AND (access_count = 0 OR access_count IS NONE)
|
|
1082
|
+
AND string::concat("memory:", id) NOT IN (
|
|
1083
|
+
SELECT VALUE memory_id FROM (
|
|
1084
|
+
SELECT memory_id FROM retrieval_outcome
|
|
1085
|
+
WHERE utilization > 0.2
|
|
1086
|
+
GROUP BY memory_id
|
|
1087
|
+
)
|
|
1088
|
+
)
|
|
1089
|
+
LIMIT 50
|
|
1090
|
+
);
|
|
1091
|
+
FOR $m IN $stale { DELETE $m.id; };
|
|
1092
|
+
RETURN array::len($stale);`
|
|
1093
|
+
);
|
|
1094
|
+
return Number(pruned ?? 0);
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
swallow.warn("surreal:garbageCollectMemories", e);
|
|
1097
|
+
return 0;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async archiveOldTurns() {
|
|
1101
|
+
try {
|
|
1102
|
+
const countRows = await this.queryFirst(
|
|
1103
|
+
`SELECT count() AS count FROM turn GROUP ALL`
|
|
1104
|
+
);
|
|
1105
|
+
const count = countRows[0]?.count ?? 0;
|
|
1106
|
+
if (count <= 2e3) return 0;
|
|
1107
|
+
const archived = await this.queryMulti(
|
|
1108
|
+
`LET $stale = (SELECT id FROM turn WHERE timestamp < time::now() - 7d AND id NOT IN (SELECT VALUE memory_id FROM retrieval_outcome WHERE memory_table = 'turn'));
|
|
1109
|
+
FOR $t IN $stale {
|
|
1110
|
+
INSERT INTO turn_archive (SELECT * FROM ONLY $t.id);
|
|
1111
|
+
DELETE $t.id;
|
|
1112
|
+
};
|
|
1113
|
+
RETURN array::len($stale);`
|
|
1114
|
+
);
|
|
1115
|
+
return Number(archived ?? 0);
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
swallow.warn("surreal:archiveOldTurns", e);
|
|
1118
|
+
return 0;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async consolidateMemories(embedFn) {
|
|
1122
|
+
try {
|
|
1123
|
+
const countRows = await this.queryFirst(
|
|
1124
|
+
`SELECT count() AS count FROM memory GROUP ALL`
|
|
1125
|
+
);
|
|
1126
|
+
const count = countRows[0]?.count ?? 0;
|
|
1127
|
+
if (count <= 50) return 0;
|
|
1128
|
+
let merged = 0;
|
|
1129
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1130
|
+
const embMemories = await this.queryFirst(
|
|
1131
|
+
`SELECT id, text, importance, category, access_count, embedding, created_at
|
|
1132
|
+
FROM memory
|
|
1133
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
1134
|
+
AND embedding_provider = $provider
|
|
1135
|
+
ORDER BY created_at ASC
|
|
1136
|
+
LIMIT 50`,
|
|
1137
|
+
{ provider: this.activeProvider }
|
|
1138
|
+
);
|
|
1139
|
+
for (const mem of embMemories) {
|
|
1140
|
+
if (seen.has(String(mem.id))) continue;
|
|
1141
|
+
const dupes = await this.queryFirst(
|
|
1142
|
+
`SELECT id, importance, access_count,
|
|
1143
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
1144
|
+
FROM memory
|
|
1145
|
+
WHERE id != $mid
|
|
1146
|
+
AND category = $cat
|
|
1147
|
+
AND embedding != NONE AND array::len(embedding) > 0
|
|
1148
|
+
AND embedding_provider = $provider
|
|
1149
|
+
ORDER BY score DESC
|
|
1150
|
+
LIMIT 3`,
|
|
1151
|
+
{ vec: mem.embedding, mid: mem.id, cat: mem.category, provider: this.activeProvider }
|
|
1152
|
+
);
|
|
1153
|
+
for (const dupe of dupes) {
|
|
1154
|
+
if (dupe.score < 0.88) break;
|
|
1155
|
+
if (seen.has(String(dupe.id))) continue;
|
|
1156
|
+
const keepMem = mem.importance > dupe.importance || mem.importance === dupe.importance && (mem.access_count ?? 0) >= (dupe.access_count ?? 0);
|
|
1157
|
+
const [keep, drop] = keepMem ? [mem.id, dupe.id] : [dupe.id, mem.id];
|
|
1158
|
+
assertRecordId(String(keep));
|
|
1159
|
+
assertRecordId(String(drop));
|
|
1160
|
+
await this.queryExec(
|
|
1161
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1162
|
+
{ imp: dupe.importance }
|
|
1163
|
+
);
|
|
1164
|
+
await this.queryExec(`DELETE ${String(drop)}`);
|
|
1165
|
+
seen.add(String(drop));
|
|
1166
|
+
merged++;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const unembedded = await this.queryFirst(
|
|
1170
|
+
`SELECT id, text, importance, category, access_count
|
|
1171
|
+
FROM memory
|
|
1172
|
+
WHERE embedding IS NONE OR array::len(embedding) = 0
|
|
1173
|
+
LIMIT 20`
|
|
1174
|
+
);
|
|
1175
|
+
for (const mem of unembedded) {
|
|
1176
|
+
if (seen.has(String(mem.id))) continue;
|
|
1177
|
+
try {
|
|
1178
|
+
const emb = await embedFn(mem.text);
|
|
1179
|
+
if (!emb) continue;
|
|
1180
|
+
await this.queryExec(
|
|
1181
|
+
`UPDATE ${String(mem.id)} SET embedding = $emb, embedding_provider = $provider`,
|
|
1182
|
+
{ emb, provider: this.activeProvider }
|
|
1183
|
+
);
|
|
1184
|
+
const dupes = await this.queryFirst(
|
|
1185
|
+
`SELECT id, importance, access_count,
|
|
1186
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
1187
|
+
FROM memory
|
|
1188
|
+
WHERE id != $mid
|
|
1189
|
+
AND category = $cat
|
|
1190
|
+
AND embedding != NONE AND array::len(embedding) > 0
|
|
1191
|
+
AND embedding_provider = $provider
|
|
1192
|
+
ORDER BY score DESC
|
|
1193
|
+
LIMIT 3`,
|
|
1194
|
+
{ vec: emb, mid: mem.id, cat: mem.category, provider: this.activeProvider }
|
|
1195
|
+
);
|
|
1196
|
+
for (const dupe of dupes) {
|
|
1197
|
+
if (dupe.score < 0.88) break;
|
|
1198
|
+
if (seen.has(String(dupe.id))) continue;
|
|
1199
|
+
const keepMem = mem.importance > dupe.importance || mem.importance === dupe.importance && (mem.access_count ?? 0) >= (dupe.access_count ?? 0);
|
|
1200
|
+
const [keep, drop] = keepMem ? [mem.id, dupe.id] : [dupe.id, mem.id];
|
|
1201
|
+
assertRecordId(String(keep));
|
|
1202
|
+
assertRecordId(String(drop));
|
|
1203
|
+
await this.queryExec(
|
|
1204
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1205
|
+
{ imp: dupe.importance }
|
|
1206
|
+
);
|
|
1207
|
+
await this.queryExec(`DELETE ${String(drop)}`);
|
|
1208
|
+
seen.add(String(drop));
|
|
1209
|
+
merged++;
|
|
1210
|
+
}
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
swallow.warn("surreal:consolidate-backfill", e);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return merged;
|
|
1216
|
+
} catch (e) {
|
|
1217
|
+
swallow.warn("surreal:consolidateMemories", e);
|
|
1218
|
+
return 0;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// ── Retrieval session memory ───────────────────────────────────────────
|
|
1222
|
+
async getSessionRetrievedMemories(sessionId) {
|
|
1223
|
+
try {
|
|
1224
|
+
const rows = await this.queryFirst(
|
|
1225
|
+
`SELECT memory_id FROM retrieval_outcome WHERE session_id = $sid AND memory_table = 'memory' GROUP BY memory_id`,
|
|
1226
|
+
{ sid: sessionId }
|
|
1227
|
+
);
|
|
1228
|
+
if (rows.length === 0) return [];
|
|
1229
|
+
const ids = rows.map((r) => r.memory_id).filter(Boolean);
|
|
1230
|
+
if (ids.length === 0) return [];
|
|
1231
|
+
const validated = ids.filter((id) => {
|
|
1232
|
+
try {
|
|
1233
|
+
assertRecordId(String(id));
|
|
1234
|
+
return true;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
if (validated.length === 0) return [];
|
|
1240
|
+
const idList = validated.join(", ");
|
|
1241
|
+
return this.queryFirst(
|
|
1242
|
+
`SELECT id, text FROM memory WHERE id IN [${idList}] AND (status = 'active' OR status IS NONE)`
|
|
1243
|
+
);
|
|
1244
|
+
} catch (e) {
|
|
1245
|
+
swallow.warn("surreal:getSessionRetrievedMemories", e);
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// ── Fibonacci resurfacing ──────────────────────────────────────────────
|
|
1250
|
+
async markSurfaceable(memoryId) {
|
|
1251
|
+
await this.queryExec(
|
|
1252
|
+
`UPDATE $id SET surfaceable = true, fib_index = 0, surface_count = 0, next_surface_at = time::now() + 1d`,
|
|
1253
|
+
{ id: memoryId }
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
async getDueMemories(limit = 5) {
|
|
1257
|
+
return await this.queryFirst(
|
|
1258
|
+
`SELECT id, text, importance, fib_index, surface_count, created_at
|
|
1259
|
+
FROM memory
|
|
1260
|
+
WHERE surfaceable = true
|
|
1261
|
+
AND next_surface_at <= time::now()
|
|
1262
|
+
AND status = 'active'
|
|
1263
|
+
ORDER BY importance DESC
|
|
1264
|
+
LIMIT $lim`,
|
|
1265
|
+
{ lim: limit }
|
|
1266
|
+
) ?? [];
|
|
1267
|
+
}
|
|
1268
|
+
// ── Compaction checkpoints ─────────────────────────────────────────────
|
|
1269
|
+
async createCompactionCheckpoint(sessionId, rangeStart, rangeEnd) {
|
|
1270
|
+
const rows = await this.queryFirst(
|
|
1271
|
+
`CREATE compaction_checkpoint CONTENT $data RETURN id`,
|
|
1272
|
+
{
|
|
1273
|
+
data: {
|
|
1274
|
+
session_id: sessionId,
|
|
1275
|
+
msg_range_start: rangeStart,
|
|
1276
|
+
msg_range_end: rangeEnd,
|
|
1277
|
+
status: "pending"
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
return String(rows[0]?.id ?? "");
|
|
1282
|
+
}
|
|
1283
|
+
async completeCompactionCheckpoint(checkpointId, memoryId) {
|
|
1284
|
+
assertRecordId(checkpointId);
|
|
1285
|
+
await this.queryExec(
|
|
1286
|
+
`UPDATE ${checkpointId} SET status = "complete", memory_id = $mid`,
|
|
1287
|
+
{ mid: memoryId }
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
async getPendingCheckpoints(sessionId) {
|
|
1291
|
+
return this.queryFirst(
|
|
1292
|
+
`SELECT id, msg_range_start, msg_range_end FROM compaction_checkpoint WHERE session_id = $sid AND (status = "pending" OR status = "failed")`,
|
|
1293
|
+
{ sid: sessionId }
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
// ── Availability check ────────────────────────────────────────────────
|
|
1297
|
+
isAvailable() {
|
|
1298
|
+
try {
|
|
1299
|
+
return this.db?.isConnected ?? false;
|
|
1300
|
+
} catch {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
clearReflectionCache() {
|
|
1305
|
+
this._reflectionSessions = null;
|
|
1306
|
+
}
|
|
1307
|
+
async getReflectionSessionIds() {
|
|
1308
|
+
if (this._reflectionSessions) return this._reflectionSessions;
|
|
1309
|
+
try {
|
|
1310
|
+
const rows = await this.queryFirst(
|
|
1311
|
+
`SELECT session_id FROM reflection GROUP BY session_id`
|
|
1312
|
+
);
|
|
1313
|
+
this._reflectionSessions = new Set(rows.map((r) => r.session_id).filter(Boolean));
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
swallow.warn("surreal:getReflectionSessionIds", e);
|
|
1316
|
+
this._reflectionSessions = /* @__PURE__ */ new Set();
|
|
1317
|
+
}
|
|
1318
|
+
return this._reflectionSessions;
|
|
1319
|
+
}
|
|
1320
|
+
static {
|
|
1321
|
+
// ── Fibonacci resurfacing: advance ────────────────────────────────────
|
|
1322
|
+
this.FIB_DAYS = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
|
|
1323
|
+
}
|
|
1324
|
+
async advanceSurfaceFade(memoryId) {
|
|
1325
|
+
const current = await this.queryFirst(
|
|
1326
|
+
`SELECT fib_index FROM $id`,
|
|
1327
|
+
{ id: memoryId }
|
|
1328
|
+
);
|
|
1329
|
+
const idx = current?.[0]?.fib_index ?? 0;
|
|
1330
|
+
const nextIdx = Math.min(idx + 1, _SurrealStore.FIB_DAYS.length - 1);
|
|
1331
|
+
const days = nextIdx < _SurrealStore.FIB_DAYS.length ? _SurrealStore.FIB_DAYS[nextIdx] : _SurrealStore.FIB_DAYS[_SurrealStore.FIB_DAYS.length - 1];
|
|
1332
|
+
await this.queryExec(
|
|
1333
|
+
`UPDATE $id SET fib_index = $nextIdx, surface_count += 1, last_surfaced = time::now(), next_surface_at = time::now() + type::duration($dur)`,
|
|
1334
|
+
{ id: memoryId, nextIdx, dur: `${days}d` }
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
async resolveSurfaceMemory(memoryId, outcome) {
|
|
1338
|
+
await this.queryExec(
|
|
1339
|
+
`UPDATE $id SET surfaceable = false, last_engaged = time::now(), surface_outcome = $outcome`,
|
|
1340
|
+
{ id: memoryId, outcome }
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
// ── Dispose ───────────────────────────────────────────────────────────
|
|
1344
|
+
async dispose() {
|
|
1345
|
+
try {
|
|
1346
|
+
await this.close();
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
swallow("surreal:dispose", e);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
export {
|
|
1354
|
+
__esm,
|
|
1355
|
+
__export,
|
|
1356
|
+
__toCommonJS,
|
|
1357
|
+
log,
|
|
1358
|
+
init_log,
|
|
1359
|
+
swallow,
|
|
1360
|
+
init_errors,
|
|
1361
|
+
loadSchema,
|
|
1362
|
+
assertRecordId,
|
|
1363
|
+
SurrealStore
|
|
1364
|
+
};
|