kongbrain 0.3.5 → 0.3.9
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/.clawhubignore +5 -0
- package/README.github.md +414 -0
- package/README.md +9 -13
- package/README.npm.md +400 -0
- package/SKILL.md +1 -1
- package/package.json +3 -1
- package/src/acan.ts +4 -2
- package/src/causal.ts +17 -12
- package/src/cognitive-bootstrap.ts +159 -0
- package/src/cognitive-check.ts +16 -8
- package/src/concept-extract.ts +113 -0
- package/src/context-engine.ts +12 -35
- package/src/hooks/after-tool-call.ts +19 -4
- package/src/index.ts +8 -0
- package/src/memory-daemon.ts +26 -7
- package/src/skills.ts +5 -2
- package/src/state.ts +1 -0
- package/src/surreal.ts +34 -45
- package/src/tools/introspect.ts +2 -1
package/src/cognitive-check.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { CompleteFn, SessionState } from "./state.js";
|
|
12
12
|
import type { SurrealStore } from "./surreal.js";
|
|
13
13
|
import { swallow } from "./errors.js";
|
|
14
|
+
import { assertRecordId } from "./surreal.js";
|
|
14
15
|
|
|
15
16
|
// --- Types ---
|
|
16
17
|
|
|
@@ -182,15 +183,17 @@ Return ONLY valid JSON.`,
|
|
|
182
183
|
for (const g of correctionGrades) {
|
|
183
184
|
if (g.learned) {
|
|
184
185
|
// Agent followed the correction unprompted — decay toward background (floor 3)
|
|
186
|
+
assertRecordId(g.id);
|
|
187
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
185
188
|
await store.queryExec(
|
|
186
|
-
`UPDATE
|
|
187
|
-
{ gid: g.id },
|
|
189
|
+
`UPDATE ${g.id} SET importance = math::max([3, importance - 2])`,
|
|
188
190
|
).catch(e => swallow.warn("cognitive-check:correctionDecay", e));
|
|
189
191
|
} else {
|
|
190
192
|
// Correction was relevant but agent ignored it — reinforce (cap 9)
|
|
193
|
+
assertRecordId(g.id);
|
|
194
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
191
195
|
await store.queryExec(
|
|
192
|
-
`UPDATE
|
|
193
|
-
{ gid: g.id },
|
|
196
|
+
`UPDATE ${g.id} SET importance = math::min([9, importance + 1])`,
|
|
194
197
|
).catch(e => swallow.warn("cognitive-check:correctionReinforce", e));
|
|
195
198
|
}
|
|
196
199
|
}
|
|
@@ -219,9 +222,11 @@ Return ONLY valid JSON.`,
|
|
|
219
222
|
// Mid-session resolution — mark addressed memories immediately
|
|
220
223
|
const resolvedGrades = result.grades.filter(g => g.resolved && g.id.startsWith("memory:"));
|
|
221
224
|
for (const g of resolvedGrades) {
|
|
225
|
+
assertRecordId(g.id);
|
|
226
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
222
227
|
await store.queryExec(
|
|
223
|
-
`UPDATE
|
|
224
|
-
{
|
|
228
|
+
`UPDATE ${g.id} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
|
|
229
|
+
{ sid: params.sessionId },
|
|
225
230
|
).catch(e => swallow.warn("cognitive-check:resolve", e));
|
|
226
231
|
}
|
|
227
232
|
} catch (e) {
|
|
@@ -317,9 +322,12 @@ async function applyRetrievalGrades(
|
|
|
317
322
|
{ id: grade.id, sid: sessionId },
|
|
318
323
|
);
|
|
319
324
|
if (row?.[0]?.id) {
|
|
325
|
+
const rid = String(row[0].id);
|
|
326
|
+
assertRecordId(rid);
|
|
327
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
320
328
|
await store.queryExec(
|
|
321
|
-
`UPDATE
|
|
322
|
-
{
|
|
329
|
+
`UPDATE ${rid} SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
|
|
330
|
+
{ score: grade.score, relevant: grade.relevant, reason: grade.reason },
|
|
323
331
|
);
|
|
324
332
|
}
|
|
325
333
|
// Feed relevance score into the utility cache — drives WMR provenUtility scoring
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared concept-extraction helpers.
|
|
3
|
+
*
|
|
4
|
+
* Regex-based extraction of concept names from text, plus helpers to
|
|
5
|
+
* upsert extracted concepts and link them via arbitrary edge types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SurrealStore } from "./surreal.js";
|
|
9
|
+
import type { EmbeddingService } from "./embeddings.js";
|
|
10
|
+
import { swallow } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
// Same regexes used by the original extractAndLinkConcepts in context-engine.
|
|
13
|
+
export const CONCEPT_RE = /\b(?:(?:use|using|implement|create|add|configure|setup|install|import)\s+)([A-Z][a-zA-Z0-9_-]+(?:\s+[A-Z][a-zA-Z0-9_-]+)?)/g;
|
|
14
|
+
export const TECH_TERMS = /\b(api|database|schema|migration|endpoint|middleware|component|service|module|handler|controller|model|interface|type|class|function|method|hook|plugin|extension|config|cache|queue|worker|daemon)\b/gi;
|
|
15
|
+
|
|
16
|
+
/** Extract concept name strings from free text using regex heuristics. */
|
|
17
|
+
export function extractConceptNames(text: string): string[] {
|
|
18
|
+
const concepts = new Set<string>();
|
|
19
|
+
|
|
20
|
+
let match: RegExpExecArray | null;
|
|
21
|
+
const re1 = new RegExp(CONCEPT_RE.source, CONCEPT_RE.flags);
|
|
22
|
+
while ((match = re1.exec(text)) !== null) {
|
|
23
|
+
concepts.add(match[1].trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const re2 = new RegExp(TECH_TERMS.source, TECH_TERMS.flags);
|
|
27
|
+
while ((match = re2.exec(text)) !== null) {
|
|
28
|
+
concepts.add(match[1].toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [...concepts].slice(0, 10);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Upsert concepts from text and link them to a source node via the given edge.
|
|
36
|
+
*
|
|
37
|
+
* Used for:
|
|
38
|
+
* - turn → "mentions" → concept (existing behaviour)
|
|
39
|
+
* - memory → "about_concept" → concept (Fix 1)
|
|
40
|
+
* - artifact → "artifact_mentions" → concept (Fix 2)
|
|
41
|
+
*/
|
|
42
|
+
export async function upsertAndLinkConcepts(
|
|
43
|
+
sourceId: string,
|
|
44
|
+
edgeName: string,
|
|
45
|
+
text: string,
|
|
46
|
+
store: SurrealStore,
|
|
47
|
+
embeddings: EmbeddingService,
|
|
48
|
+
logTag: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const names = extractConceptNames(text);
|
|
51
|
+
if (names.length === 0) return;
|
|
52
|
+
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
try {
|
|
55
|
+
let embedding: number[] | null = null;
|
|
56
|
+
if (embeddings.isAvailable()) {
|
|
57
|
+
try { embedding = await embeddings.embed(name); } catch { /* ok */ }
|
|
58
|
+
}
|
|
59
|
+
const conceptId = await store.upsertConcept(name, embedding);
|
|
60
|
+
if (conceptId) {
|
|
61
|
+
await store.relate(sourceId, edgeName, conceptId)
|
|
62
|
+
.catch(e => swallow(`${logTag}:relate`, e));
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
swallow(`${logTag}:upsert`, e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Link a newly-upserted concept to existing concepts via narrower/broader
|
|
72
|
+
* edges when one concept's name is a substring of the other (indicating a
|
|
73
|
+
* parent-child hierarchy, e.g. "React" → "React hooks").
|
|
74
|
+
*/
|
|
75
|
+
export async function linkConceptHierarchy(
|
|
76
|
+
conceptId: string,
|
|
77
|
+
conceptName: string,
|
|
78
|
+
store: SurrealStore,
|
|
79
|
+
logTag: string,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const existing = await store.queryFirst<{ id: string; content: string }>(
|
|
83
|
+
`SELECT id, content FROM concept WHERE id != $cid LIMIT 50`,
|
|
84
|
+
{ cid: conceptId },
|
|
85
|
+
);
|
|
86
|
+
if (existing.length === 0) return;
|
|
87
|
+
|
|
88
|
+
const lowerName = conceptName.toLowerCase();
|
|
89
|
+
|
|
90
|
+
for (const other of existing) {
|
|
91
|
+
const otherLower = (other.content ?? "").toLowerCase();
|
|
92
|
+
if (!otherLower || otherLower === lowerName) continue;
|
|
93
|
+
|
|
94
|
+
const otherId = String(other.id);
|
|
95
|
+
|
|
96
|
+
if (lowerName.includes(otherLower) && lowerName !== otherLower) {
|
|
97
|
+
// New concept is more specific (e.g. "React hooks" contains "React")
|
|
98
|
+
await store.relate(conceptId, "narrower", otherId)
|
|
99
|
+
.catch(e => swallow(`${logTag}:narrower`, e));
|
|
100
|
+
await store.relate(otherId, "broader", conceptId)
|
|
101
|
+
.catch(e => swallow(`${logTag}:broader`, e));
|
|
102
|
+
} else if (otherLower.includes(lowerName) && otherLower !== lowerName) {
|
|
103
|
+
// New concept is more general (e.g. "React" contained in "React hooks")
|
|
104
|
+
await store.relate(conceptId, "broader", otherId)
|
|
105
|
+
.catch(e => swallow(`${logTag}:broader`, e));
|
|
106
|
+
await store.relate(otherId, "narrower", conceptId)
|
|
107
|
+
.catch(e => swallow(`${logTag}:narrower`, e));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
swallow(`${logTag}:hierarchy`, e);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/context-engine.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { extractSkill } from "./skills.js";
|
|
|
51
51
|
import { generateReflection } from "./reflection.js";
|
|
52
52
|
import { graduateCausalToSkills } from "./skills.js";
|
|
53
53
|
import { swallow } from "./errors.js";
|
|
54
|
+
import { upsertAndLinkConcepts } from "./concept-extract.js";
|
|
54
55
|
|
|
55
56
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
56
57
|
|
|
@@ -305,6 +306,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
305
306
|
}
|
|
306
307
|
} else {
|
|
307
308
|
session.lastAssistantText = text;
|
|
309
|
+
if (turnId) session.lastAssistantTurnId = turnId;
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
return { ingested: true };
|
|
@@ -506,7 +508,11 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
506
508
|
if (embeddings.isAvailable()) {
|
|
507
509
|
try { embedding = await embeddings.embed(handoffText); } catch { /* ok */ }
|
|
508
510
|
}
|
|
509
|
-
await store.createMemory(handoffText, embedding, 8, "handoff", session.sessionId);
|
|
511
|
+
const handoffMemId = await store.createMemory(handoffText, embedding, 8, "handoff", session.sessionId);
|
|
512
|
+
if (handoffMemId && session.surrealSessionId) {
|
|
513
|
+
await store.relate(handoffMemId, "summarizes", session.surrealSessionId)
|
|
514
|
+
.catch(e => swallow.warn("midCleanup:summarizes", e));
|
|
515
|
+
}
|
|
510
516
|
}
|
|
511
517
|
})().catch(e => swallow.warn("midCleanup:handoff", e)),
|
|
512
518
|
);
|
|
@@ -548,44 +554,15 @@ function hasSemantic(text: string): boolean {
|
|
|
548
554
|
return text.split(/\s+/).filter(w => w.length > 2).length >= 3;
|
|
549
555
|
}
|
|
550
556
|
|
|
551
|
-
// --- Concept extraction (
|
|
552
|
-
|
|
553
|
-
const CONCEPT_RE = /\b(?:(?:use|using|implement|create|add|configure|setup|install|import)\s+)([A-Z][a-zA-Z0-9_-]+(?:\s+[A-Z][a-zA-Z0-9_-]+)?)/g;
|
|
554
|
-
const TECH_TERMS = /\b(api|database|schema|migration|endpoint|middleware|component|service|module|handler|controller|model|interface|type|class|function|method|hook|plugin|extension|config|cache|queue|worker|daemon)\b/gi;
|
|
557
|
+
// --- Concept extraction (delegates to shared helper) ---
|
|
555
558
|
|
|
556
559
|
async function extractAndLinkConcepts(
|
|
557
560
|
turnId: string,
|
|
558
561
|
text: string,
|
|
559
562
|
state: GlobalPluginState,
|
|
560
563
|
): Promise<void> {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
while ((match = re1.exec(text)) !== null) {
|
|
566
|
-
concepts.add(match[1].trim());
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const re2 = new RegExp(TECH_TERMS.source, TECH_TERMS.flags);
|
|
570
|
-
while ((match = re2.exec(text)) !== null) {
|
|
571
|
-
concepts.add(match[1].toLowerCase());
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (concepts.size === 0) return;
|
|
575
|
-
|
|
576
|
-
for (const conceptText of [...concepts].slice(0, 10)) {
|
|
577
|
-
try {
|
|
578
|
-
let embedding: number[] | null = null;
|
|
579
|
-
if (state.embeddings.isAvailable()) {
|
|
580
|
-
try { embedding = await state.embeddings.embed(conceptText); } catch { /* ok */ }
|
|
581
|
-
}
|
|
582
|
-
const conceptId = await state.store.upsertConcept(conceptText, embedding);
|
|
583
|
-
if (conceptId) {
|
|
584
|
-
await state.store.relate(turnId, "mentions", conceptId)
|
|
585
|
-
.catch(e => swallow("concepts:relate", e));
|
|
586
|
-
}
|
|
587
|
-
} catch (e) {
|
|
588
|
-
swallow("concepts:upsert", e);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
564
|
+
await upsertAndLinkConcepts(
|
|
565
|
+
turnId, "mentions", text,
|
|
566
|
+
state.store, state.embeddings, "concepts",
|
|
567
|
+
);
|
|
591
568
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { GlobalPluginState } from "../state.js";
|
|
6
6
|
import { recordToolOutcome } from "../retrieval-quality.js";
|
|
7
7
|
import { swallow } from "../errors.js";
|
|
8
|
+
import { upsertAndLinkConcepts } from "../concept-extract.js";
|
|
8
9
|
|
|
9
10
|
export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
10
11
|
return async (
|
|
@@ -30,13 +31,20 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
30
31
|
? event.result.slice(0, 500)
|
|
31
32
|
: JSON.stringify(event.result ?? "").slice(0, 500);
|
|
32
33
|
|
|
34
|
+
let toolResultTurnId: string | undefined;
|
|
33
35
|
try {
|
|
34
|
-
await state.store.upsertTurn({
|
|
36
|
+
toolResultTurnId = await state.store.upsertTurn({
|
|
35
37
|
session_id: session.sessionId,
|
|
36
38
|
role: "tool",
|
|
37
39
|
text: `[${event.toolName}] ${resultText}`,
|
|
38
40
|
embedding: null,
|
|
39
41
|
});
|
|
42
|
+
|
|
43
|
+
// Fix 5: Link tool result turn back to the assistant turn that triggered it
|
|
44
|
+
if (toolResultTurnId && session.lastAssistantTurnId) {
|
|
45
|
+
await state.store.relate(toolResultTurnId, "tool_result_of", session.lastAssistantTurnId)
|
|
46
|
+
.catch(e => swallow.warn("hook:afterToolCall:tool_result_of", e));
|
|
47
|
+
}
|
|
40
48
|
} catch (e) {
|
|
41
49
|
swallow("hook:afterToolCall:store", e);
|
|
42
50
|
}
|
|
@@ -92,8 +100,15 @@ async function trackArtifact(
|
|
|
92
100
|
const artifactId = await state.store.createArtifact(
|
|
93
101
|
(args.path as string) ?? "shell", ext, description, emb,
|
|
94
102
|
);
|
|
95
|
-
if (artifactId
|
|
96
|
-
|
|
97
|
-
.
|
|
103
|
+
if (artifactId) {
|
|
104
|
+
if (taskId) {
|
|
105
|
+
await state.store.relate(taskId, "produced", artifactId)
|
|
106
|
+
.catch(e => swallow.warn("artifact:relate", e));
|
|
107
|
+
}
|
|
108
|
+
// Fix 2: Link artifact to concepts it mentions
|
|
109
|
+
await upsertAndLinkConcepts(
|
|
110
|
+
artifactId, "artifact_mentions", description,
|
|
111
|
+
state.store, state.embeddings, "artifact:concepts",
|
|
112
|
+
);
|
|
98
113
|
}
|
|
99
114
|
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +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 { seedCognitiveBootstrap } from "./cognitive-bootstrap.js";
|
|
26
27
|
import { synthesizeWakeup } from "./wakeup.js";
|
|
27
28
|
import { extractSkill } from "./skills.js";
|
|
28
29
|
import { generateReflection, setReflectionContextWindow } from "./reflection.js";
|
|
@@ -418,6 +419,13 @@ export default definePluginEntry({
|
|
|
418
419
|
.then(n => { if (n > 0) logger.info(`Seeded ${n} identity chunks`); })
|
|
419
420
|
.catch(e => swallow.warn("factory:seedIdentity", e));
|
|
420
421
|
|
|
422
|
+
seedCognitiveBootstrap(store, embeddings)
|
|
423
|
+
.then(r => {
|
|
424
|
+
if (r.identitySeeded > 0 || r.coreSeeded > 0)
|
|
425
|
+
logger.info(`Cognitive bootstrap: ${r.identitySeeded} identity + ${r.coreSeeded} core`);
|
|
426
|
+
})
|
|
427
|
+
.catch(e => swallow.warn("factory:seedBootstrap", e));
|
|
428
|
+
|
|
421
429
|
return new KongBrainContextEngine(state);
|
|
422
430
|
});
|
|
423
431
|
|
package/src/memory-daemon.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { TurnData, PriorExtractions } from "./daemon-types.js";
|
|
|
12
12
|
import type { SurrealStore } from "./surreal.js";
|
|
13
13
|
import type { EmbeddingService } from "./embeddings.js";
|
|
14
14
|
import { swallow } from "./errors.js";
|
|
15
|
+
import { assertRecordId } from "./surreal.js";
|
|
16
|
+
import { upsertAndLinkConcepts, linkConceptHierarchy } from "./concept-extract.js";
|
|
15
17
|
|
|
16
18
|
// --- Build the extraction prompt ---
|
|
17
19
|
|
|
@@ -172,10 +174,12 @@ export async function writeExtractionResults(
|
|
|
172
174
|
writeOps.push((async () => {
|
|
173
175
|
for (const memId of result.resolved!.slice(0, 20)) {
|
|
174
176
|
if (typeof memId !== "string" || !RECORD_ID_RE.test(memId)) continue;
|
|
177
|
+
assertRecordId(memId);
|
|
175
178
|
counts.resolved++;
|
|
179
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
176
180
|
await store.queryExec(
|
|
177
|
-
`UPDATE
|
|
178
|
-
{
|
|
181
|
+
`UPDATE ${memId} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
|
|
182
|
+
{ sid: sessionId },
|
|
179
183
|
).catch(e => swallow.warn("daemon:resolveMemory", e));
|
|
180
184
|
}
|
|
181
185
|
})());
|
|
@@ -193,7 +197,10 @@ export async function writeExtractionResults(
|
|
|
193
197
|
if (embeddings.isAvailable()) {
|
|
194
198
|
try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
|
|
195
199
|
}
|
|
196
|
-
await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
200
|
+
const conceptId = await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
201
|
+
if (conceptId) {
|
|
202
|
+
await linkConceptHierarchy(conceptId, c.name, store, "daemon:concept");
|
|
203
|
+
}
|
|
197
204
|
})());
|
|
198
205
|
}
|
|
199
206
|
}
|
|
@@ -209,7 +216,10 @@ export async function writeExtractionResults(
|
|
|
209
216
|
if (embeddings.isAvailable()) {
|
|
210
217
|
try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedCorrection", e); }
|
|
211
218
|
}
|
|
212
|
-
await store.createMemory(text, emb, 9, "correction", sessionId);
|
|
219
|
+
const memId = await store.createMemory(text, emb, 9, "correction", sessionId);
|
|
220
|
+
if (memId) {
|
|
221
|
+
await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction");
|
|
222
|
+
}
|
|
213
223
|
})());
|
|
214
224
|
}
|
|
215
225
|
}
|
|
@@ -225,7 +235,10 @@ export async function writeExtractionResults(
|
|
|
225
235
|
if (embeddings.isAvailable()) {
|
|
226
236
|
try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedPreference", e); }
|
|
227
237
|
}
|
|
228
|
-
await store.createMemory(text, emb, 7, "preference", sessionId);
|
|
238
|
+
const memId = await store.createMemory(text, emb, 7, "preference", sessionId);
|
|
239
|
+
if (memId) {
|
|
240
|
+
await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference");
|
|
241
|
+
}
|
|
229
242
|
})());
|
|
230
243
|
}
|
|
231
244
|
}
|
|
@@ -243,7 +256,10 @@ export async function writeExtractionResults(
|
|
|
243
256
|
if (embeddings.isAvailable()) {
|
|
244
257
|
try { emb = await embeddings.embed(`${a.path} ${desc}`); } catch (e) { swallow("daemon:embedArtifact", e); }
|
|
245
258
|
}
|
|
246
|
-
await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
|
|
259
|
+
const artId = await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
|
|
260
|
+
if (artId) {
|
|
261
|
+
await upsertAndLinkConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact");
|
|
262
|
+
}
|
|
247
263
|
})());
|
|
248
264
|
}
|
|
249
265
|
}
|
|
@@ -259,7 +275,10 @@ export async function writeExtractionResults(
|
|
|
259
275
|
if (embeddings.isAvailable()) {
|
|
260
276
|
try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedDecision", e); }
|
|
261
277
|
}
|
|
262
|
-
await store.createMemory(text, emb, 7, "decision", sessionId);
|
|
278
|
+
const memId = await store.createMemory(text, emb, 7, "decision", sessionId);
|
|
279
|
+
if (memId) {
|
|
280
|
+
await upsertAndLinkConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision");
|
|
281
|
+
}
|
|
263
282
|
})());
|
|
264
283
|
}
|
|
265
284
|
}
|
package/src/skills.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { CompleteFn } from "./state.js";
|
|
|
13
13
|
import type { EmbeddingService } from "./embeddings.js";
|
|
14
14
|
import type { SurrealStore } from "./surreal.js";
|
|
15
15
|
import { swallow } from "./errors.js";
|
|
16
|
+
import { assertRecordId } from "./surreal.js";
|
|
16
17
|
|
|
17
18
|
// --- Types ---
|
|
18
19
|
|
|
@@ -237,12 +238,14 @@ export async function recordSkillOutcome(
|
|
|
237
238
|
|
|
238
239
|
try {
|
|
239
240
|
const field = success ? "success_count" : "failure_count";
|
|
241
|
+
assertRecordId(skillId);
|
|
242
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
240
243
|
await store.queryExec(
|
|
241
|
-
`UPDATE
|
|
244
|
+
`UPDATE ${skillId} SET
|
|
242
245
|
${field} += 1,
|
|
243
246
|
avg_duration_ms = (avg_duration_ms * (success_count + failure_count - 1) + $dur) / (success_count + failure_count),
|
|
244
247
|
last_used = time::now()`,
|
|
245
|
-
{
|
|
248
|
+
{ dur: durationMs },
|
|
246
249
|
);
|
|
247
250
|
} catch (e) { swallow("skills:non-critical", e); }
|
|
248
251
|
}
|
package/src/state.ts
CHANGED
package/src/surreal.ts
CHANGED
|
@@ -447,10 +447,8 @@ export class SurrealStore {
|
|
|
447
447
|
assertRecordId(fromId);
|
|
448
448
|
assertRecordId(toId);
|
|
449
449
|
const safeName = edge.replace(/[^a-zA-Z0-9_]/g, "");
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
{ from: fromId, to: toId },
|
|
453
|
-
);
|
|
450
|
+
// Direct interpolation safe: assertRecordId validates format above
|
|
451
|
+
await this.queryExec(`RELATE ${fromId}->${safeName}->${toId}`);
|
|
454
452
|
}
|
|
455
453
|
|
|
456
454
|
// ── 5-Pillar entity operations ─────────────────────────────────────────
|
|
@@ -504,12 +502,12 @@ export class SurrealStore {
|
|
|
504
502
|
): Promise<void> {
|
|
505
503
|
assertRecordId(sessionId);
|
|
506
504
|
await this.queryExec(
|
|
507
|
-
`UPDATE
|
|
505
|
+
`UPDATE ${sessionId} SET
|
|
508
506
|
turn_count += 1,
|
|
509
507
|
total_input_tokens += $input,
|
|
510
508
|
total_output_tokens += $output,
|
|
511
509
|
last_active = time::now()`,
|
|
512
|
-
{
|
|
510
|
+
{ input: inputTokens, output: outputTokens },
|
|
513
511
|
);
|
|
514
512
|
}
|
|
515
513
|
|
|
@@ -517,27 +515,25 @@ export class SurrealStore {
|
|
|
517
515
|
assertRecordId(sessionId);
|
|
518
516
|
if (summary) {
|
|
519
517
|
await this.queryExec(
|
|
520
|
-
`UPDATE
|
|
521
|
-
{
|
|
518
|
+
`UPDATE ${sessionId} SET ended_at = time::now(), summary = $summary`,
|
|
519
|
+
{ summary },
|
|
522
520
|
);
|
|
523
521
|
} else {
|
|
524
|
-
await this.queryExec(`UPDATE
|
|
522
|
+
await this.queryExec(`UPDATE ${sessionId} SET ended_at = time::now()`);
|
|
525
523
|
}
|
|
526
524
|
}
|
|
527
525
|
|
|
528
526
|
async markSessionActive(sessionId: string): Promise<void> {
|
|
529
527
|
assertRecordId(sessionId);
|
|
530
528
|
await this.queryExec(
|
|
531
|
-
`UPDATE
|
|
532
|
-
{ sid: sessionId },
|
|
529
|
+
`UPDATE ${sessionId} SET cleanup_completed = false, last_active = time::now()`,
|
|
533
530
|
);
|
|
534
531
|
}
|
|
535
532
|
|
|
536
533
|
async markSessionEnded(sessionId: string): Promise<void> {
|
|
537
534
|
assertRecordId(sessionId);
|
|
538
535
|
await this.queryExec(
|
|
539
|
-
`UPDATE
|
|
540
|
-
{ sid: sessionId },
|
|
536
|
+
`UPDATE ${sessionId} SET ended_at = time::now(), cleanup_completed = true`,
|
|
541
537
|
);
|
|
542
538
|
}
|
|
543
539
|
|
|
@@ -555,8 +551,7 @@ export class SurrealStore {
|
|
|
555
551
|
assertRecordId(sessionId);
|
|
556
552
|
assertRecordId(taskId);
|
|
557
553
|
await this.queryExec(
|
|
558
|
-
`RELATE
|
|
559
|
-
{ from: sessionId, to: taskId },
|
|
554
|
+
`RELATE ${sessionId}->session_task->${taskId}`,
|
|
560
555
|
);
|
|
561
556
|
}
|
|
562
557
|
|
|
@@ -564,8 +559,7 @@ export class SurrealStore {
|
|
|
564
559
|
assertRecordId(taskId);
|
|
565
560
|
assertRecordId(projectId);
|
|
566
561
|
await this.queryExec(
|
|
567
|
-
`RELATE
|
|
568
|
-
{ from: taskId, to: projectId },
|
|
562
|
+
`RELATE ${taskId}->task_part_of->${projectId}`,
|
|
569
563
|
);
|
|
570
564
|
}
|
|
571
565
|
|
|
@@ -573,8 +567,7 @@ export class SurrealStore {
|
|
|
573
567
|
assertRecordId(agentId);
|
|
574
568
|
assertRecordId(taskId);
|
|
575
569
|
await this.queryExec(
|
|
576
|
-
`RELATE
|
|
577
|
-
{ from: agentId, to: taskId },
|
|
570
|
+
`RELATE ${agentId}->performed->${taskId}`,
|
|
578
571
|
);
|
|
579
572
|
}
|
|
580
573
|
|
|
@@ -582,8 +575,7 @@ export class SurrealStore {
|
|
|
582
575
|
assertRecordId(agentId);
|
|
583
576
|
assertRecordId(projectId);
|
|
584
577
|
await this.queryExec(
|
|
585
|
-
`RELATE
|
|
586
|
-
{ from: agentId, to: projectId },
|
|
578
|
+
`RELATE ${agentId}->owns->${projectId}`,
|
|
587
579
|
);
|
|
588
580
|
}
|
|
589
581
|
|
|
@@ -625,7 +617,7 @@ export class SurrealStore {
|
|
|
625
617
|
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
626
618
|
const forwardQueries = frontier.flatMap((id) =>
|
|
627
619
|
forwardEdges.map((edge) =>
|
|
628
|
-
this.queryFirst<any>(`${selectFields} FROM
|
|
620
|
+
this.queryFirst<any>(`${selectFields} FROM ${id}->${edge}->? LIMIT 3`, bindings).catch(
|
|
629
621
|
(e) => {
|
|
630
622
|
swallow.warn("surreal:graphExpand", e);
|
|
631
623
|
return [] as Record<string, unknown>[];
|
|
@@ -636,7 +628,7 @@ export class SurrealStore {
|
|
|
636
628
|
|
|
637
629
|
const reverseQueries = frontier.flatMap((id) =>
|
|
638
630
|
reverseEdges.map((edge) =>
|
|
639
|
-
this.queryFirst<any>(`${selectFields} FROM
|
|
631
|
+
this.queryFirst<any>(`${selectFields} FROM ${id}<-${edge}<-? LIMIT 3`, bindings).catch(
|
|
640
632
|
(e) => {
|
|
641
633
|
swallow.warn("surreal:graphExpand", e);
|
|
642
634
|
return [] as Record<string, unknown>[];
|
|
@@ -687,8 +679,7 @@ export class SurrealStore {
|
|
|
687
679
|
try {
|
|
688
680
|
assertRecordId(id);
|
|
689
681
|
await this.queryExec(
|
|
690
|
-
`UPDATE
|
|
691
|
-
{ rid: id },
|
|
682
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
|
|
692
683
|
);
|
|
693
684
|
} catch (e) {
|
|
694
685
|
swallow.warn("surreal:bumpAccessCounts", e);
|
|
@@ -710,8 +701,7 @@ export class SurrealStore {
|
|
|
710
701
|
if (rows.length > 0) {
|
|
711
702
|
const id = String(rows[0].id);
|
|
712
703
|
await this.queryExec(
|
|
713
|
-
`UPDATE
|
|
714
|
-
{ cid: id },
|
|
704
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
|
|
715
705
|
);
|
|
716
706
|
return id;
|
|
717
707
|
}
|
|
@@ -768,8 +758,8 @@ export class SurrealStore {
|
|
|
768
758
|
const existing = dupes[0];
|
|
769
759
|
const newImp = Math.max(existing.importance ?? 0, importance);
|
|
770
760
|
await this.queryExec(
|
|
771
|
-
`UPDATE
|
|
772
|
-
{
|
|
761
|
+
`UPDATE ${String(existing.id)} SET access_count += 1, importance = $imp, last_accessed = time::now()`,
|
|
762
|
+
{ imp: newImp },
|
|
773
763
|
);
|
|
774
764
|
return String(existing.id);
|
|
775
765
|
}
|
|
@@ -844,7 +834,7 @@ export class SurrealStore {
|
|
|
844
834
|
assertRecordId(id);
|
|
845
835
|
const ALLOWED_FIELDS = new Set(["text", "category", "priority", "tier", "active"]);
|
|
846
836
|
const sets: string[] = [];
|
|
847
|
-
const bindings: Record<string, unknown> = {
|
|
837
|
+
const bindings: Record<string, unknown> = {};
|
|
848
838
|
for (const [key, val] of Object.entries(fields)) {
|
|
849
839
|
if (val !== undefined && ALLOWED_FIELDS.has(key)) {
|
|
850
840
|
sets.push(`${key} = $${key}`);
|
|
@@ -854,7 +844,7 @@ export class SurrealStore {
|
|
|
854
844
|
if (sets.length === 0) return false;
|
|
855
845
|
sets.push("updated_at = time::now()");
|
|
856
846
|
const rows = await this.queryFirst<{ id: string }>(
|
|
857
|
-
`UPDATE
|
|
847
|
+
`UPDATE ${id} SET ${sets.join(", ")} RETURN id`,
|
|
858
848
|
bindings,
|
|
859
849
|
);
|
|
860
850
|
return rows.length > 0;
|
|
@@ -863,8 +853,7 @@ export class SurrealStore {
|
|
|
863
853
|
async deleteCoreMemory(id: string): Promise<void> {
|
|
864
854
|
assertRecordId(id);
|
|
865
855
|
await this.queryExec(
|
|
866
|
-
`UPDATE
|
|
867
|
-
{ rid: id },
|
|
856
|
+
`UPDATE ${id} SET active = false, updated_at = time::now()`,
|
|
868
857
|
);
|
|
869
858
|
}
|
|
870
859
|
|
|
@@ -1013,9 +1002,9 @@ export class SurrealStore {
|
|
|
1013
1002
|
|
|
1014
1003
|
async resolveMemory(memoryId: string): Promise<boolean> {
|
|
1015
1004
|
try {
|
|
1005
|
+
assertRecordId(memoryId);
|
|
1016
1006
|
await this.queryFirst(
|
|
1017
|
-
`UPDATE
|
|
1018
|
-
{ id: memoryId },
|
|
1007
|
+
`UPDATE ${memoryId} SET status = 'resolved', resolved_at = time::now()`,
|
|
1019
1008
|
);
|
|
1020
1009
|
return true;
|
|
1021
1010
|
} catch (e) {
|
|
@@ -1223,10 +1212,10 @@ export class SurrealStore {
|
|
|
1223
1212
|
assertRecordId(String(keep));
|
|
1224
1213
|
assertRecordId(String(drop));
|
|
1225
1214
|
await this.queryExec(
|
|
1226
|
-
`UPDATE
|
|
1227
|
-
{
|
|
1215
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1216
|
+
{ imp: dupe.importance },
|
|
1228
1217
|
);
|
|
1229
|
-
await this.queryExec(`DELETE
|
|
1218
|
+
await this.queryExec(`DELETE ${String(drop)}`);
|
|
1230
1219
|
seen.add(String(drop));
|
|
1231
1220
|
merged++;
|
|
1232
1221
|
}
|
|
@@ -1252,8 +1241,8 @@ export class SurrealStore {
|
|
|
1252
1241
|
const emb = await embedFn(mem.text);
|
|
1253
1242
|
if (!emb) continue;
|
|
1254
1243
|
await this.queryExec(
|
|
1255
|
-
`UPDATE
|
|
1256
|
-
{
|
|
1244
|
+
`UPDATE ${String(mem.id)} SET embedding = $emb`,
|
|
1245
|
+
{ emb },
|
|
1257
1246
|
);
|
|
1258
1247
|
|
|
1259
1248
|
const dupes = await this.queryFirst<{
|
|
@@ -1283,10 +1272,10 @@ export class SurrealStore {
|
|
|
1283
1272
|
assertRecordId(String(keep));
|
|
1284
1273
|
assertRecordId(String(drop));
|
|
1285
1274
|
await this.queryExec(
|
|
1286
|
-
`UPDATE
|
|
1287
|
-
{
|
|
1275
|
+
`UPDATE ${String(keep)} SET access_count += 1, importance = math::max([importance, $imp])`,
|
|
1276
|
+
{ imp: dupe.importance },
|
|
1288
1277
|
);
|
|
1289
|
-
await this.queryExec(`DELETE
|
|
1278
|
+
await this.queryExec(`DELETE ${String(drop)}`);
|
|
1290
1279
|
seen.add(String(drop));
|
|
1291
1280
|
merged++;
|
|
1292
1281
|
}
|
|
@@ -1387,8 +1376,8 @@ export class SurrealStore {
|
|
|
1387
1376
|
): Promise<void> {
|
|
1388
1377
|
assertRecordId(checkpointId);
|
|
1389
1378
|
await this.queryExec(
|
|
1390
|
-
`UPDATE
|
|
1391
|
-
{
|
|
1379
|
+
`UPDATE ${checkpointId} SET status = "complete", memory_id = $mid`,
|
|
1380
|
+
{ mid: memoryId },
|
|
1392
1381
|
);
|
|
1393
1382
|
}
|
|
1394
1383
|
|