kongbrain 0.3.3 → 0.3.8

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 ADDED
@@ -0,0 +1,24 @@
1
+ # Binary assets (not text-based, rejected by clawhub)
2
+ *.png
3
+ *.jpg
4
+ *.jpeg
5
+ *.gif
6
+
7
+ # Dev tooling
8
+ .claude/
9
+ test/
10
+ vitest.config.ts
11
+ *.test.ts
12
+
13
+ # Build/deps
14
+ node_modules/
15
+ dist/
16
+ package-lock.json
17
+
18
+ # Secrets/runtime
19
+ .env
20
+ .env.*
21
+ .kongbrain-handoff.json
22
+
23
+ # Internal docs
24
+ UPSTREAM-PROPOSALS.md
package/README.md CHANGED
@@ -1,10 +1,7 @@
1
- <div align="center">
2
-
3
1
  # KongBrain
4
2
 
5
- ![KongBrain](KongClaw.png)
6
-
7
3
  [![npm](https://img.shields.io/npm/v/kongbrain?style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/kongbrain)
4
+ [![ClawHub](https://img.shields.io/badge/ClawHub-kongbrain-ff6b35?style=for-the-badge)](https://clawhub.ai/packages/kongbrain)
8
5
  [![GitHub Stars](https://img.shields.io/github/stars/42U/kongbrain?style=for-the-badge&logo=github&color=gold)](https://github.com/42U/kongbrain)
9
6
  [![License: MIT](https://img.shields.io/github/license/42U/kongbrain?style=for-the-badge&logo=opensourceinitiative&color=blue)](https://opensource.org/licenses/MIT)
10
7
  [![Node.js](https://img.shields.io/badge/Node.js-20+-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org)
@@ -20,8 +17,6 @@ Your assistant stops forgetting. Then it starts getting better.
20
17
 
21
18
  [Quick Start](#quick-start) | [Architecture](#architecture) | [How It Works](#how-it-works) | [Tools](#tools) | [Development](#development)
22
19
 
23
- </div>
24
-
25
20
  ---
26
21
 
27
22
  ## What Changes
@@ -47,29 +42,46 @@ npm install -g openclaw
47
42
 
48
43
  ### 2. Start SurrealDB
49
44
 
50
- Pick one:
45
+ Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
51
46
 
52
47
  ```bash
53
- # Native install
48
+ # macOS
49
+ brew install surrealdb/tap/surreal
50
+
51
+ # Linux (Debian/Ubuntu)
54
52
  curl -sSf https://install.surrealdb.com | sh
55
53
  export PATH="$HOME/.surrealdb:$PATH"
56
- surreal start --user root --pass root --bind 0.0.0.0:8042 surrealkv:~/.kongbrain/surreal.db
57
54
  ```
58
55
 
56
+ Then start it locally — **change the credentials before use**:
57
+
58
+ ```bash
59
+ surreal start --user youruser --pass yourpass --bind 127.0.0.1:8042 surrealkv:~/.kongbrain/surreal.db
60
+ ```
61
+
62
+ Or with Docker:
63
+
59
64
  ```bash
60
- # Docker
61
- docker run -d --name surrealdb -p 8042:8000 \
65
+ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
62
66
  -v ~/.kongbrain/surreal-data:/data \
63
67
  surrealdb/surrealdb:latest start \
64
- --user root --pass root surrealkv:/data/surreal.db
68
+ --user youruser --pass yourpass surrealkv:/data/surreal.db
65
69
  ```
66
70
 
71
+ > **Security note:** Always bind to `127.0.0.1` (not `0.0.0.0`) unless you need remote access. Never use default credentials in production.
72
+
67
73
  ### 3. Install KongBrain
68
74
 
69
75
  ```bash
76
+ # From ClawHub (recommended)
77
+ openclaw plugins install clawhub:kongbrain
78
+
79
+ # From npm (fallback)
70
80
  openclaw plugins install kongbrain
71
81
  ```
72
82
 
83
+ > **Note:** Bare `openclaw plugins install kongbrain` checks ClawHub first, then falls back to npm. Use the `clawhub:` prefix to install from ClawHub explicitly.
84
+
73
85
  ### 4. Activate
74
86
 
75
87
  Add to your OpenClaw config (`~/.openclaw/openclaw.json`):
@@ -93,7 +105,7 @@ openclaw tui
93
105
 
94
106
  That's it. KongBrain uses whatever LLM provider and model you already have configured in OpenClaw (Anthropic, OpenAI, Google, Ollama, whatever). No separate API keys needed for the brain itself.
95
107
 
96
- The BGE-M3 embedding model (~420MB) downloads automatically on first startup. All database tables and indexes are created automatically on first run. No manual setup required.
108
+ The BGE-M3 embedding model (~420MB) downloads automatically on first startup from [Hugging Face](https://huggingface.co/BAAI/bge-m3). All database tables and indexes are created automatically on first run. No manual setup required.
97
109
 
98
110
  <details>
99
111
  <summary><strong>Configuration Options</strong></summary>
@@ -102,9 +114,9 @@ All options have sensible defaults. Override via plugin config or environment va
102
114
 
103
115
  | Option | Env Var | Default |
104
116
  |--------|---------|---------|
105
- | `surreal.url` | `SURREAL_URL` | `ws://localhost:8042/rpc` |
106
- | `surreal.user` | `SURREAL_USER` | `root` |
107
- | `surreal.pass` | `SURREAL_PASS` | `root` |
117
+ | `surreal.url` | `SURREAL_URL` | `ws://127.0.0.1:8042/rpc` |
118
+ | `surreal.user` | `SURREAL_USER` | (required) |
119
+ | `surreal.pass` | `SURREAL_PASS` | (required) |
108
120
  | `surreal.ns` | `SURREAL_NS` | `kong` |
109
121
  | `surreal.db` | `SURREAL_DB` | `memory` |
110
122
  | `embedding.modelPath` | `KONGBRAIN_EMBEDDING_MODEL` | Auto-downloaded BGE-M3 Q4_K_M |
@@ -123,9 +135,9 @@ Full config example:
123
135
  "kongbrain": {
124
136
  "config": {
125
137
  "surreal": {
126
- "url": "ws://localhost:8042/rpc",
127
- "user": "root",
128
- "pass": "root",
138
+ "url": "ws://127.0.0.1:8042/rpc",
139
+ "user": "youruser",
140
+ "pass": "yourpass",
129
141
  "ns": "kong",
130
142
  "db": "memory"
131
143
  }
@@ -379,8 +391,4 @@ The lobster doesn't accept contributions. The ape does.
379
391
 
380
392
  ---
381
393
 
382
- <div align="center">
383
-
384
394
  MIT License | Built by [42U](https://github.com/42U)
385
-
386
- </div>
package/SKILL.md ADDED
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: kongbrain
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.8
5
+ homepage: https://github.com/42U/kongbrain
6
+ metadata:
7
+ openclaw:
8
+ requires:
9
+ bins:
10
+ - surreal
11
+ env:
12
+ - SURREAL_URL
13
+ - SURREAL_USER
14
+ - SURREAL_PASS
15
+ - SURREAL_NS
16
+ - SURREAL_DB
17
+ primaryEnv: SURREAL_URL
18
+ install:
19
+ - kind: node
20
+ package: kongbrain
21
+ bins: []
22
+ ---
23
+
24
+ # KongBrain
25
+
26
+ Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.
27
+
28
+ ## What it does
29
+
30
+ KongBrain gives your OpenClaw agent persistent, structured memory:
31
+
32
+ - **Session tracking** - records conversations and extracts knowledge automatically
33
+ - **9 memory categories** - knowledge, goals, reflections, handoffs, corrections, preferences, decisions, skills, and causal chains
34
+ - **Vector search** - BGE-M3 embeddings for semantic recall
35
+ - **Graph relationships** - memories linked via SurrealDB graph edges for traversal
36
+ - **Tiered memory** - core memories always loaded, session memories pinned, rest searched on demand
37
+ - **Mid-session extraction** - extracts knowledge during conversation, not just at exit
38
+ - **Crash resilience** - deferred cleanup processes orphaned sessions on next startup
39
+
40
+ ## Requirements
41
+
42
+ - **SurrealDB** - running instance (local or remote)
43
+ - **Node.js** >= 18
44
+
45
+ ## Setup
46
+
47
+ ### Install SurrealDB
48
+
49
+ See the official install guide: https://surrealdb.com/docs/surrealdb/installation
50
+
51
+ Platform packages:
52
+
53
+ ```bash
54
+ # macOS
55
+ brew install surrealdb/tap/surreal
56
+
57
+ # Linux (Debian/Ubuntu)
58
+ curl -sSf https://install.surrealdb.com | sh
59
+
60
+ # Docker
61
+ docker pull surrealdb/surrealdb:latest
62
+ ```
63
+
64
+ ### Start SurrealDB
65
+
66
+ ```bash
67
+ # Local only (recommended) - use strong credentials in production
68
+ surreal start --user youruser --pass yourpass --bind 127.0.0.1:8000 surrealkv:~/.kongbrain/surreal.db
69
+ ```
70
+
71
+ > **Security note:** Always bind to `127.0.0.1` (not `0.0.0.0`) unless you specifically need remote access. Change the default credentials before use.
72
+
73
+ For Docker:
74
+
75
+ ```bash
76
+ docker run -d --name surrealdb -p 127.0.0.1:8000:8000 \
77
+ -v ~/.kongbrain/surreal-data:/data \
78
+ surrealdb/surrealdb:latest start \
79
+ --user youruser --pass yourpass surrealkv:/data/surreal.db
80
+ ```
81
+
82
+ ## Configuration
83
+
84
+ Set environment variables or provide a `.env` file:
85
+
86
+ ```
87
+ SURREAL_URL=ws://127.0.0.1:8000/rpc
88
+ SURREAL_USER=youruser
89
+ SURREAL_PASS=yourpass
90
+ SURREAL_NS=kongbrain
91
+ SURREAL_DB=kongbrain
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ Install as an OpenClaw plugin:
97
+
98
+ ```bash
99
+ openclaw plugins install clawhub:kongbrain
100
+ ```
101
+
102
+ Or via npm:
103
+
104
+ ```bash
105
+ npm install kongbrain
106
+ ```
107
+
108
+ The BGE-M3 embedding model (~420MB) downloads automatically on first startup from Hugging Face (https://huggingface.co/BAAI/bge-m3). All database tables and indexes are created automatically on first run.
109
+
110
+ KongBrain hooks into OpenClaw's plugin lifecycle automatically. Memory extraction runs in the background via a daemon worker thread.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kongbrain",
3
- "version": "0.3.3",
3
+ "version": "0.3.8",
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",
@@ -30,6 +30,14 @@
30
30
  ],
31
31
  "install": {
32
32
  "minHostVersion": ">=2026.3.23"
33
+ },
34
+ "compat": {
35
+ "pluginApi": ">=2026.3.23",
36
+ "minGatewayVersion": "2026.3.23"
37
+ },
38
+ "build": {
39
+ "openclawVersion": "2026.3.23",
40
+ "pluginSdkVersion": "2026.3.23"
33
41
  }
34
42
  },
35
43
  "scripts": {
package/src/acan.ts CHANGED
@@ -13,6 +13,7 @@ import { join } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { Worker } from "node:worker_threads";
15
15
  import type { SurrealStore } from "./surreal.js";
16
+ import { assertRecordId } from "./surreal.js";
16
17
  import { swallow } from "./errors.js";
17
18
 
18
19
  // ── Types ──
@@ -197,9 +198,10 @@ async function fetchTrainingData(store: SurrealStore): Promise<TrainingSample[]>
197
198
  const embeddingMap = new Map<string, number[]>();
198
199
  for (const mid of uniqueMemIds) {
199
200
  try {
201
+ assertRecordId(mid);
202
+ // Direct interpolation safe: assertRecordId validates format above
200
203
  const flat = await store.queryFirst<{ id: string; embedding: number[] }>(
201
- `SELECT id, embedding FROM type::record($mid) WHERE embedding != NONE`,
202
- { mid },
204
+ `SELECT id, embedding FROM ${mid} WHERE embedding != NONE`,
203
205
  );
204
206
  if (flat[0]?.embedding) embeddingMap.set(mid, flat[0].embedding);
205
207
  } catch (e) { swallow("acan:fetchEmb", e); }
package/src/causal.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import type { EmbeddingService } from "./embeddings.js";
13
13
  import type { SurrealStore, VectorSearchResult } from "./surreal.js";
14
14
  import { swallow } from "./errors.js";
15
+ import { assertRecordId } from "./surreal.js";
15
16
 
16
17
  // --- Types ---
17
18
 
@@ -133,27 +134,31 @@ export async function queryCausalContext(
133
134
  ELSE 0 END AS score`;
134
135
 
135
136
  for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
136
- const queries = frontier.flatMap((id) =>
137
- causalEdges.map((edge) =>
137
+ const queries = frontier.flatMap((id) => {
138
+ assertRecordId(id);
139
+ // Direct interpolation safe: assertRecordId validates format above
140
+ return causalEdges.map((edge) =>
138
141
  store.queryFirst<any>(
139
142
  `SELECT id, text, importance, access_count AS accessCount,
140
143
  created_at AS timestamp, category, meta::tb(id) AS table${scoreExpr}
141
- FROM type::record($nid)->${edge}->? LIMIT 3`,
142
- { ...bindings, nid: id },
144
+ FROM ${id}->${edge}->? LIMIT 3`,
145
+ bindings,
143
146
  ).catch(e => { swallow.warn("causal:edge-query", e); return [] as any[]; }),
144
- ),
145
- );
147
+ );
148
+ });
146
149
 
147
- const reverseQueries = frontier.flatMap((id) =>
148
- causalEdges.map((edge) =>
150
+ const reverseQueries = frontier.flatMap((id) => {
151
+ assertRecordId(id);
152
+ // Direct interpolation safe: assertRecordId validates format above
153
+ return causalEdges.map((edge) =>
149
154
  store.queryFirst<any>(
150
155
  `SELECT id, text, importance, access_count AS accessCount,
151
156
  created_at AS timestamp, category, meta::tb(id) AS table${scoreExpr}
152
- FROM type::record($nid)<-${edge}<-? LIMIT 3`,
153
- { ...bindings, nid: id },
157
+ FROM ${id}<-${edge}<-? LIMIT 3`,
158
+ bindings,
154
159
  ).catch(e => { swallow.warn("causal:edge-query", e); return [] as any[]; }),
155
- ),
156
- );
160
+ );
161
+ });
157
162
 
158
163
  const allQueryResults = await Promise.all([...queries, ...reverseQueries]);
159
164
  const nextFrontier: string[] = [];
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Cognitive Bootstrap — teaches the agent HOW to use its own memory system.
3
+ *
4
+ * Seeds two types of knowledge on first run:
5
+ * 1. Tier 0 core memory entries (always loaded every turn) — imperative
6
+ * reflexes the agent follows without thinking.
7
+ * 2. Identity chunks (vector-searchable) — deeper reference material
8
+ * that surfaces via similarity when the agent thinks about memory topics.
9
+ *
10
+ * The identity chunks in identity.ts tell the agent WHAT it is.
11
+ * This module tells the agent HOW to operate effectively.
12
+ */
13
+ import type { SurrealStore } from "./surreal.js";
14
+ import type { EmbeddingService } from "./embeddings.js";
15
+ import { swallow } from "./errors.js";
16
+
17
+ const BOOTSTRAP_SOURCE = "cognitive_bootstrap";
18
+
19
+ // ── Tier 0 Core Memory: imperative reflexes loaded every turn ────────────
20
+
21
+ const CORE_ENTRIES: { text: string; category: string; priority: number }[] = [
22
+ {
23
+ text: `MEMORY REFLEX: After completing a task or learning something new: (1) Save the insight to core_memory if it should persist across ALL sessions, or let the daemon extract it if session-scoped. (2) When saving, write the WHAT, WHY, and WHEN-TO-USE in the text — vague entries are useless on recall. (3) Link to existing knowledge by using concept names the graph already contains. Check with recall first.`,
24
+ category: "operations",
25
+ priority: 95,
26
+ },
27
+ {
28
+ text: `RECALL BEFORE GUESSING: When uncertain about past decisions, user preferences, project history, or your own capabilities, use the recall tool BEFORE answering. Effective queries: use specific technical terms, file paths, error messages, or concept names — not vague questions. Scope to "memories" for decisions/preferences, "concepts" for technical knowledge, "skills" for procedures, "artifacts" for files.`,
29
+ category: "operations",
30
+ priority: 90,
31
+ },
32
+ {
33
+ text: `GRAPH-AWARE SAVING: When you save knowledge (core_memory add, or concepts the daemon extracts), include terms that create graph connectivity. Mention specific file paths (links to artifacts), technical concept names (links to concepts), and session context (links to tasks). Forward traversal: "X caused Y" lets future searches from X find Y. Backward traversal: "Y was caused by X" lets searches from Y find X. Write both directions in your text.`,
34
+ category: "operations",
35
+ priority: 85,
36
+ },
37
+ {
38
+ text: `SELF-TEACHING CYCLE: Your memory improves when you (1) use recall to review what you've learned, (2) notice gaps or stale information, (3) update or add core_memory entries to correct them, (4) use introspect periodically to understand your memory depth. Corrections from the user are the highest-value signal — always save them. The daemon extracts automatically, but YOU control core_memory (Tier 0) directly.`,
39
+ category: "operations",
40
+ priority: 80,
41
+ },
42
+ {
43
+ text: `MEMORY TOOLS: recall = search graph (use: uncertain, need history, checking prior work). core_memory = manage always-loaded directives (use: permanent lessons, rules, identity updates; add/update/deactivate; Tier 0 = every turn, Tier 1 = this session). introspect = inspect DB health and counts (use: status checks, debugging memory). The daemon extracts 9 types automatically from conversation — you don't need to manually save everything.`,
44
+ category: "tools",
45
+ priority: 75,
46
+ },
47
+ ];
48
+
49
+ // ── Identity Chunks: vector-searchable reference material ────────────────
50
+
51
+ const IDENTITY_CHUNKS: { text: string; importance: number }[] = [
52
+ {
53
+ text: `KongBrain's memory daemon runs in the background and extracts 9 knowledge types from your conversations every ~4K tokens: causal chains (cause->effect from debugging), monologue traces (internal reasoning moments), resolved memories (marking issues done), concepts (technical facts worth remembering), corrections (user correcting you — highest signal), preferences (user workflow/style signals), artifacts (files created/modified/read), decisions (choices with rationale), and skills (multi-step procedures that worked). Quality over quantity — the daemon skips weak extractions. You don't need to manually save what the daemon catches, but you should use core_memory for things you want loaded EVERY turn.`,
54
+ importance: 0.85,
55
+ },
56
+ {
57
+ text: `Effective recall queries use specific terms that match how knowledge was stored. Search by: file paths ("/src/auth/login.ts"), error messages ("ECONNREFUSED"), concept names ("rate limiting"), decision descriptions ("chose PostgreSQL over MongoDB"), or skill names ("deploy to staging"). The recall tool does vector similarity search plus graph neighbor expansion — top results pull in related nodes via edges. Scope options: "all" (default), "memories" (decisions, corrections, preferences), "concepts" (extracted technical knowledge), "turns" (past conversation), "artifacts" (files), "skills" (learned procedures). Check what's already in your injected context before calling recall.`,
58
+ importance: 0.85,
59
+ },
60
+ {
61
+ text: `KongBrain's memory lifecycle: During a session, the daemon extracts knowledge incrementally. At session end (or mid-session every ~25K tokens): a handoff note is written (summarizing what happened), skills are extracted from successful multi-step tasks, metacognitive reflections are generated, and causal chains may graduate to skills. At next session start: the wakeup system synthesizes a first-person briefing from the handoff + identity + monologues + depth signals. This means what you save in one session becomes the foundation for the next. The more precisely you save knowledge, the better your future self performs.`,
62
+ importance: 0.80,
63
+ },
64
+ {
65
+ text: `Graph connectivity determines recall quality. When saving to core_memory or when the daemon extracts concepts, the text content determines which edges form. To ensure forward AND backward traversal: mention specific artifact paths (creates artifact_mentions edges), reference concept names already in the graph (creates about_concept/related_to edges), describe cause-effect relationships explicitly (creates caused_by/supports edges), and note what task or session context produced the knowledge (creates derived_from/part_of edges). Reuse existing concept names for maximum graph connectivity — use introspect or recall to discover what names already exist.`,
66
+ importance: 0.80,
67
+ },
68
+ {
69
+ text: `Three persistence mechanisms serve different purposes. Core memory (Tier 0): you control directly via the core_memory tool. Always loaded every turn. Use for: permanent operational rules, learned patterns, identity refinements. Budget-constrained (~8% of context). Core memory (Tier 1): pinned for the current session only. Use for: session-specific context like "working on auth refactor" or "user prefers verbose logging". Identity chunks: hardcoded self-knowledge, vector-searchable but not always loaded — surfaces when relevant. Daemon extraction: automatic, runs on conversation content, writes to memory/concept/skill/artifact tables. You don't control daemon extraction directly, but the quality of your conversation affects what gets extracted.`,
70
+ importance: 0.75,
71
+ },
72
+ ];
73
+
74
+ /**
75
+ * Seed cognitive bootstrap knowledge on first run.
76
+ * Idempotent — checks for existing entries before seeding.
77
+ */
78
+ export async function seedCognitiveBootstrap(
79
+ store: SurrealStore,
80
+ embeddings: EmbeddingService,
81
+ ): Promise<{ identitySeeded: number; coreSeeded: number }> {
82
+ if (!store.isAvailable()) return { identitySeeded: 0, coreSeeded: 0 };
83
+
84
+ let identitySeeded = 0;
85
+ let coreSeeded = 0;
86
+
87
+ // ── Core memory Tier 0 (always loaded, no embeddings needed) ───────────
88
+
89
+ try {
90
+ const rows = await store.queryFirst<{ cnt: number }>(
91
+ `SELECT count() AS cnt FROM core_memory WHERE text CONTAINS 'MEMORY REFLEX' GROUP ALL`,
92
+ );
93
+ const hasBootstrap = (rows[0]?.cnt ?? 0) > 0;
94
+
95
+ if (!hasBootstrap) {
96
+ for (const entry of CORE_ENTRIES) {
97
+ try {
98
+ await store.createCoreMemory(
99
+ entry.text,
100
+ entry.category,
101
+ entry.priority,
102
+ 0, // Tier 0
103
+ );
104
+ coreSeeded++;
105
+ } catch (e) {
106
+ swallow.warn("bootstrap:seedCore", e);
107
+ }
108
+ }
109
+ }
110
+ } catch (e) {
111
+ swallow.warn("bootstrap:checkCore", e);
112
+ }
113
+
114
+ // ── Identity chunks (vector-searchable, requires embeddings) ───────────
115
+
116
+ if (!embeddings.isAvailable()) return { identitySeeded, coreSeeded };
117
+
118
+ try {
119
+ const rows = await store.queryFirst<{ count: number }>(
120
+ `SELECT count() AS count FROM identity_chunk WHERE source = $source GROUP ALL`,
121
+ { source: BOOTSTRAP_SOURCE },
122
+ );
123
+ const count = rows[0]?.count ?? 0;
124
+
125
+ if (count < IDENTITY_CHUNKS.length) {
126
+ // Clear and re-seed (idempotent on content changes)
127
+ if (count > 0) {
128
+ await store.queryExec(
129
+ `DELETE identity_chunk WHERE source = $source`,
130
+ { source: BOOTSTRAP_SOURCE },
131
+ );
132
+ }
133
+
134
+ for (let i = 0; i < IDENTITY_CHUNKS.length; i++) {
135
+ const chunk = IDENTITY_CHUNKS[i];
136
+ try {
137
+ const vec = await embeddings.embed(chunk.text);
138
+ await store.queryExec(`CREATE identity_chunk CONTENT $data`, {
139
+ data: {
140
+ agent_id: "kongbrain",
141
+ source: BOOTSTRAP_SOURCE,
142
+ chunk_index: i,
143
+ text: chunk.text,
144
+ embedding: vec,
145
+ importance: chunk.importance,
146
+ },
147
+ });
148
+ identitySeeded++;
149
+ } catch (e) {
150
+ swallow.warn("bootstrap:seedIdentityChunk", e);
151
+ }
152
+ }
153
+ }
154
+ } catch (e) {
155
+ swallow.warn("bootstrap:checkIdentity", e);
156
+ }
157
+
158
+ return { identitySeeded, coreSeeded };
159
+ }
@@ -11,6 +11,7 @@
11
11
  import type { CompleteFn, SessionState } from "./state.js";
12
12
  import type { SurrealStore } from "./surreal.js";
13
13
  import { swallow } from "./errors.js";
14
+ import { assertRecordId } from "./surreal.js";
14
15
 
15
16
  // --- Types ---
16
17
 
@@ -182,15 +183,17 @@ Return ONLY valid JSON.`,
182
183
  for (const g of correctionGrades) {
183
184
  if (g.learned) {
184
185
  // Agent followed the correction unprompted — decay toward background (floor 3)
186
+ assertRecordId(g.id);
187
+ // Direct interpolation safe: assertRecordId validates format above
185
188
  await store.queryExec(
186
- `UPDATE type::record($gid) SET importance = math::max([3, importance - 2])`,
187
- { gid: g.id },
189
+ `UPDATE ${g.id} SET importance = math::max([3, importance - 2])`,
188
190
  ).catch(e => swallow.warn("cognitive-check:correctionDecay", e));
189
191
  } else {
190
192
  // Correction was relevant but agent ignored it — reinforce (cap 9)
193
+ assertRecordId(g.id);
194
+ // Direct interpolation safe: assertRecordId validates format above
191
195
  await store.queryExec(
192
- `UPDATE type::record($gid) SET importance = math::min([9, importance + 1])`,
193
- { gid: g.id },
196
+ `UPDATE ${g.id} SET importance = math::min([9, importance + 1])`,
194
197
  ).catch(e => swallow.warn("cognitive-check:correctionReinforce", e));
195
198
  }
196
199
  }
@@ -219,9 +222,11 @@ Return ONLY valid JSON.`,
219
222
  // Mid-session resolution — mark addressed memories immediately
220
223
  const resolvedGrades = result.grades.filter(g => g.resolved && g.id.startsWith("memory:"));
221
224
  for (const g of resolvedGrades) {
225
+ assertRecordId(g.id);
226
+ // Direct interpolation safe: assertRecordId validates format above
222
227
  await store.queryExec(
223
- `UPDATE type::record($gid) SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
224
- { gid: g.id, sid: params.sessionId },
228
+ `UPDATE ${g.id} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
229
+ { sid: params.sessionId },
225
230
  ).catch(e => swallow.warn("cognitive-check:resolve", e));
226
231
  }
227
232
  } catch (e) {
@@ -317,9 +322,12 @@ async function applyRetrievalGrades(
317
322
  { id: grade.id, sid: sessionId },
318
323
  );
319
324
  if (row?.[0]?.id) {
325
+ const rid = String(row[0].id);
326
+ assertRecordId(rid);
327
+ // Direct interpolation safe: assertRecordId validates format above
320
328
  await store.queryExec(
321
- `UPDATE type::record($rid) SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
322
- { rid: String(row[0].id), score: grade.score, relevant: grade.relevant, reason: grade.reason },
329
+ `UPDATE ${rid} SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
330
+ { score: grade.score, relevant: grade.relevant, reason: grade.reason },
323
331
  );
324
332
  }
325
333
  // Feed relevance score into the utility cache — drives WMR provenUtility scoring
@@ -129,6 +129,11 @@ async function processOrphanedSession(
129
129
  result = JSON.parse(jsonMatch[0].replace(/,\s*([}\]])/g, "$1"));
130
130
  } catch { result = {}; }
131
131
  }
132
+ // Strip prototype pollution keys from LLM-generated JSON
133
+ const BANNED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
134
+ for (const key of Object.keys(result)) {
135
+ if (BANNED_KEYS.has(key)) delete (result as any)[key];
136
+ }
132
137
 
133
138
  const keys = Object.keys(result);
134
139
  console.warn(`[deferred] parsed ${keys.length} keys: ${keys.join(", ")}`);
@@ -6,7 +6,7 @@
6
6
  * so the next session's wakeup has context even before deferred
7
7
  * extraction runs.
8
8
  */
9
- import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, chmodSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
 
12
12
  const HANDOFF_FILENAME = ".kongbrain-handoff.json";
@@ -29,7 +29,7 @@ export function writeHandoffFileSync(
29
29
  ): void {
30
30
  try {
31
31
  const path = join(workspaceDir, HANDOFF_FILENAME);
32
- writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
32
+ writeFileSync(path, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
33
33
  } catch {
34
34
  // Best-effort — sync exit handler, can't log async
35
35
  }
@@ -46,7 +46,19 @@ export function readAndDeleteHandoffFile(
46
46
  try {
47
47
  const raw = readFileSync(path, "utf-8");
48
48
  unlinkSync(path);
49
- return JSON.parse(raw) as HandoffFileData;
49
+ const parsed = JSON.parse(raw);
50
+ // Runtime validation — reject prototype pollution and malformed data
51
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
52
+ if ("__proto__" in parsed || "constructor" in parsed) return null;
53
+ const data: HandoffFileData = {
54
+ sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId.slice(0, 200) : "",
55
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp.slice(0, 50) : "",
56
+ userTurnCount: typeof parsed.userTurnCount === "number" ? parsed.userTurnCount : 0,
57
+ lastUserText: typeof parsed.lastUserText === "string" ? parsed.lastUserText.slice(0, 500) : "",
58
+ lastAssistantText: typeof parsed.lastAssistantText === "string" ? parsed.lastAssistantText.slice(0, 500) : "",
59
+ unextractedTokens: typeof parsed.unextractedTokens === "number" ? parsed.unextractedTokens : 0,
60
+ };
61
+ return data;
50
62
  } catch {
51
63
  // Corrupted or deleted between check and read
52
64
  try { unlinkSync(path); } catch { /* ignore */ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import { createAfterToolCallHandler } from "./hooks/after-tool-call.js";
23
23
  import { createLlmOutputHandler } from "./hooks/llm-output.js";
24
24
  import { startMemoryDaemon } from "./daemon-manager.js";
25
25
  import { seedIdentity } from "./identity.js";
26
+ import { seedCognitiveBootstrap } from "./cognitive-bootstrap.js";
26
27
  import { synthesizeWakeup } from "./wakeup.js";
27
28
  import { extractSkill } from "./skills.js";
28
29
  import { generateReflection, setReflectionContextWindow } from "./reflection.js";
@@ -418,6 +419,13 @@ export default definePluginEntry({
418
419
  .then(n => { if (n > 0) logger.info(`Seeded ${n} identity chunks`); })
419
420
  .catch(e => swallow.warn("factory:seedIdentity", e));
420
421
 
422
+ seedCognitiveBootstrap(store, embeddings)
423
+ .then(r => {
424
+ if (r.identitySeeded > 0 || r.coreSeeded > 0)
425
+ logger.info(`Cognitive bootstrap: ${r.identitySeeded} identity + ${r.coreSeeded} core`);
426
+ })
427
+ .catch(e => swallow.warn("factory:seedBootstrap", e));
428
+
421
429
  return new KongBrainContextEngine(state);
422
430
  });
423
431
 
@@ -12,6 +12,7 @@ import type { TurnData, PriorExtractions } from "./daemon-types.js";
12
12
  import type { SurrealStore } from "./surreal.js";
13
13
  import type { EmbeddingService } from "./embeddings.js";
14
14
  import { swallow } from "./errors.js";
15
+ import { assertRecordId } from "./surreal.js";
15
16
 
16
17
  // --- Build the extraction prompt ---
17
18
 
@@ -172,10 +173,12 @@ export async function writeExtractionResults(
172
173
  writeOps.push((async () => {
173
174
  for (const memId of result.resolved!.slice(0, 20)) {
174
175
  if (typeof memId !== "string" || !RECORD_ID_RE.test(memId)) continue;
176
+ assertRecordId(memId);
175
177
  counts.resolved++;
178
+ // Direct interpolation safe: assertRecordId validates format above
176
179
  await store.queryExec(
177
- `UPDATE type::record($mid) SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
178
- { mid: memId, sid: sessionId },
180
+ `UPDATE ${memId} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
181
+ { sid: sessionId },
179
182
  ).catch(e => swallow.warn("daemon:resolveMemory", e));
180
183
  }
181
184
  })());
package/src/skills.ts CHANGED
@@ -13,6 +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 { assertRecordId } from "./surreal.js";
16
17
 
17
18
  // --- Types ---
18
19
 
@@ -237,12 +238,14 @@ export async function recordSkillOutcome(
237
238
 
238
239
  try {
239
240
  const field = success ? "success_count" : "failure_count";
241
+ assertRecordId(skillId);
242
+ // Direct interpolation safe: assertRecordId validates format above
240
243
  await store.queryExec(
241
- `UPDATE type::record($sid) SET
244
+ `UPDATE ${skillId} SET
242
245
  ${field} += 1,
243
246
  avg_duration_ms = (avg_duration_ms * (success_count + failure_count - 1) + $dur) / (success_count + failure_count),
244
247
  last_used = time::now()`,
245
- { sid: skillId, dur: durationMs },
248
+ { dur: durationMs },
246
249
  );
247
250
  } catch (e) { swallow("skills:non-critical", e); }
248
251
  }
package/src/surreal.ts CHANGED
@@ -447,10 +447,8 @@ export class SurrealStore {
447
447
  assertRecordId(fromId);
448
448
  assertRecordId(toId);
449
449
  const safeName = edge.replace(/[^a-zA-Z0-9_]/g, "");
450
- await this.queryExec(
451
- `RELATE type::record($from)->${safeName}->type::record($to)`,
452
- { from: fromId, to: toId },
453
- );
450
+ // Direct interpolation safe: assertRecordId validates format above
451
+ await this.queryExec(`RELATE ${fromId}->${safeName}->${toId}`);
454
452
  }
455
453
 
456
454
  // ── 5-Pillar entity operations ─────────────────────────────────────────
@@ -504,12 +502,12 @@ export class SurrealStore {
504
502
  ): Promise<void> {
505
503
  assertRecordId(sessionId);
506
504
  await this.queryExec(
507
- `UPDATE type::record($sid) SET
505
+ `UPDATE ${sessionId} SET
508
506
  turn_count += 1,
509
507
  total_input_tokens += $input,
510
508
  total_output_tokens += $output,
511
509
  last_active = time::now()`,
512
- { sid: sessionId, input: inputTokens, output: outputTokens },
510
+ { input: inputTokens, output: outputTokens },
513
511
  );
514
512
  }
515
513
 
@@ -517,27 +515,25 @@ export class SurrealStore {
517
515
  assertRecordId(sessionId);
518
516
  if (summary) {
519
517
  await this.queryExec(
520
- `UPDATE type::record($sid) SET ended_at = time::now(), summary = $summary`,
521
- { sid: sessionId, summary },
518
+ `UPDATE ${sessionId} SET ended_at = time::now(), summary = $summary`,
519
+ { summary },
522
520
  );
523
521
  } else {
524
- await this.queryExec(`UPDATE type::record($sid) SET ended_at = time::now()`, { sid: sessionId });
522
+ await this.queryExec(`UPDATE ${sessionId} SET ended_at = time::now()`);
525
523
  }
526
524
  }
527
525
 
528
526
  async markSessionActive(sessionId: string): Promise<void> {
529
527
  assertRecordId(sessionId);
530
528
  await this.queryExec(
531
- `UPDATE type::record($sid) SET cleanup_completed = false, last_active = time::now()`,
532
- { sid: sessionId },
529
+ `UPDATE ${sessionId} SET cleanup_completed = false, last_active = time::now()`,
533
530
  );
534
531
  }
535
532
 
536
533
  async markSessionEnded(sessionId: string): Promise<void> {
537
534
  assertRecordId(sessionId);
538
535
  await this.queryExec(
539
- `UPDATE type::record($sid) SET ended_at = time::now(), cleanup_completed = true`,
540
- { sid: sessionId },
536
+ `UPDATE ${sessionId} SET ended_at = time::now(), cleanup_completed = true`,
541
537
  );
542
538
  }
543
539
 
@@ -555,8 +551,7 @@ export class SurrealStore {
555
551
  assertRecordId(sessionId);
556
552
  assertRecordId(taskId);
557
553
  await this.queryExec(
558
- `RELATE type::record($from)->session_task->type::record($to)`,
559
- { from: sessionId, to: taskId },
554
+ `RELATE ${sessionId}->session_task->${taskId}`,
560
555
  );
561
556
  }
562
557
 
@@ -564,8 +559,7 @@ export class SurrealStore {
564
559
  assertRecordId(taskId);
565
560
  assertRecordId(projectId);
566
561
  await this.queryExec(
567
- `RELATE type::record($from)->task_part_of->type::record($to)`,
568
- { from: taskId, to: projectId },
562
+ `RELATE ${taskId}->task_part_of->${projectId}`,
569
563
  );
570
564
  }
571
565
 
@@ -573,8 +567,7 @@ export class SurrealStore {
573
567
  assertRecordId(agentId);
574
568
  assertRecordId(taskId);
575
569
  await this.queryExec(
576
- `RELATE type::record($from)->performed->type::record($to)`,
577
- { from: agentId, to: taskId },
570
+ `RELATE ${agentId}->performed->${taskId}`,
578
571
  );
579
572
  }
580
573
 
@@ -582,8 +575,7 @@ export class SurrealStore {
582
575
  assertRecordId(agentId);
583
576
  assertRecordId(projectId);
584
577
  await this.queryExec(
585
- `RELATE type::record($from)->owns->type::record($to)`,
586
- { from: agentId, to: projectId },
578
+ `RELATE ${agentId}->owns->${projectId}`,
587
579
  );
588
580
  }
589
581
 
@@ -625,7 +617,7 @@ export class SurrealStore {
625
617
  for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
626
618
  const forwardQueries = frontier.flatMap((id) =>
627
619
  forwardEdges.map((edge) =>
628
- this.queryFirst<any>(`${selectFields} FROM type::record($nid)->${edge}->? LIMIT 3`, { ...bindings, nid: id }).catch(
620
+ this.queryFirst<any>(`${selectFields} FROM ${id}->${edge}->? LIMIT 3`, bindings).catch(
629
621
  (e) => {
630
622
  swallow.warn("surreal:graphExpand", e);
631
623
  return [] as Record<string, unknown>[];
@@ -636,7 +628,7 @@ export class SurrealStore {
636
628
 
637
629
  const reverseQueries = frontier.flatMap((id) =>
638
630
  reverseEdges.map((edge) =>
639
- this.queryFirst<any>(`${selectFields} FROM type::record($nid)<-${edge}<-? LIMIT 3`, { ...bindings, nid: id }).catch(
631
+ this.queryFirst<any>(`${selectFields} FROM ${id}<-${edge}<-? LIMIT 3`, bindings).catch(
640
632
  (e) => {
641
633
  swallow.warn("surreal:graphExpand", e);
642
634
  return [] as Record<string, unknown>[];
@@ -687,8 +679,7 @@ export class SurrealStore {
687
679
  try {
688
680
  assertRecordId(id);
689
681
  await this.queryExec(
690
- `UPDATE type::record($rid) SET access_count += 1, last_accessed = time::now()`,
691
- { rid: id },
682
+ `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
692
683
  );
693
684
  } catch (e) {
694
685
  swallow.warn("surreal:bumpAccessCounts", e);
@@ -710,8 +701,7 @@ export class SurrealStore {
710
701
  if (rows.length > 0) {
711
702
  const id = String(rows[0].id);
712
703
  await this.queryExec(
713
- `UPDATE type::record($cid) SET access_count += 1, last_accessed = time::now()`,
714
- { cid: id },
704
+ `UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
715
705
  );
716
706
  return id;
717
707
  }
@@ -768,8 +758,8 @@ export class SurrealStore {
768
758
  const existing = dupes[0];
769
759
  const newImp = Math.max(existing.importance ?? 0, importance);
770
760
  await this.queryExec(
771
- `UPDATE type::record($eid) SET access_count += 1, importance = $imp, last_accessed = time::now()`,
772
- { eid: String(existing.id), imp: newImp },
761
+ `UPDATE ${String(existing.id)} SET access_count += 1, importance = $imp, last_accessed = time::now()`,
762
+ { imp: newImp },
773
763
  );
774
764
  return String(existing.id);
775
765
  }
@@ -844,7 +834,7 @@ export class SurrealStore {
844
834
  assertRecordId(id);
845
835
  const ALLOWED_FIELDS = new Set(["text", "category", "priority", "tier", "active"]);
846
836
  const sets: string[] = [];
847
- const bindings: Record<string, unknown> = { _rid: id };
837
+ const bindings: Record<string, unknown> = {};
848
838
  for (const [key, val] of Object.entries(fields)) {
849
839
  if (val !== undefined && ALLOWED_FIELDS.has(key)) {
850
840
  sets.push(`${key} = $${key}`);
@@ -854,7 +844,7 @@ export class SurrealStore {
854
844
  if (sets.length === 0) return false;
855
845
  sets.push("updated_at = time::now()");
856
846
  const rows = await this.queryFirst<{ id: string }>(
857
- `UPDATE type::record($_rid) SET ${sets.join(", ")} RETURN id`,
847
+ `UPDATE ${id} SET ${sets.join(", ")} RETURN id`,
858
848
  bindings,
859
849
  );
860
850
  return rows.length > 0;
@@ -863,8 +853,7 @@ export class SurrealStore {
863
853
  async deleteCoreMemory(id: string): Promise<void> {
864
854
  assertRecordId(id);
865
855
  await this.queryExec(
866
- `UPDATE type::record($rid) SET active = false, updated_at = time::now()`,
867
- { rid: id },
856
+ `UPDATE ${id} SET active = false, updated_at = time::now()`,
868
857
  );
869
858
  }
870
859
 
@@ -1013,9 +1002,9 @@ export class SurrealStore {
1013
1002
 
1014
1003
  async resolveMemory(memoryId: string): Promise<boolean> {
1015
1004
  try {
1005
+ assertRecordId(memoryId);
1016
1006
  await this.queryFirst(
1017
- `UPDATE type::record($id) SET status = 'resolved', resolved_at = time::now()`,
1018
- { id: memoryId },
1007
+ `UPDATE ${memoryId} SET status = 'resolved', resolved_at = time::now()`,
1019
1008
  );
1020
1009
  return true;
1021
1010
  } catch (e) {
@@ -1223,10 +1212,10 @@ export class SurrealStore {
1223
1212
  assertRecordId(String(keep));
1224
1213
  assertRecordId(String(drop));
1225
1214
  await this.queryExec(
1226
- `UPDATE type::record($kid) SET access_count += 1, importance = math::max([importance, $imp])`,
1227
- { kid: String(keep), imp: dupe.importance },
1215
+ `UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
1216
+ { imp: dupe.importance },
1228
1217
  );
1229
- await this.queryExec(`DELETE type::record($did)`, { did: String(drop) });
1218
+ await this.queryExec(`DELETE ${String(drop)}`);
1230
1219
  seen.add(String(drop));
1231
1220
  merged++;
1232
1221
  }
@@ -1252,8 +1241,8 @@ export class SurrealStore {
1252
1241
  const emb = await embedFn(mem.text);
1253
1242
  if (!emb) continue;
1254
1243
  await this.queryExec(
1255
- `UPDATE type::record($mid) SET embedding = $emb`,
1256
- { mid: String(mem.id), emb },
1244
+ `UPDATE ${String(mem.id)} SET embedding = $emb`,
1245
+ { emb },
1257
1246
  );
1258
1247
 
1259
1248
  const dupes = await this.queryFirst<{
@@ -1283,10 +1272,10 @@ export class SurrealStore {
1283
1272
  assertRecordId(String(keep));
1284
1273
  assertRecordId(String(drop));
1285
1274
  await this.queryExec(
1286
- `UPDATE type::record($kid) SET access_count += 1, importance = math::max([importance, $imp])`,
1287
- { kid: String(keep), imp: dupe.importance },
1275
+ `UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
1276
+ { imp: dupe.importance },
1288
1277
  );
1289
- await this.queryExec(`DELETE type::record($did)`, { did: String(drop) });
1278
+ await this.queryExec(`DELETE ${String(drop)}`);
1290
1279
  seen.add(String(drop));
1291
1280
  merged++;
1292
1281
  }
@@ -1387,8 +1376,8 @@ export class SurrealStore {
1387
1376
  ): Promise<void> {
1388
1377
  assertRecordId(checkpointId);
1389
1378
  await this.queryExec(
1390
- `UPDATE type::record($cpid) SET status = "complete", memory_id = $mid`,
1391
- { cpid: checkpointId, mid: memoryId },
1379
+ `UPDATE ${checkpointId} SET status = "complete", memory_id = $mid`,
1380
+ { mid: memoryId },
1392
1381
  );
1393
1382
  }
1394
1383
 
@@ -207,7 +207,8 @@ async function verifyAction(store: any, recordId?: string) {
207
207
  return { content: [{ type: "text" as const, text: `Error: invalid record ID "${recordId}".` }], details: null };
208
208
  }
209
209
 
210
- const rows = await store.queryFirst(`SELECT * FROM type::record($rid)`, { rid: recordId });
210
+ // Direct interpolation safe: assertRecordId validates format above
211
+ const rows = await store.queryFirst(`SELECT * FROM ${recordId}`);
211
212
  if (rows.length === 0) {
212
213
  return { content: [{ type: "text" as const, text: `Record not found: ${recordId}` }], details: { exists: false } };
213
214
  }