kongbrain 0.3.1 → 0.3.3
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 +6 -1
- package/src/causal.ts +4 -4
- package/src/cognitive-check.ts +9 -7
- package/src/config.ts +1 -1
- package/src/daemon-manager.ts +1 -1
- package/src/deferred-cleanup.ts +19 -8
- package/src/index.ts +32 -32
- package/src/memory-daemon.ts +2 -2
- package/src/prefetch.ts +3 -1
- package/src/skills.ts +4 -4
- package/src/soul.ts +4 -2
- package/src/state.ts +1 -1
- package/src/surreal.ts +70 -33
- package/src/tools/introspect.ts +1 -1
- package/src/wakeup.ts +1 -1
- package/src/workspace-migrate.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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
|
@@ -94,7 +94,12 @@ function loadWeights(path: string): ACANWeights | null {
|
|
|
94
94
|
if (!Array.isArray(raw.W_k) || raw.W_k.length !== EMBED_DIM) return null;
|
|
95
95
|
if (!Array.isArray(raw.W_final) || raw.W_final.length !== FEATURE_COUNT) return null;
|
|
96
96
|
if (typeof raw.bias !== "number") return null;
|
|
97
|
-
|
|
97
|
+
// Validate inner dimensions — check first, middle, and last rows to catch crafted files
|
|
98
|
+
const checkIndices = [0, Math.floor(EMBED_DIM / 2), EMBED_DIM - 1];
|
|
99
|
+
for (const i of checkIndices) {
|
|
100
|
+
if (!Array.isArray(raw.W_q[i]) || raw.W_q[i].length !== ATTN_DIM) return null;
|
|
101
|
+
if (!Array.isArray(raw.W_k[i]) || raw.W_k[i].length !== ATTN_DIM) return null;
|
|
102
|
+
}
|
|
98
103
|
return raw as ACANWeights;
|
|
99
104
|
} catch (e) {
|
|
100
105
|
swallow("acan:loadWeights", e);
|
package/src/causal.ts
CHANGED
|
@@ -138,8 +138,8 @@ export async function queryCausalContext(
|
|
|
138
138
|
store.queryFirst<any>(
|
|
139
139
|
`SELECT id, text, importance, access_count AS accessCount,
|
|
140
140
|
created_at AS timestamp, category, meta::tb(id) AS table${scoreExpr}
|
|
141
|
-
FROM $
|
|
142
|
-
bindings,
|
|
141
|
+
FROM type::record($nid)->${edge}->? LIMIT 3`,
|
|
142
|
+
{ ...bindings, nid: id },
|
|
143
143
|
).catch(e => { swallow.warn("causal:edge-query", e); return [] as any[]; }),
|
|
144
144
|
),
|
|
145
145
|
);
|
|
@@ -149,8 +149,8 @@ export async function queryCausalContext(
|
|
|
149
149
|
store.queryFirst<any>(
|
|
150
150
|
`SELECT id, text, importance, access_count AS accessCount,
|
|
151
151
|
created_at AS timestamp, category, meta::tb(id) AS table${scoreExpr}
|
|
152
|
-
FROM $
|
|
153
|
-
bindings,
|
|
152
|
+
FROM type::record($nid)<-${edge}<-? LIMIT 3`,
|
|
153
|
+
{ ...bindings, nid: id },
|
|
154
154
|
).catch(e => { swallow.warn("causal:edge-query", e); return [] as any[]; }),
|
|
155
155
|
),
|
|
156
156
|
);
|
package/src/cognitive-check.ts
CHANGED
|
@@ -183,12 +183,14 @@ Return ONLY valid JSON.`,
|
|
|
183
183
|
if (g.learned) {
|
|
184
184
|
// Agent followed the correction unprompted — decay toward background (floor 3)
|
|
185
185
|
await store.queryExec(
|
|
186
|
-
`UPDATE $
|
|
186
|
+
`UPDATE type::record($gid) SET importance = math::max([3, importance - 2])`,
|
|
187
|
+
{ gid: g.id },
|
|
187
188
|
).catch(e => swallow.warn("cognitive-check:correctionDecay", e));
|
|
188
189
|
} else {
|
|
189
190
|
// Correction was relevant but agent ignored it — reinforce (cap 9)
|
|
190
191
|
await store.queryExec(
|
|
191
|
-
`UPDATE $
|
|
192
|
+
`UPDATE type::record($gid) SET importance = math::min([9, importance + 1])`,
|
|
193
|
+
{ gid: g.id },
|
|
192
194
|
).catch(e => swallow.warn("cognitive-check:correctionReinforce", e));
|
|
193
195
|
}
|
|
194
196
|
}
|
|
@@ -218,8 +220,8 @@ Return ONLY valid JSON.`,
|
|
|
218
220
|
const resolvedGrades = result.grades.filter(g => g.resolved && g.id.startsWith("memory:"));
|
|
219
221
|
for (const g of resolvedGrades) {
|
|
220
222
|
await store.queryExec(
|
|
221
|
-
`UPDATE $
|
|
222
|
-
{ sid: params.sessionId },
|
|
223
|
+
`UPDATE type::record($gid) SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
|
|
224
|
+
{ gid: g.id, sid: params.sessionId },
|
|
223
225
|
).catch(e => swallow.warn("cognitive-check:resolve", e));
|
|
224
226
|
}
|
|
225
227
|
} catch (e) {
|
|
@@ -234,7 +236,7 @@ Return ONLY valid JSON.`,
|
|
|
234
236
|
export function parseCheckResponse(text: string): CognitiveCheckResult | null {
|
|
235
237
|
// Strip markdown fences if present
|
|
236
238
|
const stripped = text.replace(/```(?:json)?\s*/g, "").replace(/```\s*$/g, "");
|
|
237
|
-
const jsonMatch = stripped.match(/\{[\s\S]
|
|
239
|
+
const jsonMatch = stripped.match(/\{[\s\S]*?\}/);
|
|
238
240
|
if (!jsonMatch) return null;
|
|
239
241
|
|
|
240
242
|
let raw: any;
|
|
@@ -316,8 +318,8 @@ async function applyRetrievalGrades(
|
|
|
316
318
|
);
|
|
317
319
|
if (row?.[0]?.id) {
|
|
318
320
|
await store.queryExec(
|
|
319
|
-
`UPDATE $
|
|
320
|
-
{ score: grade.score, relevant: grade.relevant, reason: grade.reason },
|
|
321
|
+
`UPDATE type::record($rid) SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
|
|
322
|
+
{ rid: String(row[0].id), score: grade.score, relevant: grade.relevant, reason: grade.reason },
|
|
321
323
|
);
|
|
322
324
|
}
|
|
323
325
|
// Feed relevance score into the utility cache — drives WMR provenUtility scoring
|
package/src/config.ts
CHANGED
|
@@ -79,7 +79,7 @@ export function parsePluginConfig(raw?: Record<string, unknown>): KongBrainConfi
|
|
|
79
79
|
daemonTokenThreshold:
|
|
80
80
|
typeof thresholds.daemonTokenThreshold === "number" ? thresholds.daemonTokenThreshold : 4000,
|
|
81
81
|
midSessionCleanupThreshold:
|
|
82
|
-
typeof thresholds.midSessionCleanupThreshold === "number" ? thresholds.midSessionCleanupThreshold :
|
|
82
|
+
typeof thresholds.midSessionCleanupThreshold === "number" ? thresholds.midSessionCleanupThreshold : 25_000,
|
|
83
83
|
extractionTimeoutMs:
|
|
84
84
|
typeof thresholds.extractionTimeoutMs === "number" ? thresholds.extractionTimeoutMs : 60_000,
|
|
85
85
|
maxPendingThinking:
|
package/src/daemon-manager.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function startMemoryDaemon(
|
|
|
97
97
|
|
|
98
98
|
const responseText = response.text;
|
|
99
99
|
|
|
100
|
-
const jsonMatch = responseText.match(/\{[\s\S]
|
|
100
|
+
const jsonMatch = responseText.match(/\{[\s\S]*?\}/);
|
|
101
101
|
if (!jsonMatch) return;
|
|
102
102
|
|
|
103
103
|
let result: Record<string, any>;
|
package/src/deferred-cleanup.ts
CHANGED
|
@@ -106,10 +106,16 @@ async function processOrphanedSession(
|
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
108
|
console.warn(`[deferred] extracting session ${surrealSessionId} (${turns.length} turns, transcript ${transcript.length} chars)`);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
const LLM_CALL_TIMEOUT_MS = 30_000;
|
|
110
|
+
const response = await Promise.race([
|
|
111
|
+
complete({
|
|
112
|
+
system: systemPrompt,
|
|
113
|
+
messages: [{ role: "user", content: `[TRANSCRIPT]\n${transcript.slice(0, 60000)}` }],
|
|
114
|
+
}),
|
|
115
|
+
new Promise<never>((_, reject) =>
|
|
116
|
+
setTimeout(() => reject(new Error("LLM extraction call timed out")), LLM_CALL_TIMEOUT_MS),
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
113
119
|
|
|
114
120
|
const responseText = response.text;
|
|
115
121
|
console.warn(`[deferred] extraction response: ${responseText.length} chars`);
|
|
@@ -144,10 +150,15 @@ async function processOrphanedSession(
|
|
|
144
150
|
.map(t => `[${t.role}] ${t.text.slice(0, 200)}`)
|
|
145
151
|
.join("\n");
|
|
146
152
|
|
|
147
|
-
const handoffResponse = await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
const handoffResponse = await Promise.race([
|
|
154
|
+
complete({
|
|
155
|
+
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.",
|
|
156
|
+
messages: [{ role: "user", content: turnSummary }],
|
|
157
|
+
}),
|
|
158
|
+
new Promise<never>((_, reject) =>
|
|
159
|
+
setTimeout(() => reject(new Error("LLM handoff call timed out")), 30_000),
|
|
160
|
+
),
|
|
161
|
+
]);
|
|
151
162
|
|
|
152
163
|
const handoffText = handoffResponse.text.trim();
|
|
153
164
|
console.warn(`[deferred] handoff response: ${handoffText.length} chars`);
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,36 @@ async function runSessionCleanup(
|
|
|
65
65
|
state: GlobalPluginState,
|
|
66
66
|
): Promise<void> {
|
|
67
67
|
const { store: s, embeddings: emb } = state;
|
|
68
|
+
const { complete } = state;
|
|
69
|
+
|
|
70
|
+
// 1. Handoff FIRST — highest value, must survive even if cleanup races out
|
|
71
|
+
try {
|
|
72
|
+
const recentTurns = await s.getSessionTurns(session.sessionId, 15)
|
|
73
|
+
.catch(() => [] as { role: string; text: string }[]);
|
|
74
|
+
if (recentTurns.length >= 2) {
|
|
75
|
+
const turnSummary = recentTurns
|
|
76
|
+
.map(t => `[${t.role}] ${t.text.slice(0, 200)}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
|
|
79
|
+
const handoffResponse = await complete({
|
|
80
|
+
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.",
|
|
81
|
+
messages: [{ role: "user", content: turnSummary }],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const handoffText = handoffResponse.text.trim();
|
|
85
|
+
if (handoffText.length > 20) {
|
|
86
|
+
let embedding: number[] | null = null;
|
|
87
|
+
if (emb.isAvailable()) {
|
|
88
|
+
try { embedding = await emb.embed(handoffText); } catch { /* ok */ }
|
|
89
|
+
}
|
|
90
|
+
await s.createMemory(handoffText, embedding, 8, "handoff", session.sessionId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
swallow.warn("cleanup:handoff", e);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Everything else in parallel — lower priority, OK if timeout kills it
|
|
68
98
|
const endOps: Promise<unknown>[] = [];
|
|
69
99
|
|
|
70
100
|
// Final daemon flush — send full session for extraction
|
|
@@ -80,14 +110,12 @@ async function runSessionCleanup(
|
|
|
80
110
|
}));
|
|
81
111
|
session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
|
|
82
112
|
} catch (e) { swallow.warn("cleanup:finalDaemonFlush", e); }
|
|
83
|
-
await session.daemon!.shutdown(
|
|
113
|
+
await session.daemon!.shutdown(10_000).catch(e => swallow.warn("cleanup:daemonShutdown", e));
|
|
84
114
|
session.daemon = null;
|
|
85
115
|
})(),
|
|
86
116
|
);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
|
-
const { complete } = state;
|
|
90
|
-
|
|
91
119
|
// Skill extraction
|
|
92
120
|
if (session.taskId) {
|
|
93
121
|
endOps.push(
|
|
@@ -113,10 +141,9 @@ async function runSessionCleanup(
|
|
|
113
141
|
.catch(e => { swallow.warn("cleanup:soulGraduation", e); return null; });
|
|
114
142
|
endOps.push(graduationPromise);
|
|
115
143
|
|
|
116
|
-
// The session-end LLM call is critical and needs the full 45s.
|
|
117
144
|
await Promise.race([
|
|
118
145
|
Promise.allSettled(endOps),
|
|
119
|
-
new Promise(resolve => setTimeout(resolve,
|
|
146
|
+
new Promise(resolve => setTimeout(resolve, 150_000)),
|
|
120
147
|
]);
|
|
121
148
|
|
|
122
149
|
// If soul graduation just happened, persist a graduation event so the next
|
|
@@ -192,33 +219,6 @@ async function runSessionCleanup(
|
|
|
192
219
|
} catch (e) {
|
|
193
220
|
swallow.warn("cleanup:stageTransition", e);
|
|
194
221
|
}
|
|
195
|
-
|
|
196
|
-
// Generate handoff note for next session wakeup
|
|
197
|
-
try {
|
|
198
|
-
const recentTurns = await s.getSessionTurns(session.sessionId, 15)
|
|
199
|
-
.catch(() => [] as { role: string; text: string }[]);
|
|
200
|
-
if (recentTurns.length >= 2) {
|
|
201
|
-
const turnSummary = recentTurns
|
|
202
|
-
.map(t => `[${t.role}] ${t.text.slice(0, 200)}`)
|
|
203
|
-
.join("\n");
|
|
204
|
-
|
|
205
|
-
const handoffResponse = await complete({
|
|
206
|
-
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.",
|
|
207
|
-
messages: [{ role: "user", content: turnSummary }],
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const handoffText = handoffResponse.text.trim();
|
|
211
|
-
if (handoffText.length > 20) {
|
|
212
|
-
let embedding: number[] | null = null;
|
|
213
|
-
if (emb.isAvailable()) {
|
|
214
|
-
try { embedding = await emb.embed(handoffText); } catch { /* ok */ }
|
|
215
|
-
}
|
|
216
|
-
await s.createMemory(handoffText, embedding, 8, "handoff", session.sessionId);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (e) {
|
|
220
|
-
swallow.warn("cleanup:handoff", e);
|
|
221
|
-
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
package/src/memory-daemon.ts
CHANGED
|
@@ -174,8 +174,8 @@ export async function writeExtractionResults(
|
|
|
174
174
|
if (typeof memId !== "string" || !RECORD_ID_RE.test(memId)) continue;
|
|
175
175
|
counts.resolved++;
|
|
176
176
|
await store.queryExec(
|
|
177
|
-
`UPDATE $
|
|
178
|
-
{ sid: sessionId },
|
|
177
|
+
`UPDATE type::record($mid) SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
|
|
178
|
+
{ mid: memId, sid: sessionId },
|
|
179
179
|
).catch(e => swallow.warn("daemon:resolveMemory", e));
|
|
180
180
|
}
|
|
181
181
|
})());
|
package/src/prefetch.ts
CHANGED
|
@@ -46,9 +46,11 @@ export function getPrefetchHitRate(): { hits: number; misses: number; attempts:
|
|
|
46
46
|
|
|
47
47
|
function evictStale(): void {
|
|
48
48
|
const now = Date.now();
|
|
49
|
+
const staleKeys: string[] = [];
|
|
49
50
|
for (const [key, entry] of warmCache) {
|
|
50
|
-
if (now - entry.timestamp > CACHE_TTL_MS)
|
|
51
|
+
if (now - entry.timestamp > CACHE_TTL_MS) staleKeys.push(key);
|
|
51
52
|
}
|
|
53
|
+
for (const key of staleKeys) warmCache.delete(key);
|
|
52
54
|
while (warmCache.size > MAX_CACHE_SIZE) {
|
|
53
55
|
const oldest = warmCache.keys().next().value;
|
|
54
56
|
if (oldest) warmCache.delete(oldest);
|
package/src/skills.ts
CHANGED
|
@@ -80,7 +80,7 @@ export async function extractSkill(
|
|
|
80
80
|
|
|
81
81
|
if (text.trim() === "null" || text.trim() === "None") return null;
|
|
82
82
|
|
|
83
|
-
const jsonMatch = text.match(/\{[\s\S]
|
|
83
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
84
84
|
if (!jsonMatch) return null;
|
|
85
85
|
|
|
86
86
|
const parsed = JSON.parse(jsonMatch[0]) as ExtractedSkill;
|
|
@@ -238,11 +238,11 @@ export async function recordSkillOutcome(
|
|
|
238
238
|
try {
|
|
239
239
|
const field = success ? "success_count" : "failure_count";
|
|
240
240
|
await store.queryExec(
|
|
241
|
-
`UPDATE $
|
|
241
|
+
`UPDATE type::record($sid) SET
|
|
242
242
|
${field} += 1,
|
|
243
243
|
avg_duration_ms = (avg_duration_ms * (success_count + failure_count - 1) + $dur) / (success_count + failure_count),
|
|
244
244
|
last_used = time::now()`,
|
|
245
|
-
{ dur: durationMs },
|
|
245
|
+
{ sid: skillId, dur: durationMs },
|
|
246
246
|
);
|
|
247
247
|
} catch (e) { swallow("skills:non-critical", e); }
|
|
248
248
|
}
|
|
@@ -289,7 +289,7 @@ export async function graduateCausalToSkills(
|
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
const text = resp.text;
|
|
292
|
-
const jsonMatch = text.match(/\{[\s\S]
|
|
292
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
293
293
|
if (!jsonMatch) continue;
|
|
294
294
|
|
|
295
295
|
let parsed: ExtractedSkill;
|
package/src/soul.ts
CHANGED
|
@@ -457,6 +457,8 @@ export async function reviseSoul(
|
|
|
457
457
|
store: SurrealStore,
|
|
458
458
|
): Promise<boolean> {
|
|
459
459
|
if (!store.isAvailable()) return false;
|
|
460
|
+
const ALLOWED_SECTIONS = new Set(["working_style", "emotional_dimensions", "self_observations", "earned_values"]);
|
|
461
|
+
if (!ALLOWED_SECTIONS.has(section)) return false;
|
|
460
462
|
try {
|
|
461
463
|
const now = new Date().toISOString();
|
|
462
464
|
await store.queryExec(
|
|
@@ -567,7 +569,7 @@ Be honest, not aspirational. Only claim what the data supports.`;
|
|
|
567
569
|
});
|
|
568
570
|
|
|
569
571
|
const text = response.text.trim();
|
|
570
|
-
const jsonMatch = text.match(/\{[\s\S]
|
|
572
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
571
573
|
if (!jsonMatch) return null;
|
|
572
574
|
|
|
573
575
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
@@ -842,7 +844,7 @@ CURRENT QUALITY:
|
|
|
842
844
|
});
|
|
843
845
|
|
|
844
846
|
const text = response.text.trim();
|
|
845
|
-
const jsonMatch = text.match(/\{[\s\S]
|
|
847
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
846
848
|
if (!jsonMatch) return false;
|
|
847
849
|
|
|
848
850
|
const revisions = JSON.parse(jsonMatch[0]);
|
package/src/state.ts
CHANGED
|
@@ -60,7 +60,7 @@ export class SessionState {
|
|
|
60
60
|
// Cumulative session token tracking (for mid-session cleanup trigger)
|
|
61
61
|
cumulativeTokens = 0;
|
|
62
62
|
lastCleanupTokens = 0;
|
|
63
|
-
midSessionCleanupThreshold =
|
|
63
|
+
midSessionCleanupThreshold = 25_000;
|
|
64
64
|
|
|
65
65
|
// Cleanup tracking
|
|
66
66
|
cleanedUp = false;
|
package/src/surreal.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class SurrealStore {
|
|
|
168
168
|
await new Promise((r) => setTimeout(r, BACKOFF_MS[attempt - 1]));
|
|
169
169
|
} else {
|
|
170
170
|
console.error(`[ERROR] SurrealDB reconnection failed after ${MAX_ATTEMPTS} attempts.`);
|
|
171
|
-
throw
|
|
171
|
+
throw new Error("SurrealDB reconnection failed");
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -447,7 +447,10 @@ export class SurrealStore {
|
|
|
447
447
|
assertRecordId(fromId);
|
|
448
448
|
assertRecordId(toId);
|
|
449
449
|
const safeName = edge.replace(/[^a-zA-Z0-9_]/g, "");
|
|
450
|
-
await this.queryExec(
|
|
450
|
+
await this.queryExec(
|
|
451
|
+
`RELATE type::record($from)->${safeName}->type::record($to)`,
|
|
452
|
+
{ from: fromId, to: toId },
|
|
453
|
+
);
|
|
451
454
|
}
|
|
452
455
|
|
|
453
456
|
// ── 5-Pillar entity operations ─────────────────────────────────────────
|
|
@@ -501,12 +504,12 @@ export class SurrealStore {
|
|
|
501
504
|
): Promise<void> {
|
|
502
505
|
assertRecordId(sessionId);
|
|
503
506
|
await this.queryExec(
|
|
504
|
-
`UPDATE $
|
|
507
|
+
`UPDATE type::record($sid) SET
|
|
505
508
|
turn_count += 1,
|
|
506
509
|
total_input_tokens += $input,
|
|
507
510
|
total_output_tokens += $output,
|
|
508
511
|
last_active = time::now()`,
|
|
509
|
-
{ input: inputTokens, output: outputTokens },
|
|
512
|
+
{ sid: sessionId, input: inputTokens, output: outputTokens },
|
|
510
513
|
);
|
|
511
514
|
}
|
|
512
515
|
|
|
@@ -514,25 +517,27 @@ export class SurrealStore {
|
|
|
514
517
|
assertRecordId(sessionId);
|
|
515
518
|
if (summary) {
|
|
516
519
|
await this.queryExec(
|
|
517
|
-
`UPDATE $
|
|
518
|
-
{ summary },
|
|
520
|
+
`UPDATE type::record($sid) SET ended_at = time::now(), summary = $summary`,
|
|
521
|
+
{ sid: sessionId, summary },
|
|
519
522
|
);
|
|
520
523
|
} else {
|
|
521
|
-
await this.queryExec(`UPDATE $
|
|
524
|
+
await this.queryExec(`UPDATE type::record($sid) SET ended_at = time::now()`, { sid: sessionId });
|
|
522
525
|
}
|
|
523
526
|
}
|
|
524
527
|
|
|
525
528
|
async markSessionActive(sessionId: string): Promise<void> {
|
|
526
529
|
assertRecordId(sessionId);
|
|
527
530
|
await this.queryExec(
|
|
528
|
-
`UPDATE $
|
|
531
|
+
`UPDATE type::record($sid) SET cleanup_completed = false, last_active = time::now()`,
|
|
532
|
+
{ sid: sessionId },
|
|
529
533
|
);
|
|
530
534
|
}
|
|
531
535
|
|
|
532
536
|
async markSessionEnded(sessionId: string): Promise<void> {
|
|
533
537
|
assertRecordId(sessionId);
|
|
534
538
|
await this.queryExec(
|
|
535
|
-
`UPDATE $
|
|
539
|
+
`UPDATE type::record($sid) SET ended_at = time::now(), cleanup_completed = true`,
|
|
540
|
+
{ sid: sessionId },
|
|
536
541
|
);
|
|
537
542
|
}
|
|
538
543
|
|
|
@@ -547,19 +552,39 @@ export class SurrealStore {
|
|
|
547
552
|
}
|
|
548
553
|
|
|
549
554
|
async linkSessionToTask(sessionId: string, taskId: string): Promise<void> {
|
|
550
|
-
|
|
555
|
+
assertRecordId(sessionId);
|
|
556
|
+
assertRecordId(taskId);
|
|
557
|
+
await this.queryExec(
|
|
558
|
+
`RELATE type::record($from)->session_task->type::record($to)`,
|
|
559
|
+
{ from: sessionId, to: taskId },
|
|
560
|
+
);
|
|
551
561
|
}
|
|
552
562
|
|
|
553
563
|
async linkTaskToProject(taskId: string, projectId: string): Promise<void> {
|
|
554
|
-
|
|
564
|
+
assertRecordId(taskId);
|
|
565
|
+
assertRecordId(projectId);
|
|
566
|
+
await this.queryExec(
|
|
567
|
+
`RELATE type::record($from)->task_part_of->type::record($to)`,
|
|
568
|
+
{ from: taskId, to: projectId },
|
|
569
|
+
);
|
|
555
570
|
}
|
|
556
571
|
|
|
557
572
|
async linkAgentToTask(agentId: string, taskId: string): Promise<void> {
|
|
558
|
-
|
|
573
|
+
assertRecordId(agentId);
|
|
574
|
+
assertRecordId(taskId);
|
|
575
|
+
await this.queryExec(
|
|
576
|
+
`RELATE type::record($from)->performed->type::record($to)`,
|
|
577
|
+
{ from: agentId, to: taskId },
|
|
578
|
+
);
|
|
559
579
|
}
|
|
560
580
|
|
|
561
581
|
async linkAgentToProject(agentId: string, projectId: string): Promise<void> {
|
|
562
|
-
|
|
582
|
+
assertRecordId(agentId);
|
|
583
|
+
assertRecordId(projectId);
|
|
584
|
+
await this.queryExec(
|
|
585
|
+
`RELATE type::record($from)->owns->type::record($to)`,
|
|
586
|
+
{ from: agentId, to: projectId },
|
|
587
|
+
);
|
|
563
588
|
}
|
|
564
589
|
|
|
565
590
|
// ── Graph traversal ────────────────────────────────────────────────────
|
|
@@ -600,7 +625,7 @@ export class SurrealStore {
|
|
|
600
625
|
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
601
626
|
const forwardQueries = frontier.flatMap((id) =>
|
|
602
627
|
forwardEdges.map((edge) =>
|
|
603
|
-
this.queryFirst<any>(`${selectFields} FROM $
|
|
628
|
+
this.queryFirst<any>(`${selectFields} FROM type::record($nid)->${edge}->? LIMIT 3`, { ...bindings, nid: id }).catch(
|
|
604
629
|
(e) => {
|
|
605
630
|
swallow.warn("surreal:graphExpand", e);
|
|
606
631
|
return [] as Record<string, unknown>[];
|
|
@@ -611,7 +636,7 @@ export class SurrealStore {
|
|
|
611
636
|
|
|
612
637
|
const reverseQueries = frontier.flatMap((id) =>
|
|
613
638
|
reverseEdges.map((edge) =>
|
|
614
|
-
this.queryFirst<any>(`${selectFields} FROM $
|
|
639
|
+
this.queryFirst<any>(`${selectFields} FROM type::record($nid)<-${edge}<-? LIMIT 3`, { ...bindings, nid: id }).catch(
|
|
615
640
|
(e) => {
|
|
616
641
|
swallow.warn("surreal:graphExpand", e);
|
|
617
642
|
return [] as Record<string, unknown>[];
|
|
@@ -662,7 +687,8 @@ export class SurrealStore {
|
|
|
662
687
|
try {
|
|
663
688
|
assertRecordId(id);
|
|
664
689
|
await this.queryExec(
|
|
665
|
-
`UPDATE $
|
|
690
|
+
`UPDATE type::record($rid) SET access_count += 1, last_accessed = time::now()`,
|
|
691
|
+
{ rid: id },
|
|
666
692
|
);
|
|
667
693
|
} catch (e) {
|
|
668
694
|
swallow.warn("surreal:bumpAccessCounts", e);
|
|
@@ -683,7 +709,10 @@ export class SurrealStore {
|
|
|
683
709
|
);
|
|
684
710
|
if (rows.length > 0) {
|
|
685
711
|
const id = String(rows[0].id);
|
|
686
|
-
await this.queryExec(
|
|
712
|
+
await this.queryExec(
|
|
713
|
+
`UPDATE type::record($cid) SET access_count += 1, last_accessed = time::now()`,
|
|
714
|
+
{ cid: id },
|
|
715
|
+
);
|
|
687
716
|
return id;
|
|
688
717
|
}
|
|
689
718
|
const emb = embedding?.length ? embedding : undefined;
|
|
@@ -739,8 +768,8 @@ export class SurrealStore {
|
|
|
739
768
|
const existing = dupes[0];
|
|
740
769
|
const newImp = Math.max(existing.importance ?? 0, importance);
|
|
741
770
|
await this.queryExec(
|
|
742
|
-
`UPDATE $
|
|
743
|
-
{ imp: newImp },
|
|
771
|
+
`UPDATE type::record($eid) SET access_count += 1, importance = $imp, last_accessed = time::now()`,
|
|
772
|
+
{ eid: String(existing.id), imp: newImp },
|
|
744
773
|
);
|
|
745
774
|
return String(existing.id);
|
|
746
775
|
}
|
|
@@ -813,10 +842,11 @@ export class SurrealStore {
|
|
|
813
842
|
fields: Partial<Pick<CoreMemoryEntry, "text" | "category" | "priority" | "tier" | "active">>,
|
|
814
843
|
): Promise<boolean> {
|
|
815
844
|
assertRecordId(id);
|
|
845
|
+
const ALLOWED_FIELDS = new Set(["text", "category", "priority", "tier", "active"]);
|
|
816
846
|
const sets: string[] = [];
|
|
817
|
-
const bindings: Record<string, unknown> = {};
|
|
847
|
+
const bindings: Record<string, unknown> = { _rid: id };
|
|
818
848
|
for (const [key, val] of Object.entries(fields)) {
|
|
819
|
-
if (val !== undefined) {
|
|
849
|
+
if (val !== undefined && ALLOWED_FIELDS.has(key)) {
|
|
820
850
|
sets.push(`${key} = $${key}`);
|
|
821
851
|
bindings[key] = val;
|
|
822
852
|
}
|
|
@@ -824,7 +854,7 @@ export class SurrealStore {
|
|
|
824
854
|
if (sets.length === 0) return false;
|
|
825
855
|
sets.push("updated_at = time::now()");
|
|
826
856
|
const rows = await this.queryFirst<{ id: string }>(
|
|
827
|
-
`UPDATE $
|
|
857
|
+
`UPDATE type::record($_rid) SET ${sets.join(", ")} RETURN id`,
|
|
828
858
|
bindings,
|
|
829
859
|
);
|
|
830
860
|
return rows.length > 0;
|
|
@@ -832,7 +862,10 @@ export class SurrealStore {
|
|
|
832
862
|
|
|
833
863
|
async deleteCoreMemory(id: string): Promise<void> {
|
|
834
864
|
assertRecordId(id);
|
|
835
|
-
await this.queryExec(
|
|
865
|
+
await this.queryExec(
|
|
866
|
+
`UPDATE type::record($rid) SET active = false, updated_at = time::now()`,
|
|
867
|
+
{ rid: id },
|
|
868
|
+
);
|
|
836
869
|
}
|
|
837
870
|
|
|
838
871
|
async deactivateSessionMemories(sessionId: string): Promise<void> {
|
|
@@ -1190,10 +1223,10 @@ export class SurrealStore {
|
|
|
1190
1223
|
assertRecordId(String(keep));
|
|
1191
1224
|
assertRecordId(String(drop));
|
|
1192
1225
|
await this.queryExec(
|
|
1193
|
-
`UPDATE $
|
|
1194
|
-
{ imp: dupe.importance },
|
|
1226
|
+
`UPDATE type::record($kid) SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1227
|
+
{ kid: String(keep), imp: dupe.importance },
|
|
1195
1228
|
);
|
|
1196
|
-
await this.queryExec(`DELETE ${drop}
|
|
1229
|
+
await this.queryExec(`DELETE type::record($did)`, { did: String(drop) });
|
|
1197
1230
|
seen.add(String(drop));
|
|
1198
1231
|
merged++;
|
|
1199
1232
|
}
|
|
@@ -1218,7 +1251,10 @@ export class SurrealStore {
|
|
|
1218
1251
|
try {
|
|
1219
1252
|
const emb = await embedFn(mem.text);
|
|
1220
1253
|
if (!emb) continue;
|
|
1221
|
-
await this.queryExec(
|
|
1254
|
+
await this.queryExec(
|
|
1255
|
+
`UPDATE type::record($mid) SET embedding = $emb`,
|
|
1256
|
+
{ mid: String(mem.id), emb },
|
|
1257
|
+
);
|
|
1222
1258
|
|
|
1223
1259
|
const dupes = await this.queryFirst<{
|
|
1224
1260
|
id: string;
|
|
@@ -1247,10 +1283,10 @@ export class SurrealStore {
|
|
|
1247
1283
|
assertRecordId(String(keep));
|
|
1248
1284
|
assertRecordId(String(drop));
|
|
1249
1285
|
await this.queryExec(
|
|
1250
|
-
`UPDATE $
|
|
1251
|
-
{ imp: dupe.importance },
|
|
1286
|
+
`UPDATE type::record($kid) SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1287
|
+
{ kid: String(keep), imp: dupe.importance },
|
|
1252
1288
|
);
|
|
1253
|
-
await this.queryExec(`DELETE ${drop}
|
|
1289
|
+
await this.queryExec(`DELETE type::record($did)`, { did: String(drop) });
|
|
1254
1290
|
seen.add(String(drop));
|
|
1255
1291
|
merged++;
|
|
1256
1292
|
}
|
|
@@ -1350,9 +1386,10 @@ export class SurrealStore {
|
|
|
1350
1386
|
memoryId: string,
|
|
1351
1387
|
): Promise<void> {
|
|
1352
1388
|
assertRecordId(checkpointId);
|
|
1353
|
-
await this.queryExec(
|
|
1354
|
-
|
|
1355
|
-
|
|
1389
|
+
await this.queryExec(
|
|
1390
|
+
`UPDATE type::record($cpid) SET status = "complete", memory_id = $mid`,
|
|
1391
|
+
{ cpid: checkpointId, mid: memoryId },
|
|
1392
|
+
);
|
|
1356
1393
|
}
|
|
1357
1394
|
|
|
1358
1395
|
async getPendingCheckpoints(
|
package/src/tools/introspect.ts
CHANGED
|
@@ -207,7 +207,7 @@ async function verifyAction(store: any, recordId?: string) {
|
|
|
207
207
|
return { content: [{ type: "text" as const, text: `Error: invalid record ID "${recordId}".` }], details: null };
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
const rows = await store.queryFirst(`SELECT * FROM ${recordId}
|
|
210
|
+
const rows = await store.queryFirst(`SELECT * FROM type::record($rid)`, { rid: recordId });
|
|
211
211
|
if (rows.length === 0) {
|
|
212
212
|
return { content: [{ type: "text" as const, text: `Record not found: ${recordId}` }], details: { exists: false } };
|
|
213
213
|
}
|
package/src/wakeup.ts
CHANGED
package/src/workspace-migrate.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* copyFile+unlink instead of rename (cross-filesystem safe).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { readFile, readdir, stat, copyFile, unlink, mkdir, writeFile, rmdir } from "node:fs/promises";
|
|
25
|
+
import { readFile, readdir, stat, lstat, copyFile, unlink, mkdir, writeFile, rmdir } from "node:fs/promises";
|
|
26
26
|
import { join, basename, extname, relative, dirname, sep } from "node:path";
|
|
27
27
|
import type { SurrealStore } from "./surreal.js";
|
|
28
28
|
import type { EmbeddingService } from "./embeddings.js";
|
|
@@ -376,10 +376,10 @@ async function tryReadFile(absPath: string, rootDir: string): Promise<WorkspaceF
|
|
|
376
376
|
if (SKIP_FILES.has(name)) return null;
|
|
377
377
|
|
|
378
378
|
let s;
|
|
379
|
-
try { s = await
|
|
379
|
+
try { s = await lstat(absPath); }
|
|
380
380
|
catch { return null; }
|
|
381
381
|
|
|
382
|
-
if (!s.isFile()) return null;
|
|
382
|
+
if (s.isSymbolicLink() || !s.isFile()) return null;
|
|
383
383
|
if (s.size === 0 || s.size > MAX_FILE_SIZE) return null;
|
|
384
384
|
|
|
385
385
|
try {
|
|
@@ -524,7 +524,7 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, unknow
|
|
|
524
524
|
}
|
|
525
525
|
|
|
526
526
|
// Try JSON metadata block if present
|
|
527
|
-
const jsonMatch = fmBlock.match(/metadata:\s*\n\s*(\{[\s\S]
|
|
527
|
+
const jsonMatch = fmBlock.match(/metadata:\s*\n\s*(\{[\s\S]*?\})/);
|
|
528
528
|
if (jsonMatch) {
|
|
529
529
|
try {
|
|
530
530
|
result.metadata = JSON.parse(jsonMatch[1]);
|