kongbrain 0.3.16 → 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.3.16
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.3.16",
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));
@@ -48,6 +48,7 @@ import { runDeferredCleanup } from "./deferred-cleanup.js";
48
48
  import { extractSkill } from "./skills.js";
49
49
  import { generateReflection } from "./reflection.js";
50
50
  import { graduateCausalToSkills } from "./skills.js";
51
+ import { attemptGraduation, evolveSoul, checkStageTransition } from "./soul.js";
51
52
  import { swallow } from "./errors.js";
52
53
 
53
54
  export class KongBrainContextEngine implements ContextEngine {
@@ -152,7 +153,7 @@ export class KongBrainContextEngine implements ContextEngine {
152
153
 
153
154
  const contextWindow = params.tokenBudget ?? 200000;
154
155
 
155
- const { messages, stats } = await graphTransformContext({
156
+ const { messages, stats, systemPromptSection } = await graphTransformContext({
156
157
  messages: params.messages,
157
158
  session,
158
159
  store,
@@ -160,9 +161,24 @@ export class KongBrainContextEngine implements ContextEngine {
160
161
  contextWindow,
161
162
  });
162
163
 
164
+ // Stash retrieval summary for planning gate (claw-code pattern: pre-compute and show)
165
+ session.lastRetrievalSummary = stats.graphNodes > 0
166
+ ? `${stats.graphNodes} context items + ${stats.neighborNodes} neighbors injected (${stats.mode} mode)`
167
+ : "no graph context retrieved this turn";
168
+
163
169
  // Build system prompt additions
164
170
  const additions: string[] = [];
165
171
 
172
+ // Static content for API prefix caching (claw-code: prompt.rs static/dynamic split)
173
+ if (systemPromptSection) additions.push(systemPromptSection);
174
+
175
+ // Compaction summary (claw-code: compact.rs structured signals — inject once after compaction)
176
+ const compactionSummary = (session as any)._compactionSummary as string | undefined;
177
+ if (compactionSummary) {
178
+ additions.push("[POST-COMPACTION CONTEXT]\n" + compactionSummary);
179
+ delete (session as any)._compactionSummary;
180
+ }
181
+
166
182
  // Wakeup briefing (synthesized at session start, may still be in-flight)
167
183
  const wakeupPromise = (session as any)._wakeupPromise as Promise<string | null> | undefined;
168
184
  if (wakeupPromise) {
@@ -318,11 +334,70 @@ export class KongBrainContextEngine implements ContextEngine {
318
334
  tokenBudget?: number;
319
335
  force?: boolean;
320
336
  }): Promise<CompactResult> {
321
- // Graph retrieval IS the compaction — ownsCompaction: true
337
+ // Graph retrieval IS the compaction — ownsCompaction: true.
338
+ // But we extract structured signals so the model doesn't lose context
339
+ // about pending work and key files after old messages are dropped.
340
+ // (claw-code pattern: compact.rs extracts pending work, key files, continuation directive)
341
+ const sessionKey = params.sessionKey ?? params.sessionId;
342
+ const session = this.state.getSession(sessionKey);
343
+ if (session) {
344
+ session.injectedSections.clear();
345
+ }
346
+
347
+ // Extract structured compaction signals from stored turns
348
+ let summary: string | undefined;
349
+ try {
350
+ const { store } = this.state;
351
+ if (store.isAvailable()) {
352
+ const turns = await store.getSessionTurnsRich(params.sessionId, 30);
353
+ if (turns.length > 0) {
354
+ const fullText = turns.map(t => t.text).join("\n");
355
+
356
+ // Pending work detection (claw-code: compact.rs:235-254)
357
+ const pendingRe = /\b(todo|next|pending|follow up|remaining|unfinished|still need)\b[^.\n]{0,100}/gi;
358
+ const pendingMatches = [...fullText.matchAll(pendingRe)]
359
+ .map(m => m[0].trim().slice(0, 160))
360
+ .slice(0, 5);
361
+
362
+ // Key file extraction (claw-code: compact.rs:256-269)
363
+ const filePaths = [...new Set(
364
+ (fullText.match(/[\w\-/.]+\.\w{1,5}/g) ?? [])
365
+ .filter(p => /\.(ts|js|py|rs|go|md|json|yaml|toml|tsx|jsx)$/.test(p))
366
+ )].slice(0, 10);
367
+
368
+ // Tool names used (claw-code: compact.rs:127-137)
369
+ const toolNames = [...new Set(
370
+ turns.filter(t => t.tool_name).map(t => t.tool_name!)
371
+ )];
372
+
373
+ // Current work inference (claw-code: compact.rs:272-279)
374
+ const lastText = turns.filter(t => t.text.length > 10).at(-1)?.text.slice(0, 200) ?? "";
375
+
376
+ const parts: string[] = [];
377
+ if (pendingMatches.length > 0) parts.push(`PENDING: ${pendingMatches.join("; ")}`);
378
+ if (filePaths.length > 0) parts.push(`FILES: ${filePaths.join(", ")}`);
379
+ if (toolNames.length > 0) parts.push(`TOOLS USED: ${toolNames.join(", ")}`);
380
+ if (lastText) parts.push(`LAST: ${lastText}`);
381
+ parts.push("Resume directly — do not recap what was happening.");
382
+
383
+ if (parts.length > 1) {
384
+ summary = parts.join("\n");
385
+ // Stash for next assemble() to inject
386
+ if (session) {
387
+ (session as any)._compactionSummary = summary;
388
+ }
389
+ }
390
+ }
391
+ }
392
+ } catch { /* non-critical */ }
393
+
322
394
  return {
323
395
  ok: true,
324
- compacted: false,
325
- reason: "Graph retrieval handles context selection; no LLM-based compaction needed.",
396
+ compacted: !!summary,
397
+ reason: summary
398
+ ? "Extracted structured signals for continuation."
399
+ : "Graph retrieval handles context selection; no LLM-based compaction needed.",
400
+ result: summary ? { summary, tokensBefore: 0 } : undefined,
326
401
  };
327
402
  }
328
403
 
@@ -494,6 +569,55 @@ export class KongBrainContextEngine implements ContextEngine {
494
569
  })().catch(e => swallow.warn("midCleanup:handoff", e)),
495
570
  );
496
571
 
572
+ // Soul graduation + stage transition — run mid-session so marathon
573
+ // sessions don't miss milestones that would normally fire at session_end
574
+ cleanupOps.push(
575
+ (async () => {
576
+ const gradResult = await attemptGraduation(store, this.state.complete);
577
+ if (gradResult?.graduated && gradResult.soul) {
578
+ if (gradResult.report.stage === "ready") {
579
+ // New graduation — persist event for celebration
580
+ await store.queryExec(
581
+ `CREATE graduation_event CONTENT $data`,
582
+ {
583
+ data: {
584
+ session_id: session.sessionId,
585
+ acknowledged: false,
586
+ quality_score: gradResult.report.qualityScore,
587
+ volume_score: gradResult.report.volumeScore,
588
+ stage: gradResult.report.stage,
589
+ created_at: new Date().toISOString(),
590
+ },
591
+ },
592
+ );
593
+ if (this.state.enqueueSystemEvent) {
594
+ this.state.enqueueSystemEvent(
595
+ "[GRADUATION] KongBrain has achieved soul graduation! " +
596
+ "The agent will share this milestone when ready.",
597
+ { sessionKey: session.sessionKey },
598
+ );
599
+ }
600
+ } else {
601
+ // Pre-existing soul — check for evolution
602
+ await evolveSoul(store, this.state.complete);
603
+ }
604
+ }
605
+ })().catch(e => swallow.warn("midCleanup:soulGraduation", e)),
606
+ );
607
+
608
+ cleanupOps.push(
609
+ (async () => {
610
+ const transition = await checkStageTransition(store);
611
+ if (transition.transitioned && this.state.enqueueSystemEvent) {
612
+ this.state.enqueueSystemEvent(
613
+ `[MATURITY] Stage transition: ${transition.previousStage ?? "nascent"} → ${transition.currentStage}. ` +
614
+ `Volume: ${transition.report.met.length}/7 | Quality: ${transition.report.qualityScore.toFixed(2)}`,
615
+ { sessionKey: session.sessionKey },
616
+ );
617
+ }
618
+ })().catch(e => swallow.warn("midCleanup:stageTransition", e)),
619
+ );
620
+
497
621
  // Don't await — let cleanup run in background
498
622
  Promise.allSettled(cleanupOps).catch(() => {});
499
623
  }
@@ -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) {