kongbrain 0.3.12 → 0.3.15

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/README.github.md CHANGED
@@ -60,15 +60,13 @@ npm install -g openclaw
60
60
 
61
61
  Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
62
62
 
63
+ macOS:
63
64
  ```bash
64
- # macOS
65
65
  brew install surrealdb/tap/surreal
66
-
67
- # Linux (Debian/Ubuntu)
68
- curl -sSf https://install.surrealdb.com | sh
69
- export PATH="$HOME/.surrealdb:$PATH"
70
66
  ```
71
67
 
68
+ Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
69
+
72
70
  Then start it locally, **change the credentials before use**:
73
71
 
74
72
  ```bash
@@ -88,11 +86,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
88
86
 
89
87
  ### 3. Install KongBrain
90
88
 
89
+ From ClawHub (recommended):
91
90
  ```bash
92
- # From ClawHub (recommended)
93
91
  openclaw plugins install clawhub:kongbrain
92
+ ```
94
93
 
95
- # From npm (fallback)
94
+ From npm:
95
+ ```bash
96
96
  openclaw plugins install kongbrain
97
97
  ```
98
98
 
package/README.md CHANGED
@@ -50,15 +50,13 @@ npm install -g openclaw
50
50
 
51
51
  Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
52
52
 
53
+ macOS:
53
54
  ```bash
54
- # macOS
55
55
  brew install surrealdb/tap/surreal
56
-
57
- # Linux (Debian/Ubuntu)
58
- curl -sSf https://install.surrealdb.com | sh
59
- export PATH="$HOME/.surrealdb:$PATH"
60
56
  ```
61
57
 
58
+ Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
59
+
62
60
  Then start it locally — **change the credentials before use**:
63
61
 
64
62
  ```bash
@@ -78,11 +76,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
78
76
 
79
77
  ### 3. Install KongBrain
80
78
 
79
+ From ClawHub (recommended):
81
80
  ```bash
82
- # From ClawHub (recommended)
83
81
  openclaw plugins install clawhub:kongbrain
82
+ ```
84
83
 
85
- # From npm (fallback)
84
+ From npm:
85
+ ```bash
86
86
  openclaw plugins install kongbrain
87
87
  ```
88
88
 
package/README.npm.md CHANGED
@@ -50,15 +50,13 @@ npm install -g openclaw
50
50
 
51
51
  Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
52
52
 
53
+ macOS:
53
54
  ```bash
54
- # macOS
55
55
  brew install surrealdb/tap/surreal
56
-
57
- # Linux (Debian/Ubuntu)
58
- curl -sSf https://install.surrealdb.com | sh
59
- export PATH="$HOME/.surrealdb:$PATH"
60
56
  ```
61
57
 
58
+ Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
59
+
62
60
  Then start it locally — **change the credentials before use**:
63
61
 
64
62
  ```bash
@@ -78,11 +76,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
78
76
 
79
77
  ### 3. Install KongBrain
80
78
 
79
+ From ClawHub (recommended):
81
80
  ```bash
82
- # From ClawHub (recommended)
83
81
  openclaw plugins install clawhub:kongbrain
82
+ ```
84
83
 
85
- # From npm (fallback)
84
+ From npm:
85
+ ```bash
86
86
  openclaw plugins install kongbrain
87
87
  ```
88
88
 
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.12
4
+ version: 0.3.15
5
5
  homepage: https://github.com/42U/kongbrain
6
6
  metadata:
7
7
  openclaw:
@@ -48,23 +48,22 @@ KongBrain gives your OpenClaw agent persistent, structured memory:
48
48
 
49
49
  See the official install guide: https://surrealdb.com/docs/surrealdb/installation
50
50
 
51
- Platform packages:
52
-
51
+ macOS:
53
52
  ```bash
54
- # macOS
55
53
  brew install surrealdb/tap/surreal
54
+ ```
56
55
 
57
- # Linux (Debian/Ubuntu)
58
- curl -sSf https://install.surrealdb.com | sh
56
+ Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
59
57
 
60
- # Docker
58
+ Docker:
59
+ ```bash
61
60
  docker pull surrealdb/surrealdb:latest
62
61
  ```
63
62
 
64
63
  ### Start SurrealDB
65
64
 
65
+ Local only (recommended) — use strong credentials in production:
66
66
  ```bash
67
- # Local only (recommended) - use strong credentials in production
68
67
  surreal start --user youruser --pass yourpass --bind 127.0.0.1:8000 surrealkv:~/.kongbrain/surreal.db
69
68
  ```
70
69
 
@@ -3,6 +3,10 @@
3
3
  "name": "KongBrain",
4
4
  "description": "Graph-backed cognitive context engine with SurrealDB + BGE-M3",
5
5
  "kind": "context-engine",
6
+ "requires": {
7
+ "bins": ["surreal"],
8
+ "env": ["SURREAL_URL", "SURREAL_USER", "SURREAL_PASS", "SURREAL_NS", "SURREAL_DB"]
9
+ },
6
10
  "uiHints": {
7
11
  "surreal.url": {
8
12
  "label": "SurrealDB URL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kongbrain",
3
- "version": "0.3.12",
3
+ "version": "0.3.15",
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",
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * One-shot backfill: embed all concepts that have `content` but no embedding vector.
4
+ *
5
+ * Usage:
6
+ * cd /home/zero/voidorigin/kongbrain
7
+ * npx tsx scripts/backfill-embeddings.ts
8
+ *
9
+ * Env vars (all have defaults matching the plugin config):
10
+ * SURREAL_URL (default: ws://localhost:8042/rpc)
11
+ * SURREAL_USER (default: root)
12
+ * SURREAL_PASS (default: root)
13
+ * SURREAL_NS (default: kong)
14
+ * SURREAL_DB (default: memory)
15
+ * EMBED_MODEL_PATH (default: ~/.node-llama-cpp/models/bge-m3-q4_k_m.gguf)
16
+ */
17
+
18
+ import { parsePluginConfig } from "../src/config.js";
19
+ import { SurrealStore } from "../src/surreal.js";
20
+ import { EmbeddingService } from "../src/embeddings.js";
21
+
22
+ async function main() {
23
+ const config = parsePluginConfig();
24
+ const store = new SurrealStore(config.surreal);
25
+ const embeddings = new EmbeddingService(config.embedding);
26
+
27
+ console.log("[backfill] Connecting to SurrealDB...");
28
+ await store.initialize();
29
+
30
+ console.log("[backfill] Loading embedding model...");
31
+ await embeddings.initialize();
32
+
33
+ // Find concepts with content but no embedding
34
+ const bare = await store.queryFirst<{ id: string; content: string }>(
35
+ `SELECT id, content FROM concept
36
+ WHERE content IS NOT NONE AND content != ''
37
+ AND (embedding IS NONE OR array::len(embedding) = 0)`,
38
+ );
39
+
40
+ console.log(`[backfill] Found ${bare.length} concepts needing embeddings.`);
41
+ if (bare.length === 0) {
42
+ console.log("[backfill] Nothing to do.");
43
+ await embeddings.dispose();
44
+ return;
45
+ }
46
+
47
+ let ok = 0;
48
+ let fail = 0;
49
+
50
+ for (const concept of bare) {
51
+ const id = String(concept.id);
52
+ try {
53
+ const vec = await embeddings.embed(concept.content);
54
+ await store.queryExec(
55
+ `UPDATE ${id} SET embedding = $emb`,
56
+ { emb: vec },
57
+ );
58
+ ok++;
59
+ if (ok % 10 === 0) console.log(`[backfill] ${ok}/${bare.length} done...`);
60
+ } catch (e) {
61
+ fail++;
62
+ console.error(`[backfill] Failed ${id}: ${e}`);
63
+ }
64
+ }
65
+
66
+ console.log(`[backfill] Complete. Embedded: ${ok}, Failed: ${fail}`);
67
+ await embeddings.dispose();
68
+ process.exit(0);
69
+ }
70
+
71
+ main().catch((e) => {
72
+ console.error("[backfill] Fatal:", e);
73
+ process.exit(1);
74
+ });
@@ -79,6 +79,48 @@ export async function upsertAndLinkConcepts(
79
79
  }
80
80
  }
81
81
 
82
+ /**
83
+ * Embedding-based concept linking — replaces batch-local linkToConcepts.
84
+ *
85
+ * Given a source node (memory, artifact, turn, skill) and its text content,
86
+ * embeds the text and finds the top-N most similar concepts in the graph,
87
+ * then creates edges from source → concept via the specified relation.
88
+ *
89
+ * This ensures linking works even when relevant concepts were created in
90
+ * prior batches or sessions — no batch-timing dependency.
91
+ */
92
+ export async function linkToRelevantConcepts(
93
+ sourceId: string,
94
+ edgeName: string,
95
+ text: string,
96
+ store: SurrealStore,
97
+ embeddings: EmbeddingService,
98
+ logTag: string,
99
+ limit = 5,
100
+ threshold = 0.65,
101
+ ): Promise<void> {
102
+ if (!embeddings.isAvailable() || !text) return;
103
+ try {
104
+ const vec = await embeddings.embed(text);
105
+ if (!vec?.length) return;
106
+ const matches = await store.queryFirst<{ id: string; score: number }>(
107
+ `SELECT id, vector::similarity::cosine(embedding, $vec) AS score
108
+ FROM concept
109
+ WHERE embedding != NONE AND array::len(embedding) > 0
110
+ ORDER BY score DESC
111
+ LIMIT $lim`,
112
+ { vec, lim: limit },
113
+ );
114
+ for (const m of matches) {
115
+ if (m.score < threshold) break;
116
+ await store.relate(sourceId, edgeName, String(m.id))
117
+ .catch(e => swallow(`${logTag}:relate`, e));
118
+ }
119
+ } catch (e) {
120
+ swallow(`${logTag}:embed`, e);
121
+ }
122
+ }
123
+
82
124
  /**
83
125
  * Link a newly-upserted concept to existing concepts via narrower/broader
84
126
  * edges when one concept's name is a substring of the other (indicating a
@@ -5,7 +5,7 @@
5
5
  import type { GlobalPluginState } from "../state.js";
6
6
  import { recordToolOutcome } from "../retrieval-quality.js";
7
7
  import { swallow } from "../errors.js";
8
- import { upsertAndLinkConcepts } from "../concept-extract.js";
8
+ import { linkToRelevantConcepts } from "../concept-extract.js";
9
9
 
10
10
  export function createAfterToolCallHandler(state: GlobalPluginState) {
11
11
  return async (
@@ -128,8 +128,8 @@ async function trackArtifact(
128
128
  await state.store.relate(artifactId, "used_in", projectId)
129
129
  .catch(e => swallow.warn("artifact:used_in", e));
130
130
  }
131
- // Link artifact to concepts it mentions
132
- await upsertAndLinkConcepts(
131
+ // Link artifact to concepts it mentions (embedding-based similarity)
132
+ await linkToRelevantConcepts(
133
133
  artifactId, "artifact_mentions", description,
134
134
  state.store, state.embeddings, "artifact:concepts",
135
135
  );
@@ -13,7 +13,7 @@ import type { SurrealStore } from "./surreal.js";
13
13
  import type { EmbeddingService } from "./embeddings.js";
14
14
  import { swallow } from "./errors.js";
15
15
  import { assertRecordId } from "./surreal.js";
16
- import { upsertAndLinkConcepts, linkConceptHierarchy } from "./concept-extract.js";
16
+ import { upsertAndLinkConcepts, linkConceptHierarchy, linkToRelevantConcepts } from "./concept-extract.js";
17
17
 
18
18
  // --- Build the extraction prompt ---
19
19
 
@@ -170,29 +170,23 @@ export async function writeExtractionResults(
170
170
  }
171
171
  }
172
172
 
173
- // ── Phase 2: Create mentions edges (turn → concept) using LLM concepts
174
- // Links batch turns to extracted concepts, replacing regex-based extraction.
173
+ // ── Phase 2: Create mentions edges (turn → concept) via embedding similarity
174
+ // Each turn's text is embedded and matched against existing concepts in the
175
+ // graph. This replaces the old batch-local linking that only worked when
176
+ // concepts and turns were extracted in the same batch.
175
177
 
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
- }
178
+ if (turns && turns.length > 0) {
179
+ const turnIds = turns.filter(t => t.turnId && t.text).slice(0, 15);
180
+ for (const t of turnIds) {
181
+ await linkToRelevantConcepts(
182
+ t.turnId!, "mentions", t.text!,
183
+ store, embeddings, "daemon:mentions", 5, 0.65,
184
+ );
183
185
  }
184
186
  }
185
187
 
186
188
  // ── Phase 3: All other extractions in parallel ───────────────────────
187
189
 
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
-
196
190
  const writeOps: Promise<void>[] = [];
197
191
 
198
192
  // 1. Causal chains
@@ -260,7 +254,7 @@ export async function writeExtractionResults(
260
254
  }
261
255
  const memId = await store.createMemory(text, emb, 9, "correction", sessionId);
262
256
  if (memId) {
263
- await linkToConcepts(memId, "about_concept");
257
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction:about_concept");
264
258
  }
265
259
  })());
266
260
  }
@@ -279,7 +273,7 @@ export async function writeExtractionResults(
279
273
  }
280
274
  const memId = await store.createMemory(text, emb, 7, "preference", sessionId);
281
275
  if (memId) {
282
- await linkToConcepts(memId, "about_concept");
276
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference:about_concept");
283
277
  }
284
278
  })());
285
279
  }
@@ -300,7 +294,7 @@ export async function writeExtractionResults(
300
294
  }
301
295
  const artId = await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
302
296
  if (artId) {
303
- await linkToConcepts(artId, "artifact_mentions");
297
+ await linkToRelevantConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact:artifact_mentions");
304
298
  // used_in: artifact → project
305
299
  if (projectId) {
306
300
  await store.relate(artId, "used_in", projectId)
@@ -324,7 +318,7 @@ export async function writeExtractionResults(
324
318
  }
325
319
  const memId = await store.createMemory(text, emb, 7, "decision", sessionId);
326
320
  if (memId) {
327
- await linkToConcepts(memId, "about_concept");
321
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision:about_concept");
328
322
  }
329
323
  })());
330
324
  }
package/src/skills.ts CHANGED
@@ -13,7 +13,7 @@ import type { CompleteFn } from "./state.js";
13
13
  import type { EmbeddingService } from "./embeddings.js";
14
14
  import type { SurrealStore } from "./surreal.js";
15
15
  import { swallow } from "./errors.js";
16
- import { upsertAndLinkConcepts } from "./concept-extract.js";
16
+ import { linkToRelevantConcepts } from "./concept-extract.js";
17
17
  import { assertRecordId } from "./surreal.js";
18
18
 
19
19
  // --- Types ---
@@ -122,7 +122,7 @@ export async function extractSkill(
122
122
  await supersedeOldSkills(skillId, skillEmb ?? [], store);
123
123
  // skill_uses_concept: skill → concept
124
124
  const skillDesc = `${parsed.name} ${parsed.description ?? ""} ${(parsed.preconditions ?? "")}`;
125
- await upsertAndLinkConcepts(skillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:concepts");
125
+ await linkToRelevantConcepts(skillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:concepts");
126
126
  }
127
127
 
128
128
  return skillId || null;
@@ -334,7 +334,7 @@ export async function graduateCausalToSkills(
334
334
  await supersedeOldSkills(gradSkillId, skillEmb ?? [], store);
335
335
  // skill_uses_concept: skill → concept
336
336
  const skillDesc = `${parsed.name} ${parsed.description ?? ""}`;
337
- await upsertAndLinkConcepts(gradSkillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:graduate:concepts");
337
+ await linkToRelevantConcepts(gradSkillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:graduate:concepts");
338
338
  created++;
339
339
  }
340
340
  }
package/src/surreal.ts CHANGED
@@ -692,9 +692,17 @@ export class SurrealStore {
692
692
  );
693
693
  if (rows.length > 0) {
694
694
  const id = String(rows[0].id);
695
- await this.queryExec(
696
- `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
697
- );
695
+ // Backfill embedding if the existing concept is missing one
696
+ if (embedding?.length) {
697
+ await this.queryExec(
698
+ `UPDATE ${id} SET access_count += 1, last_accessed = time::now(), embedding = IF embedding IS NONE OR array::len(embedding) = 0 THEN $emb ELSE embedding END`,
699
+ { emb: embedding },
700
+ );
701
+ } else {
702
+ await this.queryExec(
703
+ `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
704
+ );
705
+ }
698
706
  return id;
699
707
  }
700
708
  const emb = embedding?.length ? embedding : undefined;