kongbrain 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/acan.ts +4 -1
- package/src/cognitive-check.ts +2 -2
- package/src/concept-extract.ts +1 -1
- package/src/daemon-manager.ts +17 -11
- package/src/deferred-cleanup.ts +3 -7
- package/src/handoff-file.ts +12 -5
- package/src/hooks/after-tool-call.ts +3 -3
- package/src/index.ts +8 -2
- package/src/orchestrator.ts +1 -1
- package/src/reflection.ts +1 -0
- package/src/soul.ts +1 -1
- package/src/surreal.ts +4 -0
- package/src/tools/recall.ts +1 -1
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.4.
|
|
4
|
+
version: 0.4.1
|
|
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.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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
|
@@ -273,15 +273,18 @@ function trainInBackground(
|
|
|
273
273
|
workerData: { samples, cfg, warmStart: warmStart ?? null, EMBED_DIM, ATTN_DIM, FEATURE_COUNT },
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
+
worker.unref();
|
|
277
|
+
|
|
276
278
|
worker.on("message", (msg: any) => {
|
|
277
279
|
try {
|
|
278
280
|
saveWeights(msg.weights, weightsPath);
|
|
279
281
|
_weights = msg.weights;
|
|
280
282
|
_active = true;
|
|
281
283
|
} catch { /* non-fatal */ }
|
|
284
|
+
worker.terminate();
|
|
282
285
|
});
|
|
283
286
|
|
|
284
|
-
worker.on("error", () => {
|
|
287
|
+
worker.on("error", () => { worker.terminate(); });
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
// ── Startup: auto-train and activate ──
|
package/src/cognitive-check.ts
CHANGED
|
@@ -186,14 +186,14 @@ Return ONLY valid JSON.`,
|
|
|
186
186
|
assertRecordId(g.id);
|
|
187
187
|
// Direct interpolation safe: assertRecordId validates format above
|
|
188
188
|
await store.queryExec(
|
|
189
|
-
`UPDATE ${g.id} SET importance = math::max([3, importance - 2])`,
|
|
189
|
+
`UPDATE ${g.id} SET importance = math::max([3, (importance ?? 5) - 2])`,
|
|
190
190
|
).catch(e => swallow.warn("cognitive-check:correctionDecay", e));
|
|
191
191
|
} else {
|
|
192
192
|
// Correction was relevant but agent ignored it — reinforce (cap 9)
|
|
193
193
|
assertRecordId(g.id);
|
|
194
194
|
// Direct interpolation safe: assertRecordId validates format above
|
|
195
195
|
await store.queryExec(
|
|
196
|
-
`UPDATE ${g.id} SET importance = math::min([9, importance + 1])`,
|
|
196
|
+
`UPDATE ${g.id} SET importance = math::min([9, (importance ?? 5) + 1])`,
|
|
197
197
|
).catch(e => swallow.warn("cognitive-check:correctionReinforce", e));
|
|
198
198
|
}
|
|
199
199
|
}
|
package/src/concept-extract.ts
CHANGED
|
@@ -57,7 +57,7 @@ export async function upsertAndLinkConcepts(
|
|
|
57
57
|
if (embeddings.isAvailable()) {
|
|
58
58
|
try { embedding = await embeddings.embed(name); } catch { /* ok */ }
|
|
59
59
|
}
|
|
60
|
-
const conceptId = await store.upsertConcept(name, embedding);
|
|
60
|
+
const conceptId = await store.upsertConcept(name, embedding, logTag);
|
|
61
61
|
if (conceptId) {
|
|
62
62
|
await store.relate(sourceId, edgeName, conceptId)
|
|
63
63
|
.catch(e => swallow(`${logTag}:relate`, e));
|
package/src/daemon-manager.ts
CHANGED
|
@@ -117,12 +117,17 @@ export function startMemoryDaemon(
|
|
|
117
117
|
try { result[field] = JSON.parse(fieldMatch[1]); } catch { /* skip */ }
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
const PRIMARY_FIELDS = ["causal", "monologue", "artifacts"];
|
|
121
|
+
if (!PRIMARY_FIELDS.some(f => f in result)) return;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
try {
|
|
126
|
+
const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId, turns);
|
|
127
|
+
extractedTurnCount = turns.length;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
swallow.warn("daemon:writeExtractionResults", e);
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
// Pending batch (only keep latest — newer batch supersedes older)
|
|
@@ -158,6 +163,9 @@ export function startMemoryDaemon(
|
|
|
158
163
|
return {
|
|
159
164
|
sendTurnBatch(turns, thinking, retrievedMemories, priorExtractions) {
|
|
160
165
|
if (shuttingDown) return;
|
|
166
|
+
if (pendingBatch) {
|
|
167
|
+
swallow.warn("daemon:batchOverwrite", new Error(`Overwriting pending batch (${pendingBatch.turns.length} turns) with new batch (${turns.length} turns)`));
|
|
168
|
+
}
|
|
161
169
|
pendingBatch = { turns, thinking, retrievedMemories, priorExtractions };
|
|
162
170
|
// Fire-and-forget
|
|
163
171
|
processPending().catch(e => swallow.warn("daemon:sendBatch", e));
|
|
@@ -176,14 +184,12 @@ export function startMemoryDaemon(
|
|
|
176
184
|
shuttingDown = true;
|
|
177
185
|
// Wait for current extraction to finish
|
|
178
186
|
if (processing) {
|
|
179
|
-
await Promise
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
new Promise<void>(resolve => setTimeout(resolve, timeoutMs)),
|
|
186
|
-
]);
|
|
187
|
+
await new Promise<void>(resolve => {
|
|
188
|
+
const check = setInterval(() => {
|
|
189
|
+
if (!processing) { clearInterval(check); clearTimeout(timeout); resolve(); }
|
|
190
|
+
}, 100);
|
|
191
|
+
const timeout = setTimeout(() => { clearInterval(check); resolve(); }, timeoutMs);
|
|
192
|
+
});
|
|
187
193
|
}
|
|
188
194
|
// Shared store/embeddings — don't dispose (owned by global state)
|
|
189
195
|
},
|
package/src/deferred-cleanup.ts
CHANGED
|
@@ -51,18 +51,14 @@ async function runDeferredCleanupInner(
|
|
|
51
51
|
const orphaned = await store.getOrphanedSessions(10).catch(() => []);
|
|
52
52
|
if (orphaned.length === 0) return 0;
|
|
53
53
|
|
|
54
|
-
// Immediately claim all orphaned sessions so no concurrent run can pick them up
|
|
55
|
-
await Promise.all(
|
|
56
|
-
orphaned.map(s =>
|
|
57
|
-
store.markSessionEnded(s.id).catch(e => swallow("deferred:claim", e))
|
|
58
|
-
)
|
|
59
|
-
);
|
|
60
|
-
|
|
61
54
|
let processed = 0;
|
|
62
55
|
|
|
63
56
|
const cleanup = async () => {
|
|
64
57
|
for (const session of orphaned) {
|
|
65
58
|
try {
|
|
59
|
+
// Claim each session just before processing so unclaimed ones remain
|
|
60
|
+
// available to the next run if we time out partway through
|
|
61
|
+
await store.markSessionEnded(session.id).catch(e => swallow("deferred:claim", e));
|
|
66
62
|
await processOrphanedSession(session.id, store, embeddings, complete);
|
|
67
63
|
processed++;
|
|
68
64
|
} catch (e) {
|
package/src/handoff-file.ts
CHANGED
|
@@ -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,
|
|
9
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, renameSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
const HANDOFF_FILENAME = ".kongbrain-handoff.json";
|
|
@@ -42,14 +42,21 @@ export function readAndDeleteHandoffFile(
|
|
|
42
42
|
workspaceDir: string,
|
|
43
43
|
): HandoffFileData | null {
|
|
44
44
|
const path = join(workspaceDir, HANDOFF_FILENAME);
|
|
45
|
+
const processingPath = path + ".processing";
|
|
46
|
+
// Also clean up stale .processing files from prior crashes
|
|
47
|
+
if (existsSync(processingPath) && !existsSync(path)) {
|
|
48
|
+
try { unlinkSync(processingPath); } catch { /* ignore */ }
|
|
49
|
+
}
|
|
45
50
|
if (!existsSync(path)) return null;
|
|
46
51
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
// Atomic rename first so a crash between read and delete can't re-process
|
|
53
|
+
renameSync(path, processingPath);
|
|
54
|
+
const raw = readFileSync(processingPath, "utf-8");
|
|
55
|
+
unlinkSync(processingPath);
|
|
49
56
|
const parsed = JSON.parse(raw);
|
|
50
57
|
// Runtime validation — reject prototype pollution and malformed data
|
|
51
58
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
52
|
-
if ("__proto__"
|
|
59
|
+
if (Object.hasOwn(parsed, "__proto__") || Object.hasOwn(parsed, "constructor")) return null;
|
|
53
60
|
const data: HandoffFileData = {
|
|
54
61
|
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId.slice(0, 200) : "",
|
|
55
62
|
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp.slice(0, 50) : "",
|
|
@@ -61,7 +68,7 @@ export function readAndDeleteHandoffFile(
|
|
|
61
68
|
return data;
|
|
62
69
|
} catch {
|
|
63
70
|
// Corrupted or deleted between check and read
|
|
64
|
-
try { unlinkSync(
|
|
71
|
+
try { unlinkSync(processingPath); } catch { /* ignore */ }
|
|
65
72
|
return null;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -54,7 +54,7 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
54
54
|
});
|
|
55
55
|
if (assistantTurnId) session.lastAssistantTurnId = assistantTurnId;
|
|
56
56
|
} catch (e) {
|
|
57
|
-
swallow("hook:afterToolCall:eagerAssistantTurn", e);
|
|
57
|
+
swallow.warn("hook:afterToolCall:eagerAssistantTurn", e);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
if (session.lastAssistantTurnId) {
|
|
@@ -63,12 +63,12 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
} catch (e) {
|
|
66
|
-
swallow("hook:afterToolCall:store", e);
|
|
66
|
+
swallow.warn("hook:afterToolCall:store", e);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Auto-track file artifacts from write/edit tools
|
|
70
70
|
if (!isError) {
|
|
71
|
-
trackArtifact(event.toolName, event.params, session.taskId, session.projectId, state)
|
|
71
|
+
await trackArtifact(event.toolName, event.params, session.taskId, session.projectId, state)
|
|
72
72
|
.catch(e => swallow.warn("hook:afterToolCall:artifact", e));
|
|
73
73
|
}
|
|
74
74
|
|
package/src/index.ts
CHANGED
|
@@ -147,11 +147,18 @@ async function runSessionCleanup(
|
|
|
147
147
|
new Promise(resolve => setTimeout(resolve, 150_000)),
|
|
148
148
|
]);
|
|
149
149
|
|
|
150
|
+
// Await the graduation promise once and reuse the result below
|
|
151
|
+
let gradResult: Awaited<typeof graduationPromise> = null;
|
|
152
|
+
try {
|
|
153
|
+
gradResult = await graduationPromise;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
swallow.warn("cleanup:graduationAwait", e);
|
|
156
|
+
}
|
|
157
|
+
|
|
150
158
|
// If soul graduation just happened, persist a graduation event so the next
|
|
151
159
|
// session can celebrate with the user. We also fire a system event for
|
|
152
160
|
// immediate visibility if the session is still active.
|
|
153
161
|
try {
|
|
154
|
-
const gradResult = await graduationPromise;
|
|
155
162
|
if (gradResult?.graduated && gradResult.soul) {
|
|
156
163
|
// Check if this is a NEW graduation (not a pre-existing soul)
|
|
157
164
|
const isNewGraduation = gradResult.report.stage === "ready";
|
|
@@ -189,7 +196,6 @@ async function runSessionCleanup(
|
|
|
189
196
|
// Soul evolution — if soul already exists, check if it should be revised
|
|
190
197
|
// based on new experience (runs every 10 sessions after last revision)
|
|
191
198
|
try {
|
|
192
|
-
const gradResult = await graduationPromise;
|
|
193
199
|
if (gradResult?.graduated && gradResult.report.stage !== "ready") {
|
|
194
200
|
// Pre-existing soul — check for evolution
|
|
195
201
|
await evolveSoul(s, complete);
|
package/src/orchestrator.ts
CHANGED
|
@@ -192,7 +192,7 @@ export async function preflight(
|
|
|
192
192
|
|
|
193
193
|
// Non-first-turn short inputs → continuation
|
|
194
194
|
if (orch.turnIndex > 1 && input.length < 20 && !input.includes("?")) {
|
|
195
|
-
const inheritedLimit = Math.
|
|
195
|
+
const inheritedLimit = Math.min(orch.lastConfig.toolLimit, 25);
|
|
196
196
|
const config: AdaptiveConfig = {
|
|
197
197
|
...orch.lastConfig, toolLimit: inheritedLimit, skipRetrieval: true,
|
|
198
198
|
vectorSearchLimits: { turn: 0, identity: 0, concept: 0, memory: 0, artifact: 0 },
|
package/src/reflection.ts
CHANGED
|
@@ -207,6 +207,7 @@ export async function generateReflection(
|
|
|
207
207
|
{ record },
|
|
208
208
|
);
|
|
209
209
|
const reflectionId = String(rows[0]?.id ?? "");
|
|
210
|
+
store.clearReflectionCache();
|
|
210
211
|
|
|
211
212
|
if (reflectionId && surrealSessionId) {
|
|
212
213
|
await store.relate(reflectionId, "reflects_on", surrealSessionId).catch(e => swallow.warn("reflection:relate", e));
|
package/src/soul.ts
CHANGED
|
@@ -191,7 +191,7 @@ export async function getQualitySignals(store: SurrealStore): Promise<QualitySig
|
|
|
191
191
|
const totalSuccess = Number(skillRow?.totalSuccess ?? 0);
|
|
192
192
|
const totalFailure = Number(skillRow?.totalFailure ?? 0);
|
|
193
193
|
const skillTotal = totalSuccess + totalFailure;
|
|
194
|
-
const skillSuccessRate = skillTotal > 0 ? totalSuccess / skillTotal : 0;
|
|
194
|
+
const skillSuccessRate = skillTotal > 0 && Number.isFinite(skillTotal) ? totalSuccess / skillTotal : 0;
|
|
195
195
|
|
|
196
196
|
const critCount = Number(critRow?.count ?? 0);
|
|
197
197
|
const reflCount = Number(totalRow?.count ?? 0);
|
package/src/surreal.ts
CHANGED
|
@@ -1408,6 +1408,10 @@ export class SurrealStore {
|
|
|
1408
1408
|
|
|
1409
1409
|
private _reflectionSessions: Set<string> | null = null;
|
|
1410
1410
|
|
|
1411
|
+
clearReflectionCache(): void {
|
|
1412
|
+
this._reflectionSessions = null;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1411
1415
|
async getReflectionSessionIds(): Promise<Set<string>> {
|
|
1412
1416
|
if (this._reflectionSessions) return this._reflectionSessions;
|
|
1413
1417
|
try {
|
package/src/tools/recall.ts
CHANGED
|
@@ -63,7 +63,7 @@ export function createRecallToolDef(state: GlobalPluginState, session: SessionSt
|
|
|
63
63
|
|
|
64
64
|
const topIds = results
|
|
65
65
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
66
|
-
.slice(0,
|
|
66
|
+
.slice(0, Math.min(maxResults, 8))
|
|
67
67
|
.map((r) => r.id);
|
|
68
68
|
|
|
69
69
|
let neighbors: VectorSearchResult[] = [];
|