kongbrain 0.1.1 → 0.1.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.
@@ -1,67 +1,21 @@
1
1
  /**
2
- * Memory Daemon — Persistent worker thread for incremental extraction.
2
+ * Memory Daemon — extraction logic for incremental knowledge extraction.
3
3
  *
4
- * Runs alongside the main conversation thread for the entire session.
5
- * Receives turn batches from the main thread, calls an LLM for incremental
6
- * extraction of 9 knowledge types, and writes results to SurrealDB.
4
+ * Contains the prompt building, transcript formatting, and DB write logic
5
+ * used by the daemon manager to extract 9 knowledge types from conversation
6
+ * turns: causal chains, monologue traces, resolved memories, concepts,
7
+ * corrections, preferences, artifacts, decisions, skills.
7
8
  *
8
- * Extracts: causal chains, monologue traces, resolved memories,
9
- * concepts, corrections, preferences, artifacts, decisions, skills.
10
- *
11
- * This file runs inside a Worker thread — it is NOT imported by the main thread.
12
- *
13
- * Ported from kongbrain — creates own SurrealStore/EmbeddingService instances.
9
+ * Ported from kongbrain takes SurrealStore/EmbeddingService as params.
14
10
  */
15
- import { parentPort, workerData } from "node:worker_threads";
16
- import type { DaemonMessage, DaemonResponse, DaemonWorkerData, PriorExtractions, TurnData } from "./daemon-types.js";
17
- import { SurrealStore } from "./surreal.js";
18
- import { EmbeddingService } from "./embeddings.js";
11
+ import type { TurnData, PriorExtractions } from "./daemon-types.js";
12
+ import type { SurrealStore } from "./surreal.js";
13
+ import type { EmbeddingService } from "./embeddings.js";
19
14
  import { swallow } from "./errors.js";
20
15
 
21
- if (!parentPort) {
22
- throw new Error("memory-daemon.ts must be run as a worker thread");
23
- }
24
-
25
- const config = workerData as DaemonWorkerData;
26
-
27
- // Worker-local instances
28
- let store: SurrealStore;
29
- let embeddings: EmbeddingService;
30
-
31
- // --- Cumulative extraction counts ---
32
- const counts = {
33
- turns: 0, causal: 0, monologue: 0, resolved: 0, concept: 0,
34
- correction: 0, preference: 0, artifact: 0, decision: 0, skill: 0, errors: 0,
35
- };
36
-
37
- let processing = false;
38
- let shuttingDown = false;
39
- const batchQueue: DaemonMessage[] = [];
40
-
41
- const priorState: PriorExtractions = {
42
- conceptNames: [], artifactPaths: [], skillNames: [],
43
- };
44
-
45
- // --- Initialization ---
46
-
47
- async function init(): Promise<boolean> {
48
- try {
49
- store = new SurrealStore(config.surrealConfig);
50
- await store.initialize();
51
-
52
- embeddings = new EmbeddingService(config.embeddingConfig);
53
- await embeddings.initialize();
54
-
55
- return true;
56
- } catch (e) {
57
- swallow.warn("memory-daemon:init", e);
58
- return false;
59
- }
60
- }
61
-
62
16
  // --- Build the extraction prompt ---
63
17
 
64
- function buildSystemPrompt(
18
+ export function buildSystemPrompt(
65
19
  hasThinking: boolean,
66
20
  hasRetrievedMemories: boolean,
67
21
  prior: PriorExtractions,
@@ -136,7 +90,7 @@ RULES:
136
90
  - For artifacts, extract file paths from bash/tool commands in the transcript.`;
137
91
  }
138
92
 
139
- function buildTranscript(turns: TurnData[]): string {
93
+ export function buildTranscript(turns: TurnData[]): string {
140
94
  return turns
141
95
  .map(t => {
142
96
  const prefix = t.tool_name ? `[tool:${t.tool_name}]` : `[${t.role}]`;
@@ -148,348 +102,199 @@ function buildTranscript(turns: TurnData[]): string {
148
102
  .join("\n");
149
103
  }
150
104
 
151
- // --- Main extraction logic ---
152
-
153
- async function processExtraction(msg: DaemonMessage & { type: "turn_batch" }): Promise<void> {
154
- processing = true;
155
- try {
156
- const { turns, thinking, retrievedMemories, sessionId, priorExtractions } = msg;
157
-
158
- if (turns.length < 2) return;
159
-
160
- // Merge incoming prior state
161
- if (priorExtractions) {
162
- for (const name of priorExtractions.conceptNames) {
163
- if (!priorState.conceptNames.includes(name)) priorState.conceptNames.push(name);
164
- }
165
- for (const path of priorExtractions.artifactPaths) {
166
- if (!priorState.artifactPaths.includes(path)) priorState.artifactPaths.push(path);
167
- }
168
- for (const name of priorExtractions.skillNames) {
169
- if (!priorState.skillNames.includes(name)) priorState.skillNames.push(name);
170
- }
171
- }
172
-
173
- const transcript = buildTranscript(turns);
174
- const sections: string[] = [`[TRANSCRIPT]\n${transcript.slice(0, 60000)}`];
175
-
176
- if (thinking.length > 0) {
177
- sections.push(`[THINKING]\n${thinking.slice(-8).join("\n---\n").slice(0, 4000)}`);
178
- }
105
+ // --- Write extraction results to DB ---
106
+
107
+ export interface ExtractionCounts {
108
+ causal: number;
109
+ monologue: number;
110
+ resolved: number;
111
+ concept: number;
112
+ correction: number;
113
+ preference: number;
114
+ artifact: number;
115
+ decision: number;
116
+ skill: number;
117
+ }
179
118
 
180
- if (retrievedMemories.length > 0) {
181
- const memList = retrievedMemories.map(m => `${m.id}: ${String(m.text).slice(0, 200)}`).join("\n");
182
- sections.push(`[RETRIEVED MEMORIES]\nMark any that have been fully addressed/fixed/completed.\n${memList}`);
119
+ export async function writeExtractionResults(
120
+ result: Record<string, any>,
121
+ sessionId: string,
122
+ store: SurrealStore,
123
+ embeddings: EmbeddingService,
124
+ priorState: PriorExtractions,
125
+ ): Promise<ExtractionCounts> {
126
+ const counts: ExtractionCounts = {
127
+ causal: 0, monologue: 0, resolved: 0, concept: 0,
128
+ correction: 0, preference: 0, artifact: 0, decision: 0, skill: 0,
129
+ };
130
+
131
+ const writeOps: Promise<void>[] = [];
132
+
133
+ // 1. Causal chains
134
+ if (Array.isArray(result.causal) && result.causal.length > 0) {
135
+ const { linkCausalEdges } = await import("./causal.js");
136
+ const validated = result.causal
137
+ .filter((c: any) => c.triggerText && c.outcomeText && c.chainType && typeof c.success === "boolean")
138
+ .slice(0, 5)
139
+ .map((c: any) => ({
140
+ triggerText: String(c.triggerText).slice(0, 200),
141
+ outcomeText: String(c.outcomeText).slice(0, 200),
142
+ chainType: (["debug", "refactor", "feature", "fix"].includes(c.chainType) ? c.chainType : "fix") as "debug" | "refactor" | "feature" | "fix",
143
+ success: Boolean(c.success),
144
+ confidence: Math.max(0, Math.min(1, Number(c.confidence) || 0.5)),
145
+ description: String(c.description ?? "").slice(0, 150),
146
+ }));
147
+ if (validated.length > 0) {
148
+ writeOps.push(linkCausalEdges(validated, sessionId, store, embeddings));
149
+ counts.causal += validated.length;
183
150
  }
151
+ }
184
152
 
185
- const systemPrompt = buildSystemPrompt(thinking.length > 0, retrievedMemories.length > 0, priorState);
186
-
187
- const { completeSimple, getModel } = await import("@mariozechner/pi-ai");
188
- const provider = config.llmProvider ?? "anthropic";
189
- const modelId = config.llmModel ?? "claude-opus-4-6";
190
- // getModel is heavily typed for known providers; cast needed for runtime-configured values
191
- const model = (getModel as any)(provider, modelId);
192
-
193
- const response = await completeSimple(model, {
194
- systemPrompt,
195
- messages: [{
196
- role: "user",
197
- timestamp: Date.now(),
198
- content: sections.join("\n\n"),
199
- }],
200
- });
201
-
202
- const responseText = response.content
203
- .filter((c: any) => c.type === "text")
204
- .map((c: any) => c.text)
205
- .join("");
206
-
207
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
208
- if (!jsonMatch) return;
209
-
210
- let result: Record<string, any>;
211
- try {
212
- result = JSON.parse(jsonMatch[0]);
213
- } catch {
214
- try {
215
- result = JSON.parse(jsonMatch[0].replace(/,\s*([}\]])/g, "$1"));
216
- } catch {
217
- // Per-field fallback
218
- result = {};
219
- const fields = ["causal", "monologue", "resolved", "concepts", "corrections", "preferences", "artifacts", "decisions", "skills"];
220
- for (const field of fields) {
221
- const fieldMatch = jsonMatch[0].match(new RegExp(`"${field}"\\s*:\\s*(\\[[\\s\\S]*?\\])(?=\\s*[,}]\\s*"[a-z]|\\s*\\}$)`, "m"));
222
- if (fieldMatch) {
223
- try { result[field] = JSON.parse(fieldMatch[1]); } catch { /* skip */ }
224
- }
153
+ // 2. Monologue traces
154
+ if (Array.isArray(result.monologue) && result.monologue.length > 0) {
155
+ for (const entry of result.monologue.slice(0, 5)) {
156
+ if (!entry.category || !entry.content) continue;
157
+ counts.monologue++;
158
+ writeOps.push((async () => {
159
+ let emb: number[] | null = null;
160
+ if (embeddings.isAvailable()) {
161
+ try { emb = await embeddings.embed(entry.content); } catch (e) { swallow("daemon:embedMonologue", e); }
225
162
  }
226
- if (Object.keys(result).length === 0) return;
227
- }
228
- }
229
-
230
- // --- Write all results to DB ---
231
- const writeOps: Promise<void>[] = [];
232
-
233
- // 1. Causal chains
234
- if (Array.isArray(result.causal) && result.causal.length > 0) {
235
- const { linkCausalEdges } = await import("./causal.js");
236
- const validated = result.causal
237
- .filter((c: any) => c.triggerText && c.outcomeText && c.chainType && typeof c.success === "boolean")
238
- .slice(0, 5)
239
- .map((c: any) => ({
240
- triggerText: String(c.triggerText).slice(0, 200),
241
- outcomeText: String(c.outcomeText).slice(0, 200),
242
- chainType: (["debug", "refactor", "feature", "fix"].includes(c.chainType) ? c.chainType : "fix") as "debug" | "refactor" | "feature" | "fix",
243
- success: Boolean(c.success),
244
- confidence: Math.max(0, Math.min(1, Number(c.confidence) || 0.5)),
245
- description: String(c.description ?? "").slice(0, 150),
246
- }));
247
- if (validated.length > 0) {
248
- writeOps.push(linkCausalEdges(validated, sessionId, store, embeddings));
249
- counts.causal += validated.length;
250
- }
163
+ await store.createMonologue(sessionId, entry.category, entry.content, emb);
164
+ })());
251
165
  }
166
+ }
252
167
 
253
- // 2. Monologue traces
254
- if (Array.isArray(result.monologue) && result.monologue.length > 0) {
255
- for (const entry of result.monologue.slice(0, 5)) {
256
- if (!entry.category || !entry.content) continue;
257
- counts.monologue++;
258
- writeOps.push((async () => {
259
- let emb: number[] | null = null;
260
- if (embeddings.isAvailable()) {
261
- try { emb = await embeddings.embed(entry.content); } catch (e) { swallow("daemon:embedMonologue", e); }
262
- }
263
- await store.createMonologue(sessionId, entry.category, entry.content, emb);
264
- })());
168
+ // 3. Resolved memories
169
+ if (Array.isArray(result.resolved) && result.resolved.length > 0) {
170
+ const RECORD_ID_RE = /^memory:[a-zA-Z0-9_]+$/;
171
+ writeOps.push((async () => {
172
+ for (const memId of result.resolved!.slice(0, 20)) {
173
+ if (typeof memId !== "string" || !RECORD_ID_RE.test(memId)) continue;
174
+ counts.resolved++;
175
+ await store.queryExec(
176
+ `UPDATE ${memId} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
177
+ { sid: sessionId },
178
+ ).catch(e => swallow.warn("daemon:resolveMemory", e));
265
179
  }
266
- }
180
+ })());
181
+ }
267
182
 
268
- // 3. Resolved memories
269
- if (Array.isArray(result.resolved) && result.resolved.length > 0) {
270
- const RECORD_ID_RE = /^memory:[a-zA-Z0-9_]+$/;
183
+ // 4. Concepts
184
+ if (Array.isArray(result.concepts) && result.concepts.length > 0) {
185
+ for (const c of result.concepts.slice(0, 11)) {
186
+ if (!c.name || !c.content) continue;
187
+ if (priorState.conceptNames.includes(c.name)) continue;
188
+ counts.concept++;
189
+ priorState.conceptNames.push(c.name);
271
190
  writeOps.push((async () => {
272
- for (const memId of result.resolved!.slice(0, 20)) {
273
- if (typeof memId !== "string" || !RECORD_ID_RE.test(memId)) continue;
274
- counts.resolved++;
275
- await store.queryExec(
276
- `UPDATE ${memId} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
277
- { sid: sessionId },
278
- ).catch(e => swallow.warn("daemon:resolveMemory", e));
191
+ let emb: number[] | null = null;
192
+ if (embeddings.isAvailable()) {
193
+ try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
279
194
  }
195
+ await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
280
196
  })());
281
197
  }
198
+ }
282
199
 
283
- // 4. Concepts
284
- if (Array.isArray(result.concepts) && result.concepts.length > 0) {
285
- for (const c of result.concepts.slice(0, 11)) {
286
- if (!c.name || !c.content) continue;
287
- if (priorState.conceptNames.includes(c.name)) continue;
288
- counts.concept++;
289
- priorState.conceptNames.push(c.name);
290
- writeOps.push((async () => {
291
- let emb: number[] | null = null;
292
- if (embeddings.isAvailable()) {
293
- try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
294
- }
295
- await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
296
- })());
297
- }
298
- }
299
-
300
- // 5. Corrections — high-importance memories
301
- if (Array.isArray(result.corrections) && result.corrections.length > 0) {
302
- for (const c of result.corrections.slice(0, 5)) {
303
- if (!c.original || !c.correction) continue;
304
- counts.correction++;
305
- const text = `[CORRECTION] Original: "${String(c.original).slice(0, 200)}" -> Corrected: "${String(c.correction).slice(0, 200)}" (Context: ${String(c.context ?? "").slice(0, 100)})`;
306
- writeOps.push((async () => {
307
- let emb: number[] | null = null;
308
- if (embeddings.isAvailable()) {
309
- try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedCorrection", e); }
310
- }
311
- await store.createMemory(text, emb, 9, "correction", sessionId);
312
- })());
313
- }
314
- }
315
-
316
- // 6. User preferences
317
- if (Array.isArray(result.preferences) && result.preferences.length > 0) {
318
- for (const p of result.preferences.slice(0, 5)) {
319
- if (!p.preference) continue;
320
- counts.preference++;
321
- const text = `[USER PREFERENCE] ${String(p.preference).slice(0, 250)} (Evidence: ${String(p.evidence ?? "").slice(0, 150)})`;
322
- writeOps.push((async () => {
323
- let emb: number[] | null = null;
324
- if (embeddings.isAvailable()) {
325
- try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedPreference", e); }
326
- }
327
- await store.createMemory(text, emb, 7, "preference", sessionId);
328
- })());
329
- }
330
- }
331
-
332
- // 7. Artifacts
333
- if (Array.isArray(result.artifacts) && result.artifacts.length > 0) {
334
- for (const a of result.artifacts.slice(0, 10)) {
335
- if (!a.path) continue;
336
- if (priorState.artifactPaths.includes(a.path)) continue;
337
- counts.artifact++;
338
- priorState.artifactPaths.push(a.path);
339
- const desc = `${String(a.action ?? "modified")}: ${String(a.summary ?? "").slice(0, 200)}`;
340
- writeOps.push((async () => {
341
- let emb: number[] | null = null;
342
- if (embeddings.isAvailable()) {
343
- try { emb = await embeddings.embed(`${a.path} ${desc}`); } catch (e) { swallow("daemon:embedArtifact", e); }
344
- }
345
- await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
346
- })());
347
- }
348
- }
349
-
350
- // 8. Decisions
351
- if (Array.isArray(result.decisions) && result.decisions.length > 0) {
352
- for (const d of result.decisions.slice(0, 6)) {
353
- if (!d.decision) continue;
354
- counts.decision++;
355
- const text = `[DECISION] ${String(d.decision).slice(0, 200)} — Rationale: ${String(d.rationale ?? "").slice(0, 200)} (Alternatives: ${String(d.alternatives_considered ?? "none").slice(0, 100)})`;
356
- writeOps.push((async () => {
357
- let emb: number[] | null = null;
358
- if (embeddings.isAvailable()) {
359
- try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedDecision", e); }
360
- }
361
- await store.createMemory(text, emb, 7, "decision", sessionId);
362
- })());
363
- }
200
+ // 5. Corrections — high-importance memories
201
+ if (Array.isArray(result.corrections) && result.corrections.length > 0) {
202
+ for (const c of result.corrections.slice(0, 5)) {
203
+ if (!c.original || !c.correction) continue;
204
+ counts.correction++;
205
+ const text = `[CORRECTION] Original: "${String(c.original).slice(0, 200)}" -> Corrected: "${String(c.correction).slice(0, 200)}" (Context: ${String(c.context ?? "").slice(0, 100)})`;
206
+ writeOps.push((async () => {
207
+ let emb: number[] | null = null;
208
+ if (embeddings.isAvailable()) {
209
+ try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedCorrection", e); }
210
+ }
211
+ await store.createMemory(text, emb, 9, "correction", sessionId);
212
+ })());
364
213
  }
214
+ }
365
215
 
366
- // 9. Skills
367
- if (Array.isArray(result.skills) && result.skills.length > 0) {
368
- for (const s of result.skills.slice(0, 3)) {
369
- if (!s.name || !Array.isArray(s.steps) || s.steps.length === 0) continue;
370
- if (priorState.skillNames.includes(s.name)) continue;
371
- counts.skill++;
372
- priorState.skillNames.push(s.name);
373
- const content = `${s.name}\nTrigger: ${String(s.trigger_context ?? "").slice(0, 150)}\nSteps:\n${s.steps.map((st: string, i: number) => `${i + 1}. ${String(st).slice(0, 200)}`).join("\n")}`;
374
- writeOps.push((async () => {
375
- let emb: number[] | null = null;
376
- if (embeddings.isAvailable()) {
377
- try { emb = await embeddings.embed(content); } catch (e) { swallow("daemon:embedSkill", e); }
378
- }
379
- await store.queryExec(
380
- `CREATE skill CONTENT $record`,
381
- {
382
- record: {
383
- name: String(s.name).slice(0, 100),
384
- description: content,
385
- content,
386
- steps: s.steps.map((st: string) => String(st).slice(0, 200)),
387
- trigger_context: String(s.trigger_context ?? "").slice(0, 200),
388
- tags: ["auto-extracted"],
389
- session_id: sessionId,
390
- ...(emb ? { embedding: emb } : {}),
391
- },
392
- },
393
- ).catch(e => swallow.warn("daemon:createSkill", e));
394
- })());
395
- }
216
+ // 6. User preferences
217
+ if (Array.isArray(result.preferences) && result.preferences.length > 0) {
218
+ for (const p of result.preferences.slice(0, 5)) {
219
+ if (!p.preference) continue;
220
+ counts.preference++;
221
+ const text = `[USER PREFERENCE] ${String(p.preference).slice(0, 250)} (Evidence: ${String(p.evidence ?? "").slice(0, 150)})`;
222
+ writeOps.push((async () => {
223
+ let emb: number[] | null = null;
224
+ if (embeddings.isAvailable()) {
225
+ try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedPreference", e); }
226
+ }
227
+ await store.createMemory(text, emb, 7, "preference", sessionId);
228
+ })());
396
229
  }
397
-
398
- await Promise.allSettled(writeOps);
399
-
400
- counts.turns = turns.length;
401
-
402
- parentPort!.postMessage({
403
- type: "extraction_complete",
404
- extractedTurnCount: counts.turns,
405
- causalCount: counts.causal,
406
- monologueCount: counts.monologue,
407
- resolvedCount: counts.resolved,
408
- conceptCount: counts.concept,
409
- correctionCount: counts.correction,
410
- preferenceCount: counts.preference,
411
- artifactCount: counts.artifact,
412
- decisionCount: counts.decision,
413
- skillCount: counts.skill,
414
- extractedNames: { ...priorState },
415
- } satisfies DaemonResponse);
416
- } catch (e) {
417
- counts.errors++;
418
- swallow.warn("memory-daemon:extraction", e);
419
- parentPort!.postMessage({
420
- type: "error",
421
- message: String(e),
422
- } satisfies DaemonResponse);
423
- } finally {
424
- processing = false;
425
230
  }
426
- }
427
-
428
- // --- Batch Queue Processing ---
429
231
 
430
- async function drainQueue(): Promise<void> {
431
- while (batchQueue.length > 0 && !shuttingDown) {
432
- const batch = batchQueue.shift()!;
433
- if (batch.type === "turn_batch") {
434
- await processExtraction(batch);
232
+ // 7. Artifacts
233
+ if (Array.isArray(result.artifacts) && result.artifacts.length > 0) {
234
+ for (const a of result.artifacts.slice(0, 10)) {
235
+ if (!a.path) continue;
236
+ if (priorState.artifactPaths.includes(a.path)) continue;
237
+ counts.artifact++;
238
+ priorState.artifactPaths.push(a.path);
239
+ const desc = `${String(a.action ?? "modified")}: ${String(a.summary ?? "").slice(0, 200)}`;
240
+ writeOps.push((async () => {
241
+ let emb: number[] | null = null;
242
+ if (embeddings.isAvailable()) {
243
+ try { emb = await embeddings.embed(`${a.path} ${desc}`); } catch (e) { swallow("daemon:embedArtifact", e); }
244
+ }
245
+ await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
246
+ })());
435
247
  }
436
248
  }
437
- }
438
249
 
439
- // --- Message Handler ---
440
-
441
- async function handleMessage(msg: DaemonMessage): Promise<void> {
442
- switch (msg.type) {
443
- case "turn_batch": {
444
- batchQueue.length = 0;
445
- batchQueue.push(msg);
446
- if (!processing) {
447
- drainQueue().catch(e => swallow.warn("daemon:drainQueue", e));
448
- }
449
- break;
250
+ // 8. Decisions
251
+ if (Array.isArray(result.decisions) && result.decisions.length > 0) {
252
+ for (const d of result.decisions.slice(0, 6)) {
253
+ if (!d.decision) continue;
254
+ counts.decision++;
255
+ const text = `[DECISION] ${String(d.decision).slice(0, 200)} — Rationale: ${String(d.rationale ?? "").slice(0, 200)} (Alternatives: ${String(d.alternatives_considered ?? "none").slice(0, 100)})`;
256
+ writeOps.push((async () => {
257
+ let emb: number[] | null = null;
258
+ if (embeddings.isAvailable()) {
259
+ try { emb = await embeddings.embed(text); } catch (e) { swallow("daemon:embedDecision", e); }
260
+ }
261
+ await store.createMemory(text, emb, 7, "decision", sessionId);
262
+ })());
450
263
  }
451
- case "shutdown": {
452
- shuttingDown = true;
453
- if (processing) {
454
- await Promise.race([
455
- new Promise<void>(resolve => {
456
- const check = setInterval(() => {
457
- if (!processing) { clearInterval(check); resolve(); }
458
- }, 100);
459
- }),
460
- new Promise<void>(resolve => setTimeout(resolve, 45_000)),
461
- ]);
462
- }
463
- try {
464
- await Promise.allSettled([
465
- store.dispose(),
466
- embeddings.dispose(),
467
- ]);
468
- } catch (e) { swallow("daemon:cleanup", e); }
264
+ }
469
265
 
470
- parentPort!.postMessage({ type: "shutdown_complete" } satisfies DaemonResponse);
471
- break;
472
- }
473
- case "status_request": {
474
- parentPort!.postMessage({
475
- type: "status",
476
- extractedTurns: counts.turns,
477
- pendingBatches: batchQueue.length,
478
- errors: counts.errors,
479
- } satisfies DaemonResponse);
480
- break;
266
+ // 9. Skills
267
+ if (Array.isArray(result.skills) && result.skills.length > 0) {
268
+ for (const s of result.skills.slice(0, 3)) {
269
+ if (!s.name || !Array.isArray(s.steps) || s.steps.length === 0) continue;
270
+ if (priorState.skillNames.includes(s.name)) continue;
271
+ counts.skill++;
272
+ priorState.skillNames.push(s.name);
273
+ const content = `${s.name}\nTrigger: ${String(s.trigger_context ?? "").slice(0, 150)}\nSteps:\n${s.steps.map((st: string, i: number) => `${i + 1}. ${String(st).slice(0, 200)}`).join("\n")}`;
274
+ writeOps.push((async () => {
275
+ let emb: number[] | null = null;
276
+ if (embeddings.isAvailable()) {
277
+ try { emb = await embeddings.embed(content); } catch (e) { swallow("daemon:embedSkill", e); }
278
+ }
279
+ await store.queryExec(
280
+ `CREATE skill CONTENT $record`,
281
+ {
282
+ record: {
283
+ name: String(s.name).slice(0, 100),
284
+ description: content,
285
+ content,
286
+ steps: s.steps.map((st: string) => String(st).slice(0, 200)),
287
+ trigger_context: String(s.trigger_context ?? "").slice(0, 200),
288
+ tags: ["auto-extracted"],
289
+ session_id: sessionId,
290
+ ...(emb ? { embedding: emb } : {}),
291
+ },
292
+ },
293
+ ).catch(e => swallow.warn("daemon:createSkill", e));
294
+ })());
481
295
  }
482
296
  }
483
- }
484
-
485
- // --- Main ---
486
297
 
487
- init().then(ok => {
488
- if (!ok) {
489
- parentPort!.postMessage({ type: "error", message: "Daemon initialization failed" } satisfies DaemonResponse);
490
- return;
491
- }
492
- parentPort!.on("message", (msg: DaemonMessage) => {
493
- handleMessage(msg).catch(e => swallow.warn("daemon:handleMessage", e));
494
- });
495
- });
298
+ await Promise.allSettled(writeOps);
299
+ return counts;
300
+ }
package/src/reflection.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * At session end, reviews own performance: tool failures, runaway detections,
5
5
  * low retrieval utilization, wasted tokens. If problems exceeded thresholds,
6
- * generates a structured reflection via Opus, stored as high-importance memory.
6
+ * generates a structured reflection via the configured LLM, stored as high-importance memory.
7
7
  * Retrieved when similar situations arise in future sessions.
8
8
  *
9
9
  * Ported from kongbrain — takes SurrealStore/EmbeddingService as params.
package/src/schema.surql CHANGED
@@ -97,6 +97,8 @@ DEFINE FIELD IF NOT EXISTS last_active ON session TYPE datetime DEFAULT time::no
97
97
  DEFINE FIELD IF NOT EXISTS turn_count ON session TYPE int DEFAULT 0;
98
98
  DEFINE FIELD IF NOT EXISTS total_input_tokens ON session TYPE int DEFAULT 0;
99
99
  DEFINE FIELD IF NOT EXISTS total_output_tokens ON session TYPE int DEFAULT 0;
100
+ DEFINE FIELD IF NOT EXISTS ended_at ON session TYPE option<datetime>;
101
+ DEFINE FIELD IF NOT EXISTS cleanup_completed ON session TYPE bool DEFAULT false;
100
102
 
101
103
  -- Long-term memory (episodic → consolidated)
102
104
  DEFINE TABLE IF NOT EXISTS memory SCHEMALESS;
package/src/state.ts CHANGED
@@ -34,7 +34,11 @@ export class SessionState {
34
34
  // Memory daemon
35
35
  daemon: MemoryDaemon | null = null;
36
36
  newContentTokens = 0;
37
- readonly DAEMON_TOKEN_THRESHOLD = 12000;
37
+ readonly DAEMON_TOKEN_THRESHOLD = 4000;
38
+ lastDaemonFlushTurnCount = 0;
39
+
40
+ // Cleanup tracking
41
+ cleanedUp = false;
38
42
 
39
43
  // Current adaptive config (set by orchestrator preflight each turn)
40
44
  currentConfig: AdaptiveConfig | null = null;
@@ -46,6 +50,7 @@ export class SessionState {
46
50
  agentId = "";
47
51
  projectId = "";
48
52
  taskId = "";
53
+ surrealSessionId = "";
49
54
 
50
55
  constructor(sessionId: string, sessionKey: string) {
51
56
  this.sessionId = sessionId;