kongbrain 0.3.11 → 0.3.12
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/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/context-engine.ts +3 -21
- package/src/daemon-manager.ts +1 -1
- package/src/daemon-types.ts +1 -0
- package/src/memory-daemon.ts +97 -53
- package/src/schema.surql +2 -0
- package/src/surreal.ts +2 -0
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kongbrain
|
|
3
3
|
description: Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.12
|
|
5
5
|
homepage: https://github.com/42U/kongbrain
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/context-engine.ts
CHANGED
|
@@ -49,7 +49,6 @@ import { extractSkill } from "./skills.js";
|
|
|
49
49
|
import { generateReflection } from "./reflection.js";
|
|
50
50
|
import { graduateCausalToSkills } from "./skills.js";
|
|
51
51
|
import { swallow } from "./errors.js";
|
|
52
|
-
import { upsertAndLinkConcepts } from "./concept-extract.js";
|
|
53
52
|
|
|
54
53
|
export class KongBrainContextEngine implements ContextEngine {
|
|
55
54
|
readonly info: ContextEngineInfo = {
|
|
@@ -265,11 +264,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
265
264
|
.catch(e => swallow.warn("ingest:responds_to", e));
|
|
266
265
|
}
|
|
267
266
|
|
|
268
|
-
//
|
|
269
|
-
if (worthEmbedding) {
|
|
270
|
-
extractAndLinkConcepts(turnId, text, this.state, session)
|
|
271
|
-
.catch(e => swallow.warn("ingest:concepts", e));
|
|
272
|
-
}
|
|
267
|
+
// Concept extraction (mentions edges) handled by daemon via LLM
|
|
273
268
|
}
|
|
274
269
|
|
|
275
270
|
if (role === "user") {
|
|
@@ -400,7 +395,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
400
395
|
const turnData = recentTurns.map(t => ({
|
|
401
396
|
role: t.role as "user" | "assistant",
|
|
402
397
|
text: t.text,
|
|
403
|
-
turnId: (t as any).id,
|
|
398
|
+
turnId: String((t as any).id ?? ""),
|
|
404
399
|
}));
|
|
405
400
|
|
|
406
401
|
// Gather retrieved memory IDs for dedup
|
|
@@ -441,7 +436,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
441
436
|
const turnData = recentTurns.map(t => ({
|
|
442
437
|
role: t.role as "user" | "assistant",
|
|
443
438
|
text: t.text,
|
|
444
|
-
turnId: (t as any).id,
|
|
439
|
+
turnId: String((t as any).id ?? ""),
|
|
445
440
|
}));
|
|
446
441
|
session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
|
|
447
442
|
})
|
|
@@ -537,16 +532,3 @@ function hasSemantic(text: string): boolean {
|
|
|
537
532
|
}
|
|
538
533
|
|
|
539
534
|
// --- Concept extraction (delegates to shared helper) ---
|
|
540
|
-
|
|
541
|
-
async function extractAndLinkConcepts(
|
|
542
|
-
turnId: string,
|
|
543
|
-
text: string,
|
|
544
|
-
state: GlobalPluginState,
|
|
545
|
-
session?: SessionState,
|
|
546
|
-
): Promise<void> {
|
|
547
|
-
await upsertAndLinkConcepts(
|
|
548
|
-
turnId, "mentions", text,
|
|
549
|
-
state.store, state.embeddings, "concepts",
|
|
550
|
-
session ? { taskId: session.taskId, projectId: session.projectId } : undefined,
|
|
551
|
-
);
|
|
552
|
-
}
|
package/src/daemon-manager.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function startMemoryDaemon(
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId);
|
|
124
|
+
const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId, turns);
|
|
125
125
|
extractedTurnCount = turns.length;
|
|
126
126
|
}
|
|
127
127
|
|
package/src/daemon-types.ts
CHANGED
package/src/memory-daemon.ts
CHANGED
|
@@ -127,12 +127,72 @@ export async function writeExtractionResults(
|
|
|
127
127
|
priorState: PriorExtractions,
|
|
128
128
|
taskId?: string,
|
|
129
129
|
projectId?: string,
|
|
130
|
+
turns?: TurnData[],
|
|
130
131
|
): Promise<ExtractionCounts> {
|
|
131
132
|
const counts: ExtractionCounts = {
|
|
132
133
|
causal: 0, monologue: 0, resolved: 0, concept: 0,
|
|
133
134
|
correction: 0, preference: 0, artifact: 0, decision: 0, skill: 0,
|
|
134
135
|
};
|
|
135
136
|
|
|
137
|
+
// ── Phase 1: Upsert concepts first (LLM-extracted) so we have IDs ────
|
|
138
|
+
// These IDs are used to create mentions/about_concept/artifact_mentions
|
|
139
|
+
// edges in Phase 2, replacing the old regex-based extraction.
|
|
140
|
+
|
|
141
|
+
const extractedConceptIds: string[] = [];
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(result.concepts) && result.concepts.length > 0) {
|
|
144
|
+
for (const c of result.concepts.slice(0, 11)) {
|
|
145
|
+
if (!c.name || !c.content) continue;
|
|
146
|
+
if (priorState.conceptNames.includes(c.name)) continue;
|
|
147
|
+
counts.concept++;
|
|
148
|
+
priorState.conceptNames.push(c.name);
|
|
149
|
+
try {
|
|
150
|
+
let emb: number[] | null = null;
|
|
151
|
+
if (embeddings.isAvailable()) {
|
|
152
|
+
try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
|
|
153
|
+
}
|
|
154
|
+
const conceptId = await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
155
|
+
if (conceptId) {
|
|
156
|
+
extractedConceptIds.push(conceptId);
|
|
157
|
+
await linkConceptHierarchy(conceptId, c.name, store, embeddings, "daemon:concept");
|
|
158
|
+
if (taskId) {
|
|
159
|
+
await store.relate(conceptId, "derived_from", taskId)
|
|
160
|
+
.catch(e => swallow("daemon:concept:derived_from", e));
|
|
161
|
+
}
|
|
162
|
+
if (projectId) {
|
|
163
|
+
await store.relate(conceptId, "relevant_to", projectId)
|
|
164
|
+
.catch(e => swallow("daemon:concept:relevant_to", e));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
swallow.warn("daemon:upsertConcept", e);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Phase 2: Create mentions edges (turn → concept) using LLM concepts ─
|
|
174
|
+
// Links batch turns to extracted concepts, replacing regex-based extraction.
|
|
175
|
+
|
|
176
|
+
if (extractedConceptIds.length > 0 && turns && turns.length > 0) {
|
|
177
|
+
const turnIds = turns.map(t => t.turnId).filter((id): id is string => !!id);
|
|
178
|
+
for (const turnId of turnIds) {
|
|
179
|
+
for (const conceptId of extractedConceptIds) {
|
|
180
|
+
store.relate(turnId, "mentions", conceptId)
|
|
181
|
+
.catch(e => swallow("daemon:mentions", e));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Phase 3: All other extractions in parallel ───────────────────────
|
|
187
|
+
|
|
188
|
+
/** Link a source node to all extracted concepts via the given edge. */
|
|
189
|
+
const linkToConcepts = async (sourceId: string, edgeName: string) => {
|
|
190
|
+
for (const conceptId of extractedConceptIds) {
|
|
191
|
+
await store.relate(sourceId, edgeName, conceptId)
|
|
192
|
+
.catch(e => swallow(`daemon:${edgeName}`, e));
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
136
196
|
const writeOps: Promise<void>[] = [];
|
|
137
197
|
|
|
138
198
|
// 1. Causal chains
|
|
@@ -187,37 +247,7 @@ export async function writeExtractionResults(
|
|
|
187
247
|
})());
|
|
188
248
|
}
|
|
189
249
|
|
|
190
|
-
// 4.
|
|
191
|
-
if (Array.isArray(result.concepts) && result.concepts.length > 0) {
|
|
192
|
-
for (const c of result.concepts.slice(0, 11)) {
|
|
193
|
-
if (!c.name || !c.content) continue;
|
|
194
|
-
if (priorState.conceptNames.includes(c.name)) continue;
|
|
195
|
-
counts.concept++;
|
|
196
|
-
priorState.conceptNames.push(c.name);
|
|
197
|
-
writeOps.push((async () => {
|
|
198
|
-
let emb: number[] | null = null;
|
|
199
|
-
if (embeddings.isAvailable()) {
|
|
200
|
-
try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
|
|
201
|
-
}
|
|
202
|
-
const conceptId = await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
203
|
-
if (conceptId) {
|
|
204
|
-
await linkConceptHierarchy(conceptId, c.name, store, embeddings, "daemon:concept");
|
|
205
|
-
// derived_from: concept → task
|
|
206
|
-
if (taskId) {
|
|
207
|
-
await store.relate(conceptId, "derived_from", taskId)
|
|
208
|
-
.catch(e => swallow("daemon:concept:derived_from", e));
|
|
209
|
-
}
|
|
210
|
-
// relevant_to: concept → project
|
|
211
|
-
if (projectId) {
|
|
212
|
-
await store.relate(conceptId, "relevant_to", projectId)
|
|
213
|
-
.catch(e => swallow("daemon:concept:relevant_to", e));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
})());
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 5. Corrections — high-importance memories
|
|
250
|
+
// 4. Corrections — high-importance memories, linked to LLM-extracted concepts
|
|
221
251
|
if (Array.isArray(result.corrections) && result.corrections.length > 0) {
|
|
222
252
|
for (const c of result.corrections.slice(0, 5)) {
|
|
223
253
|
if (!c.original || !c.correction) continue;
|
|
@@ -230,13 +260,13 @@ export async function writeExtractionResults(
|
|
|
230
260
|
}
|
|
231
261
|
const memId = await store.createMemory(text, emb, 9, "correction", sessionId);
|
|
232
262
|
if (memId) {
|
|
233
|
-
await
|
|
263
|
+
await linkToConcepts(memId, "about_concept");
|
|
234
264
|
}
|
|
235
265
|
})());
|
|
236
266
|
}
|
|
237
267
|
}
|
|
238
268
|
|
|
239
|
-
//
|
|
269
|
+
// 5. User preferences
|
|
240
270
|
if (Array.isArray(result.preferences) && result.preferences.length > 0) {
|
|
241
271
|
for (const p of result.preferences.slice(0, 5)) {
|
|
242
272
|
if (!p.preference) continue;
|
|
@@ -249,13 +279,13 @@ export async function writeExtractionResults(
|
|
|
249
279
|
}
|
|
250
280
|
const memId = await store.createMemory(text, emb, 7, "preference", sessionId);
|
|
251
281
|
if (memId) {
|
|
252
|
-
await
|
|
282
|
+
await linkToConcepts(memId, "about_concept");
|
|
253
283
|
}
|
|
254
284
|
})());
|
|
255
285
|
}
|
|
256
286
|
}
|
|
257
287
|
|
|
258
|
-
//
|
|
288
|
+
// 6. Artifacts
|
|
259
289
|
if (Array.isArray(result.artifacts) && result.artifacts.length > 0) {
|
|
260
290
|
for (const a of result.artifacts.slice(0, 10)) {
|
|
261
291
|
if (!a.path) continue;
|
|
@@ -270,7 +300,7 @@ export async function writeExtractionResults(
|
|
|
270
300
|
}
|
|
271
301
|
const artId = await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
|
|
272
302
|
if (artId) {
|
|
273
|
-
await
|
|
303
|
+
await linkToConcepts(artId, "artifact_mentions");
|
|
274
304
|
// used_in: artifact → project
|
|
275
305
|
if (projectId) {
|
|
276
306
|
await store.relate(artId, "used_in", projectId)
|
|
@@ -281,7 +311,7 @@ export async function writeExtractionResults(
|
|
|
281
311
|
}
|
|
282
312
|
}
|
|
283
313
|
|
|
284
|
-
//
|
|
314
|
+
// 7. Decisions
|
|
285
315
|
if (Array.isArray(result.decisions) && result.decisions.length > 0) {
|
|
286
316
|
for (const d of result.decisions.slice(0, 6)) {
|
|
287
317
|
if (!d.decision) continue;
|
|
@@ -294,13 +324,13 @@ export async function writeExtractionResults(
|
|
|
294
324
|
}
|
|
295
325
|
const memId = await store.createMemory(text, emb, 7, "decision", sessionId);
|
|
296
326
|
if (memId) {
|
|
297
|
-
await
|
|
327
|
+
await linkToConcepts(memId, "about_concept");
|
|
298
328
|
}
|
|
299
329
|
})());
|
|
300
330
|
}
|
|
301
331
|
}
|
|
302
332
|
|
|
303
|
-
//
|
|
333
|
+
// 8. Skills — get ID back to create skill_from_task + skill_uses_concept edges
|
|
304
334
|
if (Array.isArray(result.skills) && result.skills.length > 0) {
|
|
305
335
|
for (const s of result.skills.slice(0, 3)) {
|
|
306
336
|
if (!s.name || !Array.isArray(s.steps) || s.steps.length === 0) continue;
|
|
@@ -313,21 +343,35 @@ export async function writeExtractionResults(
|
|
|
313
343
|
if (embeddings.isAvailable()) {
|
|
314
344
|
try { emb = await embeddings.embed(content); } catch (e) { swallow("daemon:embedSkill", e); }
|
|
315
345
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
346
|
+
try {
|
|
347
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
348
|
+
`CREATE skill CONTENT $record RETURN id`,
|
|
349
|
+
{
|
|
350
|
+
record: {
|
|
351
|
+
name: String(s.name).slice(0, 100),
|
|
352
|
+
description: content,
|
|
353
|
+
content,
|
|
354
|
+
steps: s.steps.map((st: string) => String(st).slice(0, 200)),
|
|
355
|
+
trigger_context: String(s.trigger_context ?? "").slice(0, 200),
|
|
356
|
+
tags: ["auto-extracted"],
|
|
357
|
+
session_id: sessionId,
|
|
358
|
+
...(emb ? { embedding: emb } : {}),
|
|
359
|
+
},
|
|
328
360
|
},
|
|
329
|
-
|
|
330
|
-
|
|
361
|
+
);
|
|
362
|
+
const skillId = rows[0]?.id ? String(rows[0].id) : null;
|
|
363
|
+
if (skillId) {
|
|
364
|
+
// skill_from_task: skill → task
|
|
365
|
+
if (taskId) {
|
|
366
|
+
await store.relate(skillId, "skill_from_task", taskId)
|
|
367
|
+
.catch(e => swallow.warn("daemon:skill:skill_from_task", e));
|
|
368
|
+
}
|
|
369
|
+
// skill_uses_concept: skill → concept
|
|
370
|
+
await upsertAndLinkConcepts(skillId, "skill_uses_concept", content, store, embeddings, "daemon:skill:concepts");
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
swallow.warn("daemon:createSkill", e);
|
|
374
|
+
}
|
|
331
375
|
})());
|
|
332
376
|
}
|
|
333
377
|
}
|
package/src/schema.surql
CHANGED
|
@@ -47,6 +47,8 @@ DEFINE INDEX IF NOT EXISTS artifact_vec_idx ON artifact FIELDS embedding HNSW DI
|
|
|
47
47
|
-- PILLAR 5: Concept (semantic knowledge nodes)
|
|
48
48
|
-- ============================================================
|
|
49
49
|
DEFINE TABLE IF NOT EXISTS concept SCHEMALESS;
|
|
50
|
+
-- Recovery: restore content from name if the rename migration ran before revert
|
|
51
|
+
UPDATE concept SET content = name WHERE content = NONE AND name != NONE;
|
|
50
52
|
DEFINE FIELD IF NOT EXISTS content ON concept TYPE string;
|
|
51
53
|
DEFINE FIELD IF NOT EXISTS embedding ON concept TYPE option<array<float>>;
|
|
52
54
|
DEFINE FIELD IF NOT EXISTS stability ON concept TYPE float DEFAULT 1.0;
|
package/src/surreal.ts
CHANGED
|
@@ -684,6 +684,8 @@ export class SurrealStore {
|
|
|
684
684
|
embedding: number[] | null,
|
|
685
685
|
source?: string,
|
|
686
686
|
): Promise<string> {
|
|
687
|
+
if (!content?.trim()) return "";
|
|
688
|
+
content = content.trim();
|
|
687
689
|
const rows = await this.queryFirst<{ id: string }>(
|
|
688
690
|
`SELECT id FROM concept WHERE string::lowercase(content) = string::lowercase($content) LIMIT 1`,
|
|
689
691
|
{ content },
|