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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kongbrain",
3
- "version": "0.3.1",
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
- if (raw.W_q[0].length !== ATTN_DIM || raw.W_k[0].length !== ATTN_DIM) return null;
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 ${id}->${edge}->? LIMIT 3`,
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 ${id}<-${edge}<-? LIMIT 3`,
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
  );
@@ -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 ${g.id} SET importance = math::max([3, importance - 2])`,
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 ${g.id} SET importance = math::min([9, importance + 1])`,
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 ${g.id} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
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 ${row[0].id} SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
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 : 100_000,
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:
@@ -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>;
@@ -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 response = await complete({
110
- system: systemPrompt,
111
- messages: [{ role: "user", content: `[TRANSCRIPT]\n${transcript.slice(0, 60000)}` }],
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 complete({
148
- 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.",
149
- messages: [{ role: "user", content: turnSummary }],
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(45_000).catch(e => swallow.warn("cleanup:daemonShutdown", e));
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, 45_000)),
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
  /**
@@ -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 ${memId} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
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) warmCache.delete(key);
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 ${skillId} SET
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 = 100_000;
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 e;
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(`RELATE ${fromId}->${safeName}->${toId}`);
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 ${sessionId} SET
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 ${sessionId} SET ended_at = time::now(), summary = $summary`,
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 ${sessionId} SET ended_at = time::now()`);
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 ${sessionId} SET cleanup_completed = false, last_active = time::now()`,
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 ${sessionId} SET ended_at = time::now(), cleanup_completed = true`,
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
- await this.queryExec(`RELATE ${sessionId}->session_task->${taskId}`);
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
- await this.queryExec(`RELATE ${taskId}->task_part_of->${projectId}`);
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
- await this.queryExec(`RELATE ${agentId}->performed->${taskId}`);
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
- await this.queryExec(`RELATE ${agentId}->owns->${projectId}`);
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 ${id}->${edge}->? LIMIT 3`, bindings).catch(
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 ${id}<-${edge}<-? LIMIT 3`, bindings).catch(
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 ${id} SET access_count += 1, last_accessed = time::now()`,
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(`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`);
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 ${existing.id} SET access_count += 1, importance = $imp, last_accessed = time::now()`,
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 ${id} SET ${sets.join(", ")} RETURN id`,
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(`UPDATE ${id} SET active = false, updated_at = time::now()`);
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 ${keep} SET access_count += 1, importance = math::max([importance, $imp])`,
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(`UPDATE ${mem.id} SET embedding = $emb`, { emb });
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 ${keep} SET access_count += 1, importance = math::max([importance, $imp])`,
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(`UPDATE ${checkpointId} SET status = "complete", memory_id = $mid`, {
1354
- mid: memoryId,
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(
@@ -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
@@ -297,7 +297,7 @@ Return ONLY valid JSON.`,
297
297
 
298
298
  const text = response.text;
299
299
 
300
- const jsonMatch = text.match(/\{[\s\S]*\}/);
300
+ const jsonMatch = text.match(/\{[\s\S]*?\}/);
301
301
  if (!jsonMatch) return null;
302
302
 
303
303
  let raw: any;
@@ -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 stat(absPath); }
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]);