kongbrain 0.3.11 → 0.3.14

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.11
4
+ version: 0.3.14
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.11",
3
+ "version": "0.3.14",
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
@@ -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[];
@@ -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
 
@@ -127,12 +127,66 @@ 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) 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.
177
+
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
+ );
185
+ }
186
+ }
187
+
188
+ // ── Phase 3: All other extractions in parallel ───────────────────────
189
+
136
190
  const writeOps: Promise<void>[] = [];
137
191
 
138
192
  // 1. Causal chains
@@ -187,37 +241,7 @@ export async function writeExtractionResults(
187
241
  })());
188
242
  }
189
243
 
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
244
+ // 4. Corrections — high-importance memories, linked to LLM-extracted concepts
221
245
  if (Array.isArray(result.corrections) && result.corrections.length > 0) {
222
246
  for (const c of result.corrections.slice(0, 5)) {
223
247
  if (!c.original || !c.correction) continue;
@@ -230,13 +254,13 @@ export async function writeExtractionResults(
230
254
  }
231
255
  const memId = await store.createMemory(text, emb, 9, "correction", sessionId);
232
256
  if (memId) {
233
- await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction", { taskId, projectId });
257
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction:about_concept");
234
258
  }
235
259
  })());
236
260
  }
237
261
  }
238
262
 
239
- // 6. User preferences
263
+ // 5. User preferences
240
264
  if (Array.isArray(result.preferences) && result.preferences.length > 0) {
241
265
  for (const p of result.preferences.slice(0, 5)) {
242
266
  if (!p.preference) continue;
@@ -249,13 +273,13 @@ export async function writeExtractionResults(
249
273
  }
250
274
  const memId = await store.createMemory(text, emb, 7, "preference", sessionId);
251
275
  if (memId) {
252
- await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference", { taskId, projectId });
276
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference:about_concept");
253
277
  }
254
278
  })());
255
279
  }
256
280
  }
257
281
 
258
- // 7. Artifacts
282
+ // 6. Artifacts
259
283
  if (Array.isArray(result.artifacts) && result.artifacts.length > 0) {
260
284
  for (const a of result.artifacts.slice(0, 10)) {
261
285
  if (!a.path) continue;
@@ -270,7 +294,7 @@ export async function writeExtractionResults(
270
294
  }
271
295
  const artId = await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
272
296
  if (artId) {
273
- await upsertAndLinkConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact", { taskId, projectId });
297
+ await linkToRelevantConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact:artifact_mentions");
274
298
  // used_in: artifact → project
275
299
  if (projectId) {
276
300
  await store.relate(artId, "used_in", projectId)
@@ -281,7 +305,7 @@ export async function writeExtractionResults(
281
305
  }
282
306
  }
283
307
 
284
- // 8. Decisions
308
+ // 7. Decisions
285
309
  if (Array.isArray(result.decisions) && result.decisions.length > 0) {
286
310
  for (const d of result.decisions.slice(0, 6)) {
287
311
  if (!d.decision) continue;
@@ -294,13 +318,13 @@ export async function writeExtractionResults(
294
318
  }
295
319
  const memId = await store.createMemory(text, emb, 7, "decision", sessionId);
296
320
  if (memId) {
297
- await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision", { taskId, projectId });
321
+ await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision:about_concept");
298
322
  }
299
323
  })());
300
324
  }
301
325
  }
302
326
 
303
- // 9. Skills
327
+ // 8. Skills — get ID back to create skill_from_task + skill_uses_concept edges
304
328
  if (Array.isArray(result.skills) && result.skills.length > 0) {
305
329
  for (const s of result.skills.slice(0, 3)) {
306
330
  if (!s.name || !Array.isArray(s.steps) || s.steps.length === 0) continue;
@@ -313,21 +337,35 @@ export async function writeExtractionResults(
313
337
  if (embeddings.isAvailable()) {
314
338
  try { emb = await embeddings.embed(content); } catch (e) { swallow("daemon:embedSkill", e); }
315
339
  }
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 } : {}),
340
+ try {
341
+ const rows = await store.queryFirst<{ id: string }>(
342
+ `CREATE skill CONTENT $record RETURN id`,
343
+ {
344
+ record: {
345
+ name: String(s.name).slice(0, 100),
346
+ description: content,
347
+ content,
348
+ steps: s.steps.map((st: string) => String(st).slice(0, 200)),
349
+ trigger_context: String(s.trigger_context ?? "").slice(0, 200),
350
+ tags: ["auto-extracted"],
351
+ session_id: sessionId,
352
+ ...(emb ? { embedding: emb } : {}),
353
+ },
328
354
  },
329
- },
330
- ).catch(e => swallow.warn("daemon:createSkill", e));
355
+ );
356
+ const skillId = rows[0]?.id ? String(rows[0].id) : null;
357
+ if (skillId) {
358
+ // skill_from_task: skill → task
359
+ if (taskId) {
360
+ await store.relate(skillId, "skill_from_task", taskId)
361
+ .catch(e => swallow.warn("daemon:skill:skill_from_task", e));
362
+ }
363
+ // skill_uses_concept: skill → concept
364
+ await upsertAndLinkConcepts(skillId, "skill_uses_concept", content, store, embeddings, "daemon:skill:concepts");
365
+ }
366
+ } catch (e) {
367
+ swallow.warn("daemon:createSkill", e);
368
+ }
331
369
  })());
332
370
  }
333
371
  }
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/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
@@ -684,15 +684,25 @@ 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 },
690
692
  );
691
693
  if (rows.length > 0) {
692
694
  const id = String(rows[0].id);
693
- await this.queryExec(
694
- `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
695
- );
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
+ }
696
706
  return id;
697
707
  }
698
708
  const emb = embedding?.length ? embedding : undefined;