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 +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/context-engine.ts +128 -4
- package/src/daemon-manager.ts +17 -11
- package/src/deferred-cleanup.ts +3 -7
- package/src/graph-context.ts +220 -69
- package/src/handoff-file.ts +12 -5
- package/src/hooks/after-tool-call.ts +3 -3
- package/src/hooks/before-tool-call.ts +48 -1
- package/src/hooks/llm-output.ts +28 -8
- package/src/hooks/subagent-lifecycle.ts +142 -0
- package/src/index.ts +11 -2
- package/src/orchestrator.ts +1 -1
- package/src/reflection.ts +1 -0
- package/src/soul.ts +1 -1
- package/src/state.ts +18 -0
- package/src/surreal.ts +4 -0
- package/src/tools/core-memory.ts +9 -1
- package/src/tools/recall.ts +3 -3
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
|
+
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
|
+
"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/context-engine.ts
CHANGED
|
@@ -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:
|
|
325
|
-
reason:
|
|
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
|
}
|
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) {
|