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 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.11
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.11",
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",
@@ -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
- // Extract and link concepts for both user and assistant turns
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
- }
@@ -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
 
@@ -5,6 +5,7 @@
5
5
  export interface TurnData {
6
6
  role: string;
7
7
  text: string;
8
+ turnId?: string;
8
9
  tool_name?: string;
9
10
  tool_result?: string;
10
11
  file_paths?: string[];
@@ -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. Concepts
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 upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction", { taskId, projectId });
263
+ await linkToConcepts(memId, "about_concept");
234
264
  }
235
265
  })());
236
266
  }
237
267
  }
238
268
 
239
- // 6. User preferences
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 upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference", { taskId, projectId });
282
+ await linkToConcepts(memId, "about_concept");
253
283
  }
254
284
  })());
255
285
  }
256
286
  }
257
287
 
258
- // 7. Artifacts
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 upsertAndLinkConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact", { taskId, projectId });
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
- // 8. Decisions
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 upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision", { taskId, projectId });
327
+ await linkToConcepts(memId, "about_concept");
298
328
  }
299
329
  })());
300
330
  }
301
331
  }
302
332
 
303
- // 9. Skills
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
- await store.queryExec(
317
- `CREATE skill CONTENT $record`,
318
- {
319
- record: {
320
- name: String(s.name).slice(0, 100),
321
- description: content,
322
- content,
323
- steps: s.steps.map((st: string) => String(st).slice(0, 200)),
324
- trigger_context: String(s.trigger_context ?? "").slice(0, 200),
325
- tags: ["auto-extracted"],
326
- session_id: sessionId,
327
- ...(emb ? { embedding: emb } : {}),
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
- ).catch(e => swallow.warn("daemon:createSkill", e));
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 },