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 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.0
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.0",
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", () => { /* training failure is non-fatal */ });
287
+ worker.on("error", () => { worker.terminate(); });
285
288
  }
286
289
 
287
290
  // ── Startup: auto-train and activate ──
@@ -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
  }
@@ -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));
@@ -117,12 +117,17 @@ export function startMemoryDaemon(
117
117
  try { result[field] = JSON.parse(fieldMatch[1]); } catch { /* skip */ }
118
118
  }
119
119
  }
120
- if (Object.keys(result).length === 0) return;
120
+ const PRIMARY_FIELDS = ["causal", "monologue", "artifacts"];
121
+ if (!PRIMARY_FIELDS.some(f => f in result)) return;
121
122
  }
122
123
  }
123
124
 
124
- const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId, turns);
125
- extractedTurnCount = turns.length;
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.race([
180
- new Promise<void>(resolve => {
181
- const check = setInterval(() => {
182
- if (!processing) { clearInterval(check); resolve(); }
183
- }, 100);
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
  },
@@ -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) {
@@ -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, chmodSync } from "node:fs";
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
- const raw = readFileSync(path, "utf-8");
48
- unlinkSync(path);
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__" in parsed || "constructor" in parsed) return null;
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(path); } catch { /* ignore */ }
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);
@@ -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.max(orch.lastConfig.toolLimit, 25);
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 {
@@ -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, 5)
66
+ .slice(0, Math.min(maxResults, 8))
67
67
  .map((r) => r.id);
68
68
 
69
69
  let neighbors: VectorSearchResult[] = [];