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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kongbrain",
3
- "version": "0.2.0",
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",
@@ -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
- try {
92
- const workspace = this.state.workspaceDir || process.cwd();
93
- const projectName = workspace.split("/").pop() || "default";
94
-
95
- session.agentId = await store.ensureAgent("kongbrain", "openclaw-default");
96
- session.projectId = await store.ensureProject(projectName);
97
- await store.linkAgentToProject(session.agentId, session.projectId)
98
- .catch(e => swallow.warn("bootstrap:linkAgentToProject", e));
99
-
100
- session.taskId = await store.createTask(`Session in ${projectName}`);
101
- await store.linkAgentToTask(session.agentId, session.taskId)
102
- .catch(e => swallow.warn("bootstrap:linkAgentToTask", e));
103
- await store.linkTaskToProject(session.taskId, session.projectId)
104
- .catch(e => swallow.warn("bootstrap:linkTaskToProject", e));
105
-
106
- const surrealSessionId = await store.createSession(session.agentId);
107
- await store.markSessionActive(surrealSessionId)
108
- .catch(e => swallow.warn("bootstrap:markActive", e));
109
- await store.linkSessionToTask(surrealSessionId, session.taskId)
110
- .catch(e => swallow.warn("bootstrap:linkSessionToTask", e));
111
-
112
- // Store the DB session ID for cleanup tracking
113
- session.surrealSessionId = surrealSessionId;
114
- session.lastUserTurnId = "";
115
- } catch (e) {
116
- swallow.error("bootstrap:5pillar", e);
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 wakeupBriefing = (session as any)._wakeupBriefing as string | undefined;
161
- if (wakeupBriefing) {
162
- additions.push(wakeupBriefing);
163
- delete (session as any)._wakeupBriefing; // Only inject once
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
- // Phase 3: combined extraction, graduation, soul graduation
434
- await this.state.shutdown();
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
 
@@ -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
- surrealConfig: SurrealConfig,
37
- embeddingConfig: EmbeddingConfig,
35
+ sharedStore: SurrealStore,
36
+ sharedEmbeddings: EmbeddingService,
38
37
  sessionId: string,
39
38
  complete: CompleteFn,
40
39
  ): MemoryDaemon {
41
- // Daemon-local DB and embedding instances (separate connections)
42
- let store: SurrealStore | null = null;
43
- let embeddings: EmbeddingService | null = null;
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: init if needed, then process
176
- ensureInit()
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
- // Clean up daemon-local connections
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() {
@@ -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, synthesizeStartupCognition } from "./wakeup.js";
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
- config.surreal,
463
- config.embedding,
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 (background, non-blocking)
490
- // The briefing is stored and later injected via assemble()'s systemPromptAddition
491
- synthesizeWakeup(globalState!.store, globalState!.complete, session.sessionId, globalState!.workspaceDir)
492
- .then(briefing => {
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)
@@ -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
- ${hasThinking ? ` "monologue": [
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
- ],` : ' "monologue": [],'}
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
- const metrics = await gatherSessionMetrics(sessionId, store);
144
- if (!metrics) return;
143
+ if (!store.isAvailable()) return;
145
144
 
146
- const { reflect, reasons } = shouldReflect(metrics);
147
- if (!reflect) return;
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 severity = reasons.length >= 3 ? "critical" : reasons.length >= 2 ? "moderate" : "minor";
149
+ const transcript = turns
150
+ .map(t => `[${t.role}] ${(t.text ?? "").slice(0, 300)}`)
151
+ .join("\n");
150
152
 
151
- let category = "efficiency";
152
- if (metrics.toolFailureRate > TOOL_FAILURE_THRESHOLD) category = "failure_pattern";
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: root cause, error pattern, what to do differently. Be specific. Example: "Spent 8 tool calls reading source before checking error log. For timeout bugs, check logs first."`,
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: `${metrics.totalTurns} turns, ${metrics.totalToolCalls} tools, ${(metrics.avgUtilization * 100).toFixed(0)}% util, ${(metrics.toolFailureRate * 100).toFixed(0)}% fail, ~${metrics.wastedTokens} wasted tokens\nIssues: ${reasons.join("; ")}`,
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
- -- Drop old 768d HNSW indexes (now 1024d with BGE-M3)
379
- REMOVE INDEX IF EXISTS turn_vec_idx ON turn;
380
- REMOVE INDEX IF EXISTS identity_vec_idx ON identity_chunk;
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: `${totalTools} tool calls:\n${transcript.slice(0, 20000)}`,
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, memoryCompactions: 0, monologues: 0, spanDays: 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, compactions, monologues, span] = await Promise.all([
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 session_id = $sid AND text != NONE AND text != ""
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
  );
@@ -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
  }