kongbrain 0.3.5 → 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/README.md +0 -10
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/acan.ts +4 -2
- package/src/causal.ts +17 -12
- package/src/cognitive-bootstrap.ts +159 -0
- package/src/cognitive-check.ts +16 -8
- package/src/index.ts +8 -0
- package/src/memory-daemon.ts +5 -2
- package/src/skills.ts +5 -2
- package/src/surreal.ts +34 -45
- package/src/tools/introspect.ts +2 -1
package/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
1
|
# KongBrain
|
|
4
2
|
|
|
5
|
-

|
|
6
|
-
|
|
7
3
|
[](https://www.npmjs.com/package/kongbrain)
|
|
8
4
|
[](https://clawhub.ai/packages/kongbrain)
|
|
9
5
|
[](https://github.com/42U/kongbrain)
|
|
@@ -21,8 +17,6 @@ Your assistant stops forgetting. Then it starts getting better.
|
|
|
21
17
|
|
|
22
18
|
[Quick Start](#quick-start) | [Architecture](#architecture) | [How It Works](#how-it-works) | [Tools](#tools) | [Development](#development)
|
|
23
19
|
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
20
|
---
|
|
27
21
|
|
|
28
22
|
## What Changes
|
|
@@ -397,8 +391,4 @@ The lobster doesn't accept contributions. The ape does.
|
|
|
397
391
|
|
|
398
392
|
---
|
|
399
393
|
|
|
400
|
-
<div align="center">
|
|
401
|
-
|
|
402
394
|
MIT License | Built by [42U](https://github.com/42U)
|
|
403
|
-
|
|
404
|
-
</div>
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kongbrain
|
|
3
3
|
description: Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.8
|
|
5
5
|
homepage: https://github.com/42U/kongbrain
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|
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
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
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
|
+
}
|
package/src/cognitive-check.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
224
|
-
{
|
|
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
|
|
322
|
-
{
|
|
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
|
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
|
|
package/src/memory-daemon.ts
CHANGED
|
@@ -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
|
|
178
|
-
{
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
451
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
|
521
|
-
{
|
|
518
|
+
`UPDATE ${sessionId} SET ended_at = time::now(), summary = $summary`,
|
|
519
|
+
{ summary },
|
|
522
520
|
);
|
|
523
521
|
} else {
|
|
524
|
-
await this.queryExec(`UPDATE
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
772
|
-
{
|
|
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> = {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1227
|
-
{
|
|
1215
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1216
|
+
{ imp: dupe.importance },
|
|
1228
1217
|
);
|
|
1229
|
-
await this.queryExec(`DELETE
|
|
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
|
|
1256
|
-
{
|
|
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
|
|
1287
|
-
{
|
|
1275
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1276
|
+
{ imp: dupe.importance },
|
|
1288
1277
|
);
|
|
1289
|
-
await this.queryExec(`DELETE
|
|
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
|
|
1391
|
-
{
|
|
1379
|
+
`UPDATE ${checkpointId} SET status = "complete", memory_id = $mid`,
|
|
1380
|
+
{ mid: memoryId },
|
|
1392
1381
|
);
|
|
1393
1382
|
}
|
|
1394
1383
|
|
package/src/tools/introspect.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|