kongbrain 0.2.1 → 0.3.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/acan.ts +4 -3
- package/src/config.ts +27 -0
- package/src/context-engine.ts +24 -14
- package/src/daemon-manager.ts +7 -1
- package/src/embeddings.ts +0 -12
- package/src/graph-context.ts +16 -6
- package/src/hooks/llm-output.ts +18 -9
- package/src/index.ts +2 -1
- package/src/schema.surql +0 -3
- package/src/skills.ts +1 -1
- package/src/state.ts +5 -2
- package/src/surreal.ts +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
@@ -282,8 +282,9 @@ function trainInBackground(
|
|
|
282
282
|
const STALENESS_GROWTH_FACTOR = 0.5;
|
|
283
283
|
const STALENESS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
284
284
|
|
|
285
|
-
export async function checkACANReadiness(store?: SurrealStore): Promise<void> {
|
|
285
|
+
export async function checkACANReadiness(store?: SurrealStore, trainingThreshold?: number): Promise<void> {
|
|
286
286
|
if (!store) return;
|
|
287
|
+
const threshold = trainingThreshold ?? TRAINING_THRESHOLD;
|
|
287
288
|
const weightsPath = join(getKongDir(), WEIGHTS_FILENAME);
|
|
288
289
|
const hasWeights = initACAN();
|
|
289
290
|
const count = await getTrainingDataCount(store);
|
|
@@ -295,13 +296,13 @@ export async function checkACANReadiness(store?: SurrealStore): Promise<void> {
|
|
|
295
296
|
const ageMs = Date.now() - trainedAt;
|
|
296
297
|
const isStale = growthRatio >= STALENESS_GROWTH_FACTOR || ageMs >= STALENESS_MAX_AGE_MS;
|
|
297
298
|
if (!isStale) return;
|
|
298
|
-
} else if (count <
|
|
299
|
+
} else if (count < threshold) {
|
|
299
300
|
return;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
try {
|
|
303
304
|
const samples = await fetchTrainingData(store);
|
|
304
|
-
if (samples.length <
|
|
305
|
+
if (samples.length < threshold) return;
|
|
305
306
|
trainInBackground(samples, weightsPath, hasWeights ? _weights ?? undefined : undefined);
|
|
306
307
|
} catch {
|
|
307
308
|
// training is best-effort
|
package/src/config.ts
CHANGED
|
@@ -15,9 +15,23 @@ export interface EmbeddingConfig {
|
|
|
15
15
|
dimensions: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface ThresholdConfig {
|
|
19
|
+
/** Tokens accumulated before daemon flushes extraction (default: 4000) */
|
|
20
|
+
daemonTokenThreshold: number;
|
|
21
|
+
/** Cumulative tokens before mid-session cleanup fires (default: 100000) */
|
|
22
|
+
midSessionCleanupThreshold: number;
|
|
23
|
+
/** Per-extraction timeout in ms (default: 60000) */
|
|
24
|
+
extractionTimeoutMs: number;
|
|
25
|
+
/** Max pending thinking blocks kept in memory (default: 20) */
|
|
26
|
+
maxPendingThinking: number;
|
|
27
|
+
/** Retrieval outcome samples needed before ACAN training (default: 5000) */
|
|
28
|
+
acanTrainingThreshold: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export interface KongBrainConfig {
|
|
19
32
|
surreal: SurrealConfig;
|
|
20
33
|
embedding: EmbeddingConfig;
|
|
34
|
+
thresholds: ThresholdConfig;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|
|
@@ -27,6 +41,7 @@ export interface KongBrainConfig {
|
|
|
27
41
|
export function parsePluginConfig(raw?: Record<string, unknown>): KongBrainConfig {
|
|
28
42
|
const surreal = (raw?.surreal ?? {}) as Record<string, unknown>;
|
|
29
43
|
const embedding = (raw?.embedding ?? {}) as Record<string, unknown>;
|
|
44
|
+
const thresholds = (raw?.thresholds ?? {}) as Record<string, unknown>;
|
|
30
45
|
|
|
31
46
|
// Priority: plugin config > env vars > defaults
|
|
32
47
|
const url =
|
|
@@ -60,5 +75,17 @@ export function parsePluginConfig(raw?: Record<string, unknown>): KongBrainConfi
|
|
|
60
75
|
dimensions:
|
|
61
76
|
typeof embedding.dimensions === "number" ? embedding.dimensions : 1024,
|
|
62
77
|
},
|
|
78
|
+
thresholds: {
|
|
79
|
+
daemonTokenThreshold:
|
|
80
|
+
typeof thresholds.daemonTokenThreshold === "number" ? thresholds.daemonTokenThreshold : 4000,
|
|
81
|
+
midSessionCleanupThreshold:
|
|
82
|
+
typeof thresholds.midSessionCleanupThreshold === "number" ? thresholds.midSessionCleanupThreshold : 100_000,
|
|
83
|
+
extractionTimeoutMs:
|
|
84
|
+
typeof thresholds.extractionTimeoutMs === "number" ? thresholds.extractionTimeoutMs : 60_000,
|
|
85
|
+
maxPendingThinking:
|
|
86
|
+
typeof thresholds.maxPendingThinking === "number" ? thresholds.maxPendingThinking : 20,
|
|
87
|
+
acanTrainingThreshold:
|
|
88
|
+
typeof thresholds.acanTrainingThreshold === "number" ? thresholds.acanTrainingThreshold : 5000,
|
|
89
|
+
},
|
|
63
90
|
};
|
|
64
91
|
}
|
package/src/context-engine.ts
CHANGED
|
@@ -73,19 +73,22 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
73
73
|
}): Promise<BootstrapResult> {
|
|
74
74
|
const { store, embeddings } = this.state;
|
|
75
75
|
|
|
76
|
-
// Run schema
|
|
77
|
-
|
|
78
|
-
const schemaPath = join(__dirname, "..", "src", "schema.surql");
|
|
79
|
-
let schemaSql: string;
|
|
76
|
+
// Run schema once per process (idempotent but expensive on every bootstrap)
|
|
77
|
+
if (!this.state.schemaApplied) {
|
|
80
78
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const schemaPath = join(__dirname, "..", "src", "schema.surql");
|
|
80
|
+
let schemaSql: string;
|
|
81
|
+
try {
|
|
82
|
+
schemaSql = readFileSync(schemaPath, "utf-8");
|
|
83
|
+
} catch {
|
|
84
|
+
// Fallback: try relative to compiled output
|
|
85
|
+
schemaSql = readFileSync(join(__dirname, "schema.surql"), "utf-8");
|
|
86
|
+
}
|
|
87
|
+
await store.queryExec(schemaSql);
|
|
88
|
+
this.state.schemaApplied = true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
swallow.warn("context-engine:schema", e);
|
|
85
91
|
}
|
|
86
|
-
await store.queryExec(schemaSql);
|
|
87
|
-
} catch (e) {
|
|
88
|
-
swallow.warn("context-engine:schema", e);
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// 5-pillar graph init
|
|
@@ -122,6 +125,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
122
125
|
if (!session.daemon) {
|
|
123
126
|
session.daemon = startMemoryDaemon(
|
|
124
127
|
store, embeddings, session.sessionId, this.state.complete,
|
|
128
|
+
this.state.config.thresholds.extractionTimeoutMs,
|
|
125
129
|
);
|
|
126
130
|
}
|
|
127
131
|
} catch (e) {
|
|
@@ -135,7 +139,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
135
139
|
store.archiveOldTurns(),
|
|
136
140
|
store.consolidateMemories((text) => embeddings.embed(text)),
|
|
137
141
|
store.garbageCollectMemories(),
|
|
138
|
-
checkACANReadiness(store),
|
|
142
|
+
checkACANReadiness(store, this.state.config.thresholds.acanTrainingThreshold),
|
|
139
143
|
// Deferred cleanup is triggered on first afterTurn() when complete() is available
|
|
140
144
|
]).catch(e => swallow.warn("bootstrap:maintenance", e));
|
|
141
145
|
|
|
@@ -404,7 +408,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
// Flush to daemon when token threshold OR turn count threshold is reached
|
|
407
|
-
const tokenReady = session.newContentTokens >= session.
|
|
411
|
+
const tokenReady = session.newContentTokens >= session.daemonTokenThreshold;
|
|
408
412
|
const turnReady = session.userTurnCount >= session.lastDaemonFlushTurnCount + 3;
|
|
409
413
|
if (session.daemon && (tokenReady || turnReady)) {
|
|
410
414
|
try {
|
|
@@ -439,7 +443,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
439
443
|
// OpenClaw exits via Ctrl+C×2 (no async window), so session_end never fires.
|
|
440
444
|
// Run reflection, skill extraction, and causal graduation periodically.
|
|
441
445
|
const tokensSinceCleanup = session.cumulativeTokens - session.lastCleanupTokens;
|
|
442
|
-
if (tokensSinceCleanup >= session.
|
|
446
|
+
if (tokensSinceCleanup >= session.midSessionCleanupThreshold && typeof this.state.complete === "function") {
|
|
443
447
|
session.lastCleanupTokens = session.cumulativeTokens;
|
|
444
448
|
|
|
445
449
|
// Fire-and-forget: these are non-critical background operations
|
|
@@ -478,6 +482,12 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
478
482
|
.catch(e => swallow.warn("midCleanup:graduateCausal", e)),
|
|
479
483
|
);
|
|
480
484
|
|
|
485
|
+
// ACAN: check if new retrieval outcomes warrant retraining
|
|
486
|
+
cleanupOps.push(
|
|
487
|
+
checkACANReadiness(store, this.state.config.thresholds.acanTrainingThreshold)
|
|
488
|
+
.catch(e => swallow("midCleanup:acan", e)),
|
|
489
|
+
);
|
|
490
|
+
|
|
481
491
|
// Handoff note — snapshot for wakeup even if session continues
|
|
482
492
|
cleanupOps.push(
|
|
483
493
|
(async () => {
|
package/src/daemon-manager.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function startMemoryDaemon(
|
|
|
36
36
|
sharedEmbeddings: EmbeddingService,
|
|
37
37
|
sessionId: string,
|
|
38
38
|
complete: CompleteFn,
|
|
39
|
+
extractionTimeoutMs = 60_000,
|
|
39
40
|
): MemoryDaemon {
|
|
40
41
|
// Use shared store/embeddings from global state (no duplicate connections)
|
|
41
42
|
const store = sharedStore;
|
|
@@ -137,7 +138,12 @@ export function startMemoryDaemon(
|
|
|
137
138
|
const batch = pendingBatch;
|
|
138
139
|
pendingBatch = null;
|
|
139
140
|
try {
|
|
140
|
-
await
|
|
141
|
+
await Promise.race([
|
|
142
|
+
runExtraction(batch.turns, batch.thinking, batch.retrievedMemories, batch.priorExtractions),
|
|
143
|
+
new Promise<void>((_, reject) =>
|
|
144
|
+
setTimeout(() => reject(new Error(`Extraction timed out after ${extractionTimeoutMs}ms`)), extractionTimeoutMs),
|
|
145
|
+
),
|
|
146
|
+
]);
|
|
141
147
|
} catch (e) {
|
|
142
148
|
errorCount++;
|
|
143
149
|
swallow.warn("daemon:extraction", e);
|
package/src/embeddings.ts
CHANGED
|
@@ -11,7 +11,6 @@ export class EmbeddingService {
|
|
|
11
11
|
private model: LlamaModel | null = null;
|
|
12
12
|
private ctx: LlamaEmbeddingContext | null = null;
|
|
13
13
|
private ready = false;
|
|
14
|
-
private embedCallCount = 0;
|
|
15
14
|
|
|
16
15
|
constructor(private readonly config: EmbeddingConfig) {}
|
|
17
16
|
|
|
@@ -40,7 +39,6 @@ export class EmbeddingService {
|
|
|
40
39
|
|
|
41
40
|
async embed(text: string): Promise<number[]> {
|
|
42
41
|
if (!this.ready || !this.ctx) throw new Error("Embeddings not initialized");
|
|
43
|
-
this.embedCallCount++;
|
|
44
42
|
const result = await this.ctx.getEmbeddingFor(text);
|
|
45
43
|
return Array.from(result.vector);
|
|
46
44
|
}
|
|
@@ -58,16 +56,6 @@ export class EmbeddingService {
|
|
|
58
56
|
return this.ready;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
drainEmbedCallCount(): number {
|
|
62
|
-
const count = this.embedCallCount;
|
|
63
|
-
this.embedCallCount = 0;
|
|
64
|
-
return count;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
getEmbedCallCount(): number {
|
|
68
|
-
return this.embedCallCount;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
59
|
async dispose(): Promise<void> {
|
|
72
60
|
try {
|
|
73
61
|
await this.ctx?.dispose();
|
package/src/graph-context.ts
CHANGED
|
@@ -634,9 +634,9 @@ async function formatContextMessage(
|
|
|
634
634
|
|
|
635
635
|
function truncateToolResult(msg: AgentMessage, maxChars: number): AgentMessage {
|
|
636
636
|
if (!isToolResult(msg)) return msg;
|
|
637
|
-
const totalLen = msg.content.reduce((s, c) => s + ((c as TextContent).text?.length ?? 0), 0);
|
|
637
|
+
const totalLen = msg.content.reduce((s: number, c: any) => s + ((c as TextContent).text?.length ?? 0), 0);
|
|
638
638
|
if (totalLen <= maxChars) return msg;
|
|
639
|
-
const content = msg.content.map((c) => {
|
|
639
|
+
const content = msg.content.map((c: any) => {
|
|
640
640
|
if (c.type !== "text") return c;
|
|
641
641
|
const tc = c as TextContent;
|
|
642
642
|
const allowed = Math.max(200, Math.floor((tc.text.length / totalLen) * maxChars));
|
|
@@ -654,8 +654,8 @@ function getRecentTurns(messages: AgentMessage[], maxTokens: number, contextWind
|
|
|
654
654
|
const clean = messages.map((m) => {
|
|
655
655
|
if (isAssistant(m) && m.stopReason === "error") {
|
|
656
656
|
const errorText = m.content
|
|
657
|
-
.filter((c): c is TextContent => c.type === "text")
|
|
658
|
-
.map((c) => c.text)
|
|
657
|
+
.filter((c: any): c is TextContent => c.type === "text")
|
|
658
|
+
.map((c: any) => c.text)
|
|
659
659
|
.join("")
|
|
660
660
|
.slice(0, 150);
|
|
661
661
|
return {
|
|
@@ -672,7 +672,7 @@ function getRecentTurns(messages: AgentMessage[], maxTokens: number, contextWind
|
|
|
672
672
|
let i = 0;
|
|
673
673
|
while (i < clean.length) {
|
|
674
674
|
const msg = clean[i];
|
|
675
|
-
if (isAssistant(msg) && msg.content.some((c) => c.type === "toolCall")) {
|
|
675
|
+
if (isAssistant(msg) && msg.content.some((c: any) => c.type === "toolCall")) {
|
|
676
676
|
const group: AgentMessage[] = [clean[i]];
|
|
677
677
|
let j = i + 1;
|
|
678
678
|
while (j < clean.length && isToolResult(clean[j])) {
|
|
@@ -837,9 +837,19 @@ async function graphTransformInner(
|
|
|
837
837
|
const config = session.currentConfig;
|
|
838
838
|
const skipRetrieval = config?.skipRetrieval ?? false;
|
|
839
839
|
const currentIntent = config?.intent ?? "unknown";
|
|
840
|
-
const
|
|
840
|
+
const baseLimits = config?.vectorSearchLimits ?? {
|
|
841
841
|
turn: 25, identity: 10, concept: 20, memory: 20, artifact: 10,
|
|
842
842
|
};
|
|
843
|
+
// Scale search limits with context window — larger windows can use more results
|
|
844
|
+
const cwScale = Math.max(0.5, Math.min(2.0, contextWindow / 200_000));
|
|
845
|
+
const vectorSearchLimits = {
|
|
846
|
+
turn: Math.round((baseLimits.turn ?? 25) * cwScale),
|
|
847
|
+
identity: baseLimits.identity, // always load full identity
|
|
848
|
+
concept: Math.round((baseLimits.concept ?? 20) * cwScale),
|
|
849
|
+
memory: Math.round((baseLimits.memory ?? 20) * cwScale),
|
|
850
|
+
artifact: Math.round((baseLimits.artifact ?? 10) * cwScale),
|
|
851
|
+
monologue: Math.round(8 * cwScale),
|
|
852
|
+
};
|
|
843
853
|
let tokenBudget = Math.min(config?.tokenBudget ?? 6000, budgets.retrieval);
|
|
844
854
|
|
|
845
855
|
// Pressure-based adaptive scaling
|
package/src/hooks/llm-output.ts
CHANGED
|
@@ -30,9 +30,16 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
30
30
|
const session = state.getSession(sessionKey);
|
|
31
31
|
if (!session) return;
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
33
|
+
// Measure assistant text output (used for token estimation and planning gate)
|
|
34
|
+
const textLen = event.assistantTexts.reduce((s, t) => s + t.length, 0);
|
|
35
|
+
|
|
36
|
+
// Extract token counts — fall back to text-length estimate when provider
|
|
37
|
+
// doesn't report usage (OpenClaw often passes 0 or undefined)
|
|
38
|
+
let inputTokens = event.usage?.input ?? 0;
|
|
39
|
+
let outputTokens = event.usage?.output ?? 0;
|
|
40
|
+
if (inputTokens + outputTokens === 0 && textLen > 0) {
|
|
41
|
+
outputTokens = Math.ceil(textLen / 4); // ~4 chars per token
|
|
42
|
+
}
|
|
36
43
|
|
|
37
44
|
// Always update session stats — turn_count must increment even without usage data
|
|
38
45
|
if (session.surrealSessionId) {
|
|
@@ -47,14 +54,11 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
// Accumulate for daemon batching
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
session.cumulativeTokens += inputTokens + outputTokens;
|
|
54
|
-
}
|
|
57
|
+
// Accumulate for daemon batching and mid-session cleanup
|
|
58
|
+
session.newContentTokens += inputTokens + outputTokens;
|
|
59
|
+
session.cumulativeTokens += inputTokens + outputTokens;
|
|
55
60
|
|
|
56
61
|
// Track accumulated text output for planning gate
|
|
57
|
-
const textLen = event.assistantTexts.reduce((s, t) => s + t.length, 0);
|
|
58
62
|
session.turnTextLength += textLen;
|
|
59
63
|
|
|
60
64
|
if (textLen > 50) {
|
|
@@ -78,6 +82,11 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
78
82
|
const thinking = block.thinking ?? block.text ?? "";
|
|
79
83
|
if (thinking.length > 50) {
|
|
80
84
|
session.pendingThinking.push(thinking);
|
|
85
|
+
// Cap to prevent unbounded growth in long sessions
|
|
86
|
+
const max = state.config.thresholds.maxPendingThinking;
|
|
87
|
+
if (session.pendingThinking.length > max) {
|
|
88
|
+
session.pendingThinking.splice(0, session.pendingThinking.length - max);
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
}
|
package/src/index.ts
CHANGED
|
@@ -463,6 +463,7 @@ export default definePluginEntry({
|
|
|
463
463
|
globalState!.embeddings,
|
|
464
464
|
session.sessionId,
|
|
465
465
|
globalState!.complete,
|
|
466
|
+
globalState!.config.thresholds.extractionTimeoutMs,
|
|
466
467
|
);
|
|
467
468
|
} catch (e) {
|
|
468
469
|
swallow.warn("index:startDaemon", e);
|
|
@@ -476,7 +477,7 @@ export default definePluginEntry({
|
|
|
476
477
|
(session as any)._hasMigratableFiles = true;
|
|
477
478
|
}
|
|
478
479
|
})
|
|
479
|
-
.catch(e => swallow("index:migrationCheck", e));
|
|
480
|
+
.catch(e => swallow.warn("index:migrationCheck", e));
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
// Set reflection context window from config
|
package/src/schema.surql
CHANGED
|
@@ -76,8 +76,6 @@ DEFINE FIELD IF NOT EXISTS model ON turn TYPE option<string>;
|
|
|
76
76
|
DEFINE FIELD IF NOT EXISTS usage ON turn TYPE option<object>;
|
|
77
77
|
DEFINE INDEX IF NOT EXISTS turn_vec_idx ON turn FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
78
78
|
DEFINE INDEX IF NOT EXISTS turn_session_idx ON turn FIELDS session_id;
|
|
79
|
-
-- Migration: backfill created_at from timestamp for existing turns
|
|
80
|
-
UPDATE turn SET created_at = timestamp WHERE created_at IS NONE AND timestamp IS NOT NONE;
|
|
81
79
|
|
|
82
80
|
-- Identity chunks (agent persona / identity)
|
|
83
81
|
DEFINE TABLE IF NOT EXISTS identity_chunk SCHEMALESS;
|
|
@@ -112,7 +110,6 @@ DEFINE FIELD IF NOT EXISTS source ON memory TYPE option<string>;
|
|
|
112
110
|
DEFINE FIELD IF NOT EXISTS created_at ON memory TYPE datetime DEFAULT time::now();
|
|
113
111
|
DEFINE FIELD IF NOT EXISTS last_accessed ON memory TYPE option<datetime>;
|
|
114
112
|
DEFINE FIELD IF NOT EXISTS status ON memory TYPE option<string> DEFAULT "active";
|
|
115
|
-
UPDATE memory SET status = "active" WHERE status IS NONE;
|
|
116
113
|
DEFINE FIELD IF NOT EXISTS resolved_at ON memory TYPE option<datetime>;
|
|
117
114
|
DEFINE FIELD IF NOT EXISTS resolved_by ON memory TYPE option<string>;
|
|
118
115
|
DEFINE INDEX IF NOT EXISTS memory_vec_idx ON memory FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
package/src/skills.ts
CHANGED
package/src/state.ts
CHANGED
|
@@ -54,13 +54,13 @@ export class SessionState {
|
|
|
54
54
|
// Memory daemon
|
|
55
55
|
daemon: MemoryDaemon | null = null;
|
|
56
56
|
newContentTokens = 0;
|
|
57
|
-
|
|
57
|
+
daemonTokenThreshold = 4000;
|
|
58
58
|
lastDaemonFlushTurnCount = 0;
|
|
59
59
|
|
|
60
60
|
// Cumulative session token tracking (for mid-session cleanup trigger)
|
|
61
61
|
cumulativeTokens = 0;
|
|
62
62
|
lastCleanupTokens = 0;
|
|
63
|
-
|
|
63
|
+
midSessionCleanupThreshold = 100_000;
|
|
64
64
|
|
|
65
65
|
// Cleanup tracking
|
|
66
66
|
cleanedUp = false;
|
|
@@ -106,6 +106,7 @@ export class GlobalPluginState {
|
|
|
106
106
|
complete: CompleteFn;
|
|
107
107
|
workspaceDir?: string;
|
|
108
108
|
enqueueSystemEvent?: EnqueueSystemEventFn;
|
|
109
|
+
schemaApplied = false;
|
|
109
110
|
private sessions = new Map<string, SessionState>();
|
|
110
111
|
|
|
111
112
|
constructor(
|
|
@@ -125,6 +126,8 @@ export class GlobalPluginState {
|
|
|
125
126
|
let session = this.sessions.get(sessionKey);
|
|
126
127
|
if (!session) {
|
|
127
128
|
session = new SessionState(sessionId, sessionKey);
|
|
129
|
+
session.daemonTokenThreshold = this.config.thresholds.daemonTokenThreshold;
|
|
130
|
+
session.midSessionCleanupThreshold = this.config.thresholds.midSessionCleanupThreshold;
|
|
128
131
|
this.sessions.set(sessionKey, session);
|
|
129
132
|
}
|
|
130
133
|
return session;
|
package/src/surreal.ts
CHANGED
|
@@ -1062,16 +1062,16 @@ export class SurrealStore {
|
|
|
1062
1062
|
|
|
1063
1063
|
async runMemoryMaintenance(): Promise<void> {
|
|
1064
1064
|
try {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
`UPDATE memory SET importance = math::max([importance, 3 + ((
|
|
1065
|
+
// Single round-trip to reduce transaction conflict window
|
|
1066
|
+
await this.queryExec(`
|
|
1067
|
+
UPDATE memory SET importance = math::max([importance * 0.95, 2.0]) WHERE importance > 2.0;
|
|
1068
|
+
UPDATE memory SET importance = math::max([importance, 3 + ((
|
|
1070
1069
|
SELECT VALUE avg_utilization FROM memory_utility_cache WHERE memory_id = string::concat(meta::tb(id), ":", meta::id(id)) LIMIT 1
|
|
1071
|
-
)[0] ?? 0) * 4]) WHERE importance < 7
|
|
1072
|
-
);
|
|
1070
|
+
)[0] ?? 0) * 4]) WHERE importance < 7;
|
|
1071
|
+
`);
|
|
1073
1072
|
} catch (e) {
|
|
1074
|
-
|
|
1073
|
+
// Transaction conflicts expected when daemon writes concurrently — silent
|
|
1074
|
+
swallow("surreal:runMemoryMaintenance", e);
|
|
1075
1075
|
}
|
|
1076
1076
|
}
|
|
1077
1077
|
|