kongbrain 0.2.0 → 0.2.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/package.json +1 -1
- package/src/context-engine.ts +118 -38
- package/src/daemon-manager.ts +10 -39
- package/src/hooks/llm-output.ts +1 -0
- package/src/index.ts +7 -17
- package/src/memory-daemon.ts +3 -2
- package/src/reflection.ts +12 -11
- package/src/schema.surql +3 -12
- package/src/skills.ts +2 -11
- package/src/soul.ts +2 -7
- package/src/state.ts +5 -0
- package/src/surreal.ts +2 -1
- package/src/tools/recall.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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/context-engine.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { readFileSync } from "node:fs";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
12
|
+
import { startMemoryDaemon } from "./daemon-manager.js";
|
|
12
13
|
import type {
|
|
13
14
|
ContextEngine, ContextEngineInfo,
|
|
14
15
|
} from "openclaw/plugin-sdk";
|
|
@@ -46,6 +47,9 @@ import { shouldRunCheck, runCognitiveCheck } from "./cognitive-check.js";
|
|
|
46
47
|
import { checkACANReadiness } from "./acan.js";
|
|
47
48
|
import { predictQueries, prefetchContext } from "./prefetch.js";
|
|
48
49
|
import { runDeferredCleanup } from "./deferred-cleanup.js";
|
|
50
|
+
import { extractSkill } from "./skills.js";
|
|
51
|
+
import { generateReflection } from "./reflection.js";
|
|
52
|
+
import { graduateCausalToSkills } from "./skills.js";
|
|
49
53
|
import { swallow } from "./errors.js";
|
|
50
54
|
|
|
51
55
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -88,32 +92,41 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
88
92
|
const sessionKey = params.sessionKey ?? params.sessionId;
|
|
89
93
|
const session = this.state.getOrCreateSession(sessionKey, params.sessionId);
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
// Only create graph nodes on first bootstrap for this session
|
|
96
|
+
if (!session.surrealSessionId) {
|
|
97
|
+
try {
|
|
98
|
+
const workspace = this.state.workspaceDir || process.cwd();
|
|
99
|
+
const projectName = workspace.split("/").pop() || "default";
|
|
100
|
+
|
|
101
|
+
session.agentId = await store.ensureAgent("kongbrain", "openclaw-default");
|
|
102
|
+
session.projectId = await store.ensureProject(projectName);
|
|
103
|
+
await store.linkAgentToProject(session.agentId, session.projectId)
|
|
104
|
+
.catch(e => swallow.warn("bootstrap:linkAgentToProject", e));
|
|
105
|
+
|
|
106
|
+
session.taskId = await store.createTask(`Session in ${projectName}`);
|
|
107
|
+
await store.linkAgentToTask(session.agentId, session.taskId)
|
|
108
|
+
.catch(e => swallow.warn("bootstrap:linkAgentToTask", e));
|
|
109
|
+
await store.linkTaskToProject(session.taskId, session.projectId)
|
|
110
|
+
.catch(e => swallow.warn("bootstrap:linkTaskToProject", e));
|
|
111
|
+
|
|
112
|
+
const surrealSessionId = await store.createSession(session.agentId);
|
|
113
|
+
await store.markSessionActive(surrealSessionId)
|
|
114
|
+
.catch(e => swallow.warn("bootstrap:markActive", e));
|
|
115
|
+
await store.linkSessionToTask(surrealSessionId, session.taskId)
|
|
116
|
+
.catch(e => swallow.warn("bootstrap:linkSessionToTask", e));
|
|
117
|
+
|
|
118
|
+
session.surrealSessionId = surrealSessionId;
|
|
119
|
+
session.lastUserTurnId = "";
|
|
120
|
+
|
|
121
|
+
// Start memory daemon for this session
|
|
122
|
+
if (!session.daemon) {
|
|
123
|
+
session.daemon = startMemoryDaemon(
|
|
124
|
+
store, embeddings, session.sessionId, this.state.complete,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
swallow.error("bootstrap:5pillar", e);
|
|
129
|
+
}
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
// Background maintenance (non-blocking)
|
|
@@ -156,11 +169,12 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
156
169
|
// Build system prompt additions
|
|
157
170
|
const additions: string[] = [];
|
|
158
171
|
|
|
159
|
-
// Wakeup briefing (synthesized at session start)
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
delete (session as any).
|
|
172
|
+
// Wakeup briefing (synthesized at session start, may still be in-flight)
|
|
173
|
+
const wakeupPromise = (session as any)._wakeupPromise as Promise<string | null> | undefined;
|
|
174
|
+
if (wakeupPromise) {
|
|
175
|
+
const wakeupBriefing = await wakeupPromise;
|
|
176
|
+
delete (session as any)._wakeupPromise; // Only inject once
|
|
177
|
+
if (wakeupBriefing) additions.push(wakeupBriefing);
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
// Graduation celebration — tell the agent it just graduated so it can share with the user
|
|
@@ -389,11 +403,6 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
389
403
|
}, session, store, this.state.complete).catch(e => swallow.warn("afterTurn:cognitiveCheck", e));
|
|
390
404
|
}
|
|
391
405
|
|
|
392
|
-
// Daemon batching — accumulate content tokens and flush when threshold met
|
|
393
|
-
if (session.lastAssistantText && hasSemantic(session.lastAssistantText)) {
|
|
394
|
-
session.newContentTokens += Math.ceil(session.lastAssistantText.length / 4);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
406
|
// Flush to daemon when token threshold OR turn count threshold is reached
|
|
398
407
|
const tokenReady = session.newContentTokens >= session.DAEMON_TOKEN_THRESHOLD;
|
|
399
408
|
const turnReady = session.userTurnCount >= session.lastDaemonFlushTurnCount + 3;
|
|
@@ -425,13 +434,84 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
425
434
|
swallow.warn("afterTurn:daemonBatch", e);
|
|
426
435
|
}
|
|
427
436
|
}
|
|
437
|
+
|
|
438
|
+
// Mid-session cleanup: simulate session_end after ~100k tokens.
|
|
439
|
+
// OpenClaw exits via Ctrl+C×2 (no async window), so session_end never fires.
|
|
440
|
+
// Run reflection, skill extraction, and causal graduation periodically.
|
|
441
|
+
const tokensSinceCleanup = session.cumulativeTokens - session.lastCleanupTokens;
|
|
442
|
+
if (tokensSinceCleanup >= session.MID_SESSION_CLEANUP_THRESHOLD && typeof this.state.complete === "function") {
|
|
443
|
+
session.lastCleanupTokens = session.cumulativeTokens;
|
|
444
|
+
|
|
445
|
+
// Fire-and-forget: these are non-critical background operations
|
|
446
|
+
const cleanupOps: Promise<unknown>[] = [];
|
|
447
|
+
|
|
448
|
+
// Final daemon flush with full transcript before cleanup
|
|
449
|
+
if (session.daemon) {
|
|
450
|
+
cleanupOps.push(
|
|
451
|
+
store.getSessionTurns(session.sessionId, 50)
|
|
452
|
+
.then(recentTurns => {
|
|
453
|
+
const turnData = recentTurns.map(t => ({
|
|
454
|
+
role: t.role as "user" | "assistant",
|
|
455
|
+
text: t.text,
|
|
456
|
+
turnId: (t as any).id,
|
|
457
|
+
}));
|
|
458
|
+
session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
|
|
459
|
+
})
|
|
460
|
+
.catch(e => swallow.warn("midCleanup:daemonFlush", e)),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (session.taskId) {
|
|
465
|
+
cleanupOps.push(
|
|
466
|
+
extractSkill(session.sessionId, session.taskId, store, embeddings, this.state.complete)
|
|
467
|
+
.catch(e => swallow.warn("midCleanup:extractSkill", e)),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
cleanupOps.push(
|
|
472
|
+
generateReflection(session.sessionId, store, embeddings, this.state.complete)
|
|
473
|
+
.catch(e => swallow.warn("midCleanup:reflection", e)),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
cleanupOps.push(
|
|
477
|
+
graduateCausalToSkills(store, embeddings, this.state.complete)
|
|
478
|
+
.catch(e => swallow.warn("midCleanup:graduateCausal", e)),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// Handoff note — snapshot for wakeup even if session continues
|
|
482
|
+
cleanupOps.push(
|
|
483
|
+
(async () => {
|
|
484
|
+
const recentTurns = await store.getSessionTurns(session.sessionId, 15);
|
|
485
|
+
if (recentTurns.length < 2) return;
|
|
486
|
+
const turnSummary = recentTurns
|
|
487
|
+
.map(t => `[${t.role}] ${t.text.slice(0, 200)}`)
|
|
488
|
+
.join("\n");
|
|
489
|
+
const handoffResponse = await this.state.complete({
|
|
490
|
+
system: "Summarize this session for handoff to your next self. What was worked on, what's unfinished, what to remember. 2-3 sentences. Write in first person.",
|
|
491
|
+
messages: [{ role: "user", content: turnSummary }],
|
|
492
|
+
});
|
|
493
|
+
const handoffText = handoffResponse.text.trim();
|
|
494
|
+
if (handoffText.length > 20) {
|
|
495
|
+
let embedding: number[] | null = null;
|
|
496
|
+
if (embeddings.isAvailable()) {
|
|
497
|
+
try { embedding = await embeddings.embed(handoffText); } catch { /* ok */ }
|
|
498
|
+
}
|
|
499
|
+
await store.createMemory(handoffText, embedding, 8, "handoff", session.sessionId);
|
|
500
|
+
}
|
|
501
|
+
})().catch(e => swallow.warn("midCleanup:handoff", e)),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Don't await — let cleanup run in background
|
|
505
|
+
Promise.allSettled(cleanupOps).catch(() => {});
|
|
506
|
+
}
|
|
428
507
|
}
|
|
429
508
|
|
|
430
509
|
// ── Dispose ────────────────────────────────────────────────────────────
|
|
431
510
|
|
|
432
511
|
async dispose(): Promise<void> {
|
|
433
|
-
//
|
|
434
|
-
|
|
512
|
+
// No-op: global state (store, embeddings, sessions) is shared across
|
|
513
|
+
// context engine instances and must NOT be destroyed here. OpenClaw
|
|
514
|
+
// creates a new context engine per turn and disposes the old one.
|
|
435
515
|
}
|
|
436
516
|
}
|
|
437
517
|
|
package/src/daemon-manager.ts
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
* The extraction is I/O-bound (LLM calls + DB writes), not CPU-bound,
|
|
8
8
|
* so in-process execution is fine.
|
|
9
9
|
*/
|
|
10
|
-
import type { SurrealConfig, EmbeddingConfig } from "./config.js";
|
|
11
10
|
import type { TurnData, PriorExtractions } from "./daemon-types.js";
|
|
12
11
|
import type { CompleteFn } from "./state.js";
|
|
13
|
-
import { SurrealStore } from "./surreal.js";
|
|
14
|
-
import { EmbeddingService } from "./embeddings.js";
|
|
12
|
+
import type { SurrealStore } from "./surreal.js";
|
|
13
|
+
import type { EmbeddingService } from "./embeddings.js";
|
|
15
14
|
import { swallow } from "./errors.js";
|
|
16
15
|
|
|
17
16
|
export type { TurnData } from "./daemon-types.js";
|
|
@@ -33,16 +32,14 @@ export interface MemoryDaemon {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function startMemoryDaemon(
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
sharedStore: SurrealStore,
|
|
36
|
+
sharedEmbeddings: EmbeddingService,
|
|
38
37
|
sessionId: string,
|
|
39
38
|
complete: CompleteFn,
|
|
40
39
|
): MemoryDaemon {
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let initialized = false;
|
|
45
|
-
let initFailed = false;
|
|
40
|
+
// Use shared store/embeddings from global state (no duplicate connections)
|
|
41
|
+
const store = sharedStore;
|
|
42
|
+
const embeddings = sharedEmbeddings;
|
|
46
43
|
let processing = false;
|
|
47
44
|
let shuttingDown = false;
|
|
48
45
|
let extractedTurnCount = 0;
|
|
@@ -52,24 +49,6 @@ export function startMemoryDaemon(
|
|
|
52
49
|
conceptNames: [], artifactPaths: [], skillNames: [],
|
|
53
50
|
};
|
|
54
51
|
|
|
55
|
-
// Lazy init — connect on first batch, not at startup
|
|
56
|
-
async function ensureInit(): Promise<boolean> {
|
|
57
|
-
if (initialized) return true;
|
|
58
|
-
if (initFailed) return false;
|
|
59
|
-
try {
|
|
60
|
-
store = new SurrealStore(surrealConfig);
|
|
61
|
-
await store.initialize();
|
|
62
|
-
embeddings = new EmbeddingService(embeddingConfig);
|
|
63
|
-
await embeddings.initialize();
|
|
64
|
-
initialized = true;
|
|
65
|
-
return true;
|
|
66
|
-
} catch (e) {
|
|
67
|
-
swallow.warn("daemon:init", e);
|
|
68
|
-
initFailed = true;
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
52
|
// Import extraction logic lazily to avoid circular deps
|
|
74
53
|
async function runExtraction(
|
|
75
54
|
turns: TurnData[],
|
|
@@ -172,10 +151,8 @@ export function startMemoryDaemon(
|
|
|
172
151
|
sendTurnBatch(turns, thinking, retrievedMemories, priorExtractions) {
|
|
173
152
|
if (shuttingDown) return;
|
|
174
153
|
pendingBatch = { turns, thinking, retrievedMemories, priorExtractions };
|
|
175
|
-
// Fire-and-forget
|
|
176
|
-
|
|
177
|
-
.then(ok => { if (ok) return processPending(); })
|
|
178
|
-
.catch(e => swallow.warn("daemon:sendBatch", e));
|
|
154
|
+
// Fire-and-forget
|
|
155
|
+
processPending().catch(e => swallow.warn("daemon:sendBatch", e));
|
|
179
156
|
},
|
|
180
157
|
|
|
181
158
|
async getStatus() {
|
|
@@ -200,13 +177,7 @@ export function startMemoryDaemon(
|
|
|
200
177
|
new Promise<void>(resolve => setTimeout(resolve, timeoutMs)),
|
|
201
178
|
]);
|
|
202
179
|
}
|
|
203
|
-
//
|
|
204
|
-
await Promise.allSettled([
|
|
205
|
-
store?.dispose(),
|
|
206
|
-
embeddings?.dispose(),
|
|
207
|
-
]).catch(() => {});
|
|
208
|
-
store = null;
|
|
209
|
-
embeddings = null;
|
|
180
|
+
// Shared store/embeddings — don't dispose (owned by global state)
|
|
210
181
|
},
|
|
211
182
|
|
|
212
183
|
getExtractedTurnCount() {
|
package/src/hooks/llm-output.ts
CHANGED
|
@@ -50,6 +50,7 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
50
50
|
// Accumulate for daemon batching (only when real tokens present)
|
|
51
51
|
if (inputTokens + outputTokens > 0) {
|
|
52
52
|
session.newContentTokens += inputTokens + outputTokens;
|
|
53
|
+
session.cumulativeTokens += inputTokens + outputTokens;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Track accumulated text output for planning gate
|
package/src/index.ts
CHANGED
|
@@ -23,7 +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 { synthesizeWakeup
|
|
26
|
+
import { synthesizeWakeup } from "./wakeup.js";
|
|
27
27
|
import { extractSkill } from "./skills.js";
|
|
28
28
|
import { generateReflection, setReflectionContextWindow } from "./reflection.js";
|
|
29
29
|
import { graduateCausalToSkills } from "./skills.js";
|
|
@@ -459,8 +459,8 @@ export default definePluginEntry({
|
|
|
459
459
|
// Start memory daemon worker thread
|
|
460
460
|
try {
|
|
461
461
|
session.daemon = startMemoryDaemon(
|
|
462
|
-
|
|
463
|
-
|
|
462
|
+
globalState!.store,
|
|
463
|
+
globalState!.embeddings,
|
|
464
464
|
session.sessionId,
|
|
465
465
|
globalState!.complete,
|
|
466
466
|
);
|
|
@@ -486,20 +486,10 @@ export default definePluginEntry({
|
|
|
486
486
|
detectGraduationEvent(globalState!.store, session, globalState!)
|
|
487
487
|
.catch(e => swallow("index:graduationDetect", e));
|
|
488
488
|
|
|
489
|
-
// Synthesize wakeup briefing (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (briefing) (session as any)._wakeupBriefing = briefing;
|
|
494
|
-
})
|
|
495
|
-
.catch(e => swallow.warn("index:wakeup", e));
|
|
496
|
-
|
|
497
|
-
// Startup cognition (background)
|
|
498
|
-
synthesizeStartupCognition(globalState!.store, globalState!.complete)
|
|
499
|
-
.then(cognition => {
|
|
500
|
-
if (cognition) (session as any)._startupCognition = cognition;
|
|
501
|
-
})
|
|
502
|
-
.catch(e => swallow.warn("index:startupCognition", e));
|
|
489
|
+
// Synthesize wakeup briefing — store the promise so assemble() can await it
|
|
490
|
+
(session as any)._wakeupPromise = synthesizeWakeup(
|
|
491
|
+
globalState!.store, globalState!.complete, session.sessionId, globalState!.workspaceDir,
|
|
492
|
+
).catch(e => { swallow.warn("index:wakeup", e); return null; });
|
|
503
493
|
|
|
504
494
|
// Deferred cleanup: extract knowledge from orphaned sessions (background)
|
|
505
495
|
runDeferredCleanup(globalState!.store, globalState!.embeddings, globalState!.complete)
|
package/src/memory-daemon.ts
CHANGED
|
@@ -37,11 +37,12 @@ ${dedup}
|
|
|
37
37
|
// Only when there's a clear trigger and outcome. Max 5.
|
|
38
38
|
{"triggerText": "what caused it (max 200 chars)", "outcomeText": "what happened as a result", "chainType": "debug|refactor|feature|fix", "success": true/false, "confidence": 0.0-1.0, "description": "1-sentence summary"}
|
|
39
39
|
],
|
|
40
|
-
|
|
40
|
+
"monologue": [
|
|
41
41
|
// Internal reasoning moments worth preserving: doubts, tradeoffs, insights, realizations.
|
|
42
|
+
// Infer from the conversation flow — approach changes, surprising discoveries, tradeoff decisions.
|
|
42
43
|
// Skip routine reasoning. Only novel/surprising thoughts. Max 5.
|
|
43
44
|
{"category": "doubt|tradeoff|alternative|insight|realization", "content": "1-2 sentence description"}
|
|
44
|
-
]
|
|
45
|
+
],
|
|
45
46
|
${hasRetrievedMemories ? ` "resolved": [
|
|
46
47
|
// IDs from [RETRIEVED MEMORIES] that have been FULLY addressed/fixed/completed in this conversation.
|
|
47
48
|
// Must be exact IDs like "memory:abc123". Empty [] if none resolved.
|
package/src/reflection.ts
CHANGED
|
@@ -140,30 +140,31 @@ export async function generateReflection(
|
|
|
140
140
|
embeddings: EmbeddingService,
|
|
141
141
|
complete: CompleteFn,
|
|
142
142
|
): Promise<void> {
|
|
143
|
-
|
|
144
|
-
if (!metrics) return;
|
|
143
|
+
if (!store.isAvailable()) return;
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
// Get session turns directly — no dependency on orchestrator_metrics
|
|
146
|
+
const turns = await store.getSessionTurns(sessionId, 30).catch(() => []);
|
|
147
|
+
if (turns.length < 3) return; // Too short for meaningful reflection
|
|
148
148
|
|
|
149
|
-
const
|
|
149
|
+
const transcript = turns
|
|
150
|
+
.map(t => `[${t.role}] ${(t.text ?? "").slice(0, 300)}`)
|
|
151
|
+
.join("\n");
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (metrics.steeringCandidates >= STEERING_THRESHOLD) category = "approach_strategy";
|
|
153
|
+
const severity = turns.length >= 15 ? "moderate" : "minor";
|
|
154
|
+
const category = "session_review";
|
|
154
155
|
|
|
155
156
|
try {
|
|
156
157
|
const response = await complete({
|
|
157
|
-
system: `Write 2-4 sentences:
|
|
158
|
+
system: `Reflect on this session. Write 2-4 sentences about: what went well, what could improve, any patterns worth noting. Be specific and actionable. If the session was too trivial for reflection, respond with just "skip".`,
|
|
158
159
|
messages: [{
|
|
159
160
|
role: "user",
|
|
160
|
-
content:
|
|
161
|
+
content: `Session with ${turns.length} turns:\n${transcript.slice(0, 15000)}`,
|
|
161
162
|
}],
|
|
162
163
|
});
|
|
163
164
|
|
|
164
165
|
const reflectionText = response.text.trim();
|
|
165
166
|
|
|
166
|
-
if (reflectionText.length < 20) return;
|
|
167
|
+
if (reflectionText.length < 20 || reflectionText.toLowerCase() === "skip") return;
|
|
167
168
|
|
|
168
169
|
let reflEmb: number[] | null = null;
|
|
169
170
|
if (embeddings.isAvailable()) {
|
package/src/schema.surql
CHANGED
|
@@ -375,15 +375,6 @@ DEFINE FIELD IF NOT EXISTS created_at ON graduation_event TYPE datetime DEFAULT
|
|
|
375
375
|
-- ============================================================
|
|
376
376
|
-- MIGRATIONS (must run after table definitions)
|
|
377
377
|
-- ============================================================
|
|
378
|
-
--
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
REMOVE INDEX IF EXISTS concept_vec_idx ON concept;
|
|
382
|
-
REMOVE INDEX IF EXISTS memory_vec_idx ON memory;
|
|
383
|
-
REMOVE INDEX IF EXISTS artifact_vec_idx ON artifact;
|
|
384
|
-
-- Clear stale 768d embeddings (incompatible with new 1024d model)
|
|
385
|
-
UPDATE turn SET embedding = NONE WHERE embedding != NONE AND array::len(embedding) = 768;
|
|
386
|
-
UPDATE identity_chunk SET embedding = NONE WHERE embedding != NONE AND array::len(embedding) = 768;
|
|
387
|
-
UPDATE concept SET embedding = NONE WHERE embedding != NONE AND array::len(embedding) = 768;
|
|
388
|
-
UPDATE memory SET embedding = NONE WHERE embedding != NONE AND array::len(embedding) = 768;
|
|
389
|
-
UPDATE artifact SET embedding = NONE WHERE embedding != NONE AND array::len(embedding) = 768;
|
|
378
|
+
-- 768d → 1024d migration completed; REMOVE INDEX / UPDATE stale
|
|
379
|
+
-- embeddings removed to avoid destroying live HNSW indexes on
|
|
380
|
+
-- every startup.
|
package/src/skills.ts
CHANGED
|
@@ -60,17 +60,8 @@ export async function extractSkill(
|
|
|
60
60
|
): Promise<string | null> {
|
|
61
61
|
if (!store.isAvailable()) return null;
|
|
62
62
|
|
|
63
|
-
// Check if session had enough tool activity
|
|
64
|
-
const metricsRows = await store.queryFirst<{ totalTools: number }>(
|
|
65
|
-
`SELECT math::sum(actual_tool_calls) AS totalTools
|
|
66
|
-
FROM orchestrator_metrics WHERE session_id = $sid GROUP ALL`,
|
|
67
|
-
{ sid: sessionId },
|
|
68
|
-
).catch(() => [] as { totalTools: number }[]);
|
|
69
|
-
const totalTools = Number(metricsRows[0]?.totalTools ?? 0);
|
|
70
|
-
if (totalTools < 3) return null;
|
|
71
|
-
|
|
72
63
|
const turns = await store.getSessionTurns(sessionId, 50);
|
|
73
|
-
if (turns.length < 4) return null;
|
|
64
|
+
if (turns.length < 4) return null; // Too short for skill extraction
|
|
74
65
|
|
|
75
66
|
const transcript = turns
|
|
76
67
|
.map((t) => `[${t.role}] ${(t.text ?? "").slice(0, 300)}`)
|
|
@@ -81,7 +72,7 @@ export async function extractSkill(
|
|
|
81
72
|
system: `Return JSON or null. Fields: {name, description, preconditions, steps: [{tool, description}] (max 8), postconditions}. Generic patterns only (no specific paths). null if no clear multi-step workflow.`,
|
|
82
73
|
messages: [{
|
|
83
74
|
role: "user",
|
|
84
|
-
content: `${
|
|
75
|
+
content: `${turns.length} turns:\n${transcript.slice(0, 20000)}`,
|
|
85
76
|
}],
|
|
86
77
|
});
|
|
87
78
|
|
package/src/soul.ts
CHANGED
|
@@ -36,7 +36,6 @@ export interface GraduationSignals {
|
|
|
36
36
|
reflections: number;
|
|
37
37
|
causalChains: number;
|
|
38
38
|
concepts: number;
|
|
39
|
-
memoryCompactions: number;
|
|
40
39
|
monologues: number;
|
|
41
40
|
spanDays: number;
|
|
42
41
|
}
|
|
@@ -91,7 +90,6 @@ const THRESHOLDS: GraduationSignals = {
|
|
|
91
90
|
reflections: 10,
|
|
92
91
|
causalChains: 5,
|
|
93
92
|
concepts: 30,
|
|
94
|
-
memoryCompactions: 5,
|
|
95
93
|
monologues: 5,
|
|
96
94
|
spanDays: 3,
|
|
97
95
|
};
|
|
@@ -104,17 +102,16 @@ const QUALITY_GATE = 0.6;
|
|
|
104
102
|
async function getGraduationSignals(store: SurrealStore): Promise<GraduationSignals> {
|
|
105
103
|
const defaults: GraduationSignals = {
|
|
106
104
|
sessions: 0, reflections: 0, causalChains: 0,
|
|
107
|
-
concepts: 0,
|
|
105
|
+
concepts: 0, monologues: 0, spanDays: 0,
|
|
108
106
|
};
|
|
109
107
|
if (!store.isAvailable()) return defaults;
|
|
110
108
|
|
|
111
109
|
try {
|
|
112
|
-
const [sessions, reflections, causal, concepts,
|
|
110
|
+
const [sessions, reflections, causal, concepts, monologues, span] = await Promise.all([
|
|
113
111
|
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM session GROUP ALL`).catch(() => []),
|
|
114
112
|
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM reflection GROUP ALL`).catch(() => []),
|
|
115
113
|
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM causal_chain GROUP ALL`).catch(() => []),
|
|
116
114
|
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM concept GROUP ALL`).catch(() => []),
|
|
117
|
-
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM compaction_checkpoint WHERE status = "complete" GROUP ALL`).catch(() => []),
|
|
118
115
|
store.queryFirst<{ count: number }>(`SELECT count() AS count FROM monologue GROUP ALL`).catch(() => []),
|
|
119
116
|
store.queryFirst<{ earliest: string }>(`SELECT started_at AS earliest FROM session ORDER BY started_at ASC LIMIT 1`).catch(() => []),
|
|
120
117
|
]);
|
|
@@ -130,7 +127,6 @@ async function getGraduationSignals(store: SurrealStore): Promise<GraduationSign
|
|
|
130
127
|
reflections: (reflections as { count: number }[])[0]?.count ?? 0,
|
|
131
128
|
causalChains: (causal as { count: number }[])[0]?.count ?? 0,
|
|
132
129
|
concepts: (concepts as { count: number }[])[0]?.count ?? 0,
|
|
133
|
-
memoryCompactions: (compactions as { count: number }[])[0]?.count ?? 0,
|
|
134
130
|
monologues: (monologues as { count: number }[])[0]?.count ?? 0,
|
|
135
131
|
spanDays,
|
|
136
132
|
};
|
|
@@ -358,7 +354,6 @@ function getSuggestion(key: keyof GraduationSignals, current: number, threshold:
|
|
|
358
354
|
case "reflections": return `${remaining} more reflection(s) needed. These are generated automatically when sessions have performance issues.`;
|
|
359
355
|
case "causalChains": return `${remaining} more causal chain(s) needed. These form when the agent corrects mistakes during tool usage.`;
|
|
360
356
|
case "concepts": return `${remaining} more concept(s) needed. Concepts are extracted from conversation topics and domain vocabulary.`;
|
|
361
|
-
case "memoryCompactions": return `${remaining} more compaction(s) needed. These happen during longer sessions with substantial context.`;
|
|
362
357
|
case "monologues": return `${remaining} more monologue(s) needed. Inner monologue triggers during cognitive checks.`;
|
|
363
358
|
case "spanDays": return `${remaining} more day(s) of history needed. The agent needs time-spread experience, not just volume.`;
|
|
364
359
|
}
|
package/src/state.ts
CHANGED
|
@@ -57,6 +57,11 @@ export class SessionState {
|
|
|
57
57
|
readonly DAEMON_TOKEN_THRESHOLD = 4000;
|
|
58
58
|
lastDaemonFlushTurnCount = 0;
|
|
59
59
|
|
|
60
|
+
// Cumulative session token tracking (for mid-session cleanup trigger)
|
|
61
|
+
cumulativeTokens = 0;
|
|
62
|
+
lastCleanupTokens = 0;
|
|
63
|
+
readonly MID_SESSION_CLEANUP_THRESHOLD = 100_000;
|
|
64
|
+
|
|
60
65
|
// Cleanup tracking
|
|
61
66
|
cleanedUp = false;
|
|
62
67
|
|
package/src/surreal.ts
CHANGED
|
@@ -924,7 +924,8 @@ export class SurrealStore {
|
|
|
924
924
|
timestamp: string;
|
|
925
925
|
}>(
|
|
926
926
|
`SELECT role, text, tool_name, timestamp FROM turn
|
|
927
|
-
WHERE
|
|
927
|
+
WHERE id IN (SELECT VALUE in FROM part_of WHERE out = $sid)
|
|
928
|
+
AND text != NONE AND text != ""
|
|
928
929
|
ORDER BY timestamp DESC LIMIT $lim`,
|
|
929
930
|
{ sid: prevSessionId, lim: limit },
|
|
930
931
|
);
|
package/src/tools/recall.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function createRecallToolDef(state: GlobalPluginState, session: SessionSt
|
|
|
41
41
|
const scope = params.scope ?? "all";
|
|
42
42
|
|
|
43
43
|
if (scope === "skills") {
|
|
44
|
-
const skills = await findRelevantSkills(queryVec, maxResults);
|
|
44
|
+
const skills = await findRelevantSkills(queryVec, maxResults, store);
|
|
45
45
|
if (skills.length === 0) {
|
|
46
46
|
return { content: [{ type: "text" as const, text: `No skills found matching "${params.query}".` }], details: null };
|
|
47
47
|
}
|