@memtensor/memos-local-openclaw-plugin 0.3.20 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +239 -22
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +33 -8
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  11. package/dist/ingest/providers/anthropic.js +22 -8
  12. package/dist/ingest/providers/anthropic.js.map +1 -1
  13. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  14. package/dist/ingest/providers/bedrock.js +22 -8
  15. package/dist/ingest/providers/bedrock.js.map +1 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +22 -8
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +13 -18
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +213 -139
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +1 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +37 -17
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +28 -3
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +166 -67
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +97 -75
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/shared/llm-call.d.ts +26 -0
  35. package/dist/shared/llm-call.d.ts.map +1 -0
  36. package/dist/shared/llm-call.js +163 -0
  37. package/dist/shared/llm-call.js.map +1 -0
  38. package/dist/skill/evaluator.d.ts +0 -3
  39. package/dist/skill/evaluator.d.ts.map +1 -1
  40. package/dist/skill/evaluator.js +34 -59
  41. package/dist/skill/evaluator.js.map +1 -1
  42. package/dist/skill/evolver.d.ts +22 -1
  43. package/dist/skill/evolver.d.ts.map +1 -1
  44. package/dist/skill/evolver.js +191 -32
  45. package/dist/skill/evolver.js.map +1 -1
  46. package/dist/skill/generator.d.ts +0 -3
  47. package/dist/skill/generator.d.ts.map +1 -1
  48. package/dist/skill/generator.js +15 -50
  49. package/dist/skill/generator.js.map +1 -1
  50. package/dist/skill/upgrader.d.ts +0 -2
  51. package/dist/skill/upgrader.d.ts.map +1 -1
  52. package/dist/skill/upgrader.js +4 -39
  53. package/dist/skill/upgrader.js.map +1 -1
  54. package/dist/skill/validator.d.ts +0 -2
  55. package/dist/skill/validator.d.ts.map +1 -1
  56. package/dist/skill/validator.js +14 -44
  57. package/dist/skill/validator.js.map +1 -1
  58. package/dist/storage/sqlite.d.ts +13 -2
  59. package/dist/storage/sqlite.d.ts.map +1 -1
  60. package/dist/storage/sqlite.js +92 -15
  61. package/dist/storage/sqlite.js.map +1 -1
  62. package/dist/tools/memory-get.d.ts.map +1 -1
  63. package/dist/tools/memory-get.js +5 -1
  64. package/dist/tools/memory-get.js.map +1 -1
  65. package/dist/tools/memory-search.d.ts.map +1 -1
  66. package/dist/tools/memory-search.js +5 -0
  67. package/dist/tools/memory-search.js.map +1 -1
  68. package/dist/tools/memory-timeline.d.ts.map +1 -1
  69. package/dist/tools/memory-timeline.js +11 -2
  70. package/dist/tools/memory-timeline.js.map +1 -1
  71. package/dist/types.d.ts +2 -1
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/viewer/html.d.ts +1 -1
  76. package/dist/viewer/html.d.ts.map +1 -1
  77. package/dist/viewer/html.js +380 -26
  78. package/dist/viewer/html.js.map +1 -1
  79. package/dist/viewer/server.d.ts +9 -0
  80. package/dist/viewer/server.d.ts.map +1 -1
  81. package/dist/viewer/server.js +549 -184
  82. package/dist/viewer/server.js.map +1 -1
  83. package/index.ts +9 -3
  84. package/package.json +2 -1
  85. package/src/capture/index.ts +39 -10
  86. package/src/index.ts +3 -2
  87. package/src/ingest/providers/anthropic.ts +22 -8
  88. package/src/ingest/providers/bedrock.ts +22 -8
  89. package/src/ingest/providers/gemini.ts +22 -8
  90. package/src/ingest/providers/index.ts +192 -142
  91. package/src/ingest/providers/openai.ts +37 -17
  92. package/src/ingest/task-processor.ts +183 -65
  93. package/src/ingest/worker.ts +98 -77
  94. package/src/shared/llm-call.ts +144 -0
  95. package/src/skill/evaluator.ts +35 -64
  96. package/src/skill/evolver.ts +201 -33
  97. package/src/skill/generator.ts +16 -59
  98. package/src/skill/upgrader.ts +5 -43
  99. package/src/skill/validator.ts +15 -47
  100. package/src/storage/sqlite.ts +107 -15
  101. package/src/tools/memory-get.ts +6 -1
  102. package/src/tools/memory-search.ts +6 -0
  103. package/src/tools/memory-timeline.ts +13 -1
  104. package/src/types.ts +2 -1
  105. package/src/viewer/html.ts +380 -26
  106. package/src/viewer/server.ts +535 -197
@@ -4,10 +4,11 @@ import * as path from "path";
4
4
  import type { SqliteStore } from "../storage/sqlite";
5
5
  import type { RecallEngine } from "../recall/engine";
6
6
  import type { Embedder } from "../embedding";
7
- import type { Chunk, Task, Skill, PluginContext, SummarizerConfig, SkillGenerateOutput } from "../types";
7
+ import type { Chunk, Task, Skill, PluginContext, SkillGenerateOutput } from "../types";
8
8
  import { DEFAULTS } from "../types";
9
9
  import type { CreateEvalResult } from "./evaluator";
10
10
  import { SkillValidator } from "./validator";
11
+ import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
11
12
 
12
13
  // ─── Step 1: Generate SKILL.md ───
13
14
  // Based on Anthropic skill-creator principles:
@@ -138,6 +139,7 @@ Requirements:
138
139
  - Mix formal and casual tones, include some with typos or shorthand
139
140
  - Each prompt should be complex enough that the agent would need the skill (not simple Q&A)
140
141
  - Write expectations that are specific and verifiable
142
+ - LANGUAGE RULE: Write prompts and expectations in the SAME language as the skill content. If the skill is in Chinese, write Chinese test prompts. If English, write English.
141
143
 
142
144
  Skill:
143
145
  {SKILL_CONTENT}
@@ -161,6 +163,7 @@ Rules:
161
163
  - Each reference should be a standalone markdown document.
162
164
  - Don't duplicate what's already in SKILL.md — references are for deeper detail.
163
165
  - If there's nothing worth extracting, return an empty array.
166
+ - LANGUAGE RULE: Write reference content in the SAME language as the SKILL.md and task record. Chinese input → Chinese output.
164
167
 
165
168
  SKILL.md:
166
169
  {SKILL_CONTENT}
@@ -340,8 +343,8 @@ export class SkillGenerator {
340
343
  }
341
344
 
342
345
  private async step1GenerateSkillMd(task: Task, conversationText: string, evalResult: CreateEvalResult): Promise<string> {
343
- const cfg = this.getProviderConfig();
344
- if (!cfg) throw new Error("No LLM configured for skill generation");
346
+ const chain = buildSkillConfigChain(this.ctx);
347
+ if (chain.length === 0) throw new Error("No LLM configured for skill generation");
345
348
 
346
349
  const lang = this.detectLanguage(conversationText);
347
350
  const langInstruction = `\n\n⚠️ LANGUAGE REQUIREMENT: The task record is in ${lang}. You MUST write ALL prose content (description, headings, explanations, pitfalls) in ${lang}. Only the "name" field stays in English kebab-case.\n`;
@@ -353,7 +356,7 @@ export class SkillGenerator {
353
356
  .replace("{CONVERSATION}", conversationText.slice(0, 12000))
354
357
  + langInstruction;
355
358
 
356
- const raw = await this.callLLM(cfg, prompt, { maxTokens: 6000, temperature: 0.2 });
359
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.step1", { maxTokens: 6000, temperature: 0.2, timeoutMs: 120_000 });
357
360
 
358
361
  const trimmed = raw.trim();
359
362
  if (trimmed.startsWith("---")) return trimmed;
@@ -368,15 +371,15 @@ export class SkillGenerator {
368
371
  skillContent: string,
369
372
  conversationText: string,
370
373
  ): Promise<Array<{ filename: string; content: string }>> {
371
- const cfg = this.getProviderConfig();
372
- if (!cfg) return [];
374
+ const chain = buildSkillConfigChain(this.ctx);
375
+ if (chain.length === 0) return [];
373
376
 
374
377
  const prompt = STEP2_SCRIPTS_PROMPT
375
378
  .replace("{SKILL_CONTENT}", skillContent.slice(0, 4000))
376
379
  .replace("{CONVERSATION}", conversationText.slice(0, 6000));
377
380
 
378
381
  try {
379
- const raw = await this.callLLM(cfg, prompt, { maxTokens: 3000, temperature: 0.1 });
382
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.scripts", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000 });
380
383
  return this.parseJSONArray<{ filename: string; content: string }>(raw);
381
384
  } catch (err) {
382
385
  this.ctx.log.warn(`SkillGenerator: script extraction failed: ${err}`);
@@ -390,15 +393,15 @@ export class SkillGenerator {
390
393
  skillContent: string,
391
394
  conversationText: string,
392
395
  ): Promise<Array<{ filename: string; content: string }>> {
393
- const cfg = this.getProviderConfig();
394
- if (!cfg) return [];
396
+ const chain = buildSkillConfigChain(this.ctx);
397
+ if (chain.length === 0) return [];
395
398
 
396
399
  const prompt = STEP2B_REFS_PROMPT
397
400
  .replace("{SKILL_CONTENT}", skillContent.slice(0, 4000))
398
401
  .replace("{CONVERSATION}", conversationText.slice(0, 6000));
399
402
 
400
403
  try {
401
- const raw = await this.callLLM(cfg, prompt, { maxTokens: 3000, temperature: 0.1 });
404
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.refs", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000 });
402
405
  return this.parseJSONArray<{ filename: string; content: string }>(raw);
403
406
  } catch (err) {
404
407
  this.ctx.log.warn(`SkillGenerator: reference extraction failed: ${err}`);
@@ -411,8 +414,8 @@ export class SkillGenerator {
411
414
  private async step3GenerateEvals(
412
415
  skillContent: string,
413
416
  ): Promise<Array<{ id: number; prompt: string; expectations: string[]; trigger_confidence?: string }>> {
414
- const cfg = this.getProviderConfig();
415
- if (!cfg) return [];
417
+ const chain = buildSkillConfigChain(this.ctx);
418
+ if (chain.length === 0) return [];
416
419
 
417
420
  const lang = this.detectLanguage(skillContent);
418
421
  const prompt = STEP3_EVALS_PROMPT
@@ -420,7 +423,7 @@ export class SkillGenerator {
420
423
  + `\n\n⚠️ LANGUAGE: Write test prompts and expectations in ${lang}, matching the skill's language.\n`;
421
424
 
422
425
  try {
423
- const raw = await this.callLLM(cfg, prompt, { maxTokens: 2000, temperature: 0.3 });
426
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.evals", { maxTokens: 2000, temperature: 0.3, timeoutMs: 120_000 });
424
427
  return this.parseJSONArray(raw);
425
428
  } catch (err) {
426
429
  this.ctx.log.warn(`SkillGenerator: eval generation failed: ${err}`);
@@ -464,42 +467,6 @@ export class SkillGenerator {
464
467
  return { hitCount, results };
465
468
  }
466
469
 
467
- // ─── Shared LLM call ───
468
-
469
- private async callLLM(
470
- cfg: SummarizerConfig,
471
- prompt: string,
472
- opts: { maxTokens: number; temperature: number },
473
- ): Promise<string> {
474
- const endpoint = this.normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
475
- const model = cfg.model ?? "gpt-4o-mini";
476
- const headers: Record<string, string> = {
477
- "Content-Type": "application/json",
478
- Authorization: `Bearer ${cfg.apiKey}`,
479
- ...cfg.headers,
480
- };
481
-
482
- const resp = await fetch(endpoint, {
483
- method: "POST",
484
- headers,
485
- body: JSON.stringify({
486
- model,
487
- temperature: opts.temperature,
488
- max_tokens: opts.maxTokens,
489
- messages: [{ role: "user", content: prompt }],
490
- }),
491
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 120_000),
492
- });
493
-
494
- if (!resp.ok) {
495
- const body = await resp.text();
496
- throw new Error(`LLM call failed (${resp.status}): ${body}`);
497
- }
498
-
499
- const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
500
- return json.choices[0]?.message?.content?.trim() ?? "";
501
- }
502
-
503
470
  // ─── Helpers ───
504
471
 
505
472
  private parseJSONArray<T>(raw: string): T[] {
@@ -532,14 +499,4 @@ export class SkillGenerator {
532
499
  return "";
533
500
  }
534
501
 
535
- private getProviderConfig(): SummarizerConfig | undefined {
536
- return this.ctx.config.summarizer;
537
- }
538
-
539
- private normalizeEndpoint(url: string): string {
540
- const stripped = url.replace(/\/+$/, "");
541
- if (stripped.endsWith("/chat/completions")) return stripped;
542
- if (stripped.endsWith("/completions")) return stripped;
543
- return `${stripped}/chat/completions`;
544
- }
545
502
  }
@@ -2,9 +2,10 @@ import { v4 as uuid } from "uuid";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import type { SqliteStore } from "../storage/sqlite";
5
- import type { Task, Skill, PluginContext, SummarizerConfig } from "../types";
5
+ import type { Task, Skill, PluginContext } from "../types";
6
6
  import type { UpgradeEvalResult } from "./evaluator";
7
7
  import { SkillValidator } from "./validator";
8
+ import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
8
9
 
9
10
  const UPGRADE_PROMPT = `You are a Skill upgrade expert. You're merging new real-world execution experience into an existing Skill to make it better.
10
11
 
@@ -163,8 +164,8 @@ export class SkillUpgrader {
163
164
  currentContent: string,
164
165
  evalResult: UpgradeEvalResult,
165
166
  ): Promise<{ newContent: string; changelog: string; changeSummary: string }> {
166
- const cfg = this.getProviderConfig();
167
- if (!cfg) throw new Error("No LLM configured for skill upgrade");
167
+ const chain = buildSkillConfigChain(this.ctx);
168
+ if (chain.length === 0) throw new Error("No LLM configured for skill upgrade");
168
169
 
169
170
  const newVersion = skill.version + 1;
170
171
 
@@ -189,35 +190,7 @@ export class SkillUpgrader {
189
190
  .replace("{TASK_ID}", task.id)
190
191
  + langInstruction;
191
192
 
192
- const endpoint = this.normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
193
- const model = cfg.model ?? "gpt-4o-mini";
194
- const headers: Record<string, string> = {
195
- "Content-Type": "application/json",
196
- Authorization: `Bearer ${cfg.apiKey}`,
197
- ...cfg.headers,
198
- };
199
-
200
- const resp = await fetch(endpoint, {
201
- method: "POST",
202
- headers,
203
- body: JSON.stringify({
204
- model,
205
- temperature: cfg.temperature ?? 0.2,
206
- max_tokens: 6000,
207
- messages: [
208
- { role: "user", content: prompt },
209
- ],
210
- }),
211
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 90_000),
212
- });
213
-
214
- if (!resp.ok) {
215
- const body = await resp.text();
216
- throw new Error(`Skill upgrade LLM failed (${resp.status}): ${body}`);
217
- }
218
-
219
- const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
220
- const raw = json.choices[0]?.message?.content?.trim() ?? "";
193
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillUpgrader.upgrade", { maxTokens: 6000, temperature: 0.2, timeoutMs: 90_000 });
221
194
 
222
195
  const changelogSep = raw.indexOf("---CHANGELOG---");
223
196
  if (changelogSep !== -1) {
@@ -243,15 +216,4 @@ export class SkillUpgrader {
243
216
  if (match2) return match2[1];
244
217
  return "";
245
218
  }
246
-
247
- private getProviderConfig(): SummarizerConfig | undefined {
248
- return this.ctx.config.summarizer;
249
- }
250
-
251
- private normalizeEndpoint(url: string): string {
252
- const stripped = url.replace(/\/+$/, "");
253
- if (stripped.endsWith("/chat/completions")) return stripped;
254
- if (stripped.endsWith("/completions")) return stripped;
255
- return `${stripped}/chat/completions`;
256
- }
257
219
  }
@@ -1,7 +1,8 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import type { PluginContext, SummarizerConfig } from "../types";
3
+ import type { PluginContext } from "../types";
4
4
  import { DEFAULTS } from "../types";
5
+ import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
5
6
 
6
7
  export interface ValidationResult {
7
8
  valid: boolean;
@@ -133,44 +134,20 @@ export class SkillValidator {
133
134
  }
134
135
 
135
136
  private async assessQuality(dirPath: string, result: ValidationResult): Promise<void> {
136
- const cfg = this.getProviderConfig();
137
- if (!cfg) return;
137
+ const chain = buildSkillConfigChain(this.ctx);
138
+ if (chain.length === 0) return;
138
139
 
139
140
  const skillMdPath = path.join(dirPath, "SKILL.md");
140
141
  const content = fs.readFileSync(skillMdPath, "utf-8");
141
142
 
142
143
  const prompt = QUALITY_PROMPT.replace("{SKILL_CONTENT}", content.slice(0, 6000));
143
- const endpoint = this.normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
144
- const headers: Record<string, string> = {
145
- "Content-Type": "application/json",
146
- Authorization: `Bearer ${cfg.apiKey}`,
147
- ...cfg.headers,
148
- };
149
-
150
- const resp = await fetch(endpoint, {
151
- method: "POST",
152
- headers,
153
- body: JSON.stringify({
154
- model: cfg.model ?? "gpt-4o-mini",
155
- temperature: cfg.temperature ?? 0.1,
156
- max_tokens: 1024,
157
- messages: [{ role: "user", content: prompt }],
158
- }),
159
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
160
- });
161
-
162
- if (!resp.ok) {
163
- const body = await resp.text();
164
- throw new Error(`Quality LLM failed (${resp.status}): ${body}`);
165
- }
166
144
 
167
- const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
168
- const raw = json.choices[0]?.message?.content?.trim() ?? "";
145
+ try {
146
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillValidator.quality");
169
147
 
170
- const jsonMatch = raw.match(/\{[\s\S]*\}/);
171
- if (!jsonMatch) return;
148
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
149
+ if (!jsonMatch) return;
172
150
 
173
- try {
174
151
  const assessment = JSON.parse(jsonMatch[0]) as {
175
152
  score: number;
176
153
  strengths: string[];
@@ -189,21 +166,10 @@ export class SkillValidator {
189
166
  if (result.qualityScore < 6) {
190
167
  result.warnings.push(`Quality score ${result.qualityScore}/10 is below threshold, marked as draft`);
191
168
  }
192
- } catch {
193
- this.ctx.log.warn("SkillValidator: failed to parse quality assessment JSON");
169
+ } catch (err) {
170
+ this.ctx.log.warn(`SkillValidator: quality assessment failed: ${err}`);
194
171
  }
195
172
  }
196
-
197
- private getProviderConfig(): SummarizerConfig | undefined {
198
- return this.ctx.config.summarizer;
199
- }
200
-
201
- private normalizeEndpoint(url: string): string {
202
- const stripped = url.replace(/\/+$/, "");
203
- if (stripped.endsWith("/chat/completions")) return stripped;
204
- if (stripped.endsWith("/completions")) return stripped;
205
- return `${stripped}/chat/completions`;
206
- }
207
173
  }
208
174
 
209
175
  const QUALITY_PROMPT = `You are a skill quality reviewer. Evaluate the following SKILL.md and give a score from 0 to 10.
@@ -218,10 +184,12 @@ Criteria:
218
184
  SKILL.md:
219
185
  {SKILL_CONTENT}
220
186
 
187
+ LANGUAGE RULE: "strengths", "weaknesses", and "suggestions" MUST use the SAME language as the SKILL.md content. Chinese skill → Chinese feedback. English skill → English feedback.
188
+
221
189
  Reply in JSON only:
222
190
  {
223
191
  "score": 0-10,
224
- "strengths": ["what's good"],
225
- "weaknesses": ["what's lacking"],
226
- "suggestions": ["how to improve"]
192
+ "strengths": ["what's good (same language as skill)"],
193
+ "weaknesses": ["what's lacking (same language as skill)"],
194
+ "suggestions": ["how to improve (same language as skill)"]
227
195
  }`;
@@ -306,6 +306,14 @@ export class SqliteStore {
306
306
  .run(meta.skillStatus, meta.skillReason, Date.now(), taskId);
307
307
  }
308
308
 
309
+ getTasksBySkillStatus(statuses: string[]): Task[] {
310
+ const placeholders = statuses.map(() => "?").join(",");
311
+ const rows = this.db.prepare(
312
+ `SELECT * FROM tasks WHERE skill_status IN (${placeholders}) AND status = 'completed' ORDER BY updated_at ASC`,
313
+ ).all(...statuses) as TaskRow[];
314
+ return rows.map(rowToTask);
315
+ }
316
+
309
317
  private migrateMergeFields(): void {
310
318
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
311
319
  if (!cols.some((c) => c.name === "merge_count")) {
@@ -633,6 +641,10 @@ export class SqliteStore {
633
641
  `).run(chunkId, buf, vector.length, Date.now());
634
642
  }
635
643
 
644
+ deleteEmbedding(chunkId: string): void {
645
+ this.db.prepare("DELETE FROM embeddings WHERE chunk_id = ?").run(chunkId);
646
+ }
647
+
636
648
  // ─── Read ───
637
649
 
638
650
  getChunk(chunkId: string): Chunk | null {
@@ -640,16 +652,37 @@ export class SqliteStore {
640
652
  return row ? rowToChunk(row) : null;
641
653
  }
642
654
 
643
- getChunksByRef(ref: ChunkRef): Chunk | null {
644
- return this.getChunk(ref.chunkId);
655
+ getChunkForOwners(chunkId: string, ownerFilter?: string[]): Chunk | null {
656
+ if (!ownerFilter || ownerFilter.length === 0) return this.getChunk(chunkId);
657
+
658
+ const placeholders = ownerFilter.map(() => "?").join(",");
659
+ const row = this.db.prepare(
660
+ `SELECT * FROM chunks WHERE id = ? AND owner IN (${placeholders}) LIMIT 1`,
661
+ ).get(chunkId, ...ownerFilter) as ChunkRow | undefined;
662
+ return row ? rowToChunk(row) : null;
645
663
  }
646
664
 
647
- getNeighborChunks(sessionKey: string, turnId: string, seq: number, window: number): Chunk[] {
648
- const allRows = this.db.prepare(`
665
+ getChunksByRef(ref: ChunkRef, ownerFilter?: string[]): Chunk | null {
666
+ return this.getChunkForOwners(ref.chunkId, ownerFilter);
667
+ }
668
+
669
+ getNeighborChunks(sessionKey: string, turnId: string, seq: number, window: number, ownerFilter?: string[]): Chunk[] {
670
+ let sql = `
649
671
  SELECT * FROM chunks
650
- WHERE session_key = ?
672
+ WHERE session_key = ?`;
673
+ const params: any[] = [sessionKey];
674
+
675
+ if (ownerFilter && ownerFilter.length > 0) {
676
+ const placeholders = ownerFilter.map(() => "?").join(",");
677
+ sql += ` AND owner IN (${placeholders})`;
678
+ params.push(...ownerFilter);
679
+ }
680
+
681
+ sql += `
651
682
  ORDER BY created_at, seq
652
- `).all(sessionKey) as ChunkRow[];
683
+ `;
684
+
685
+ const allRows = this.db.prepare(sql).all(...params) as ChunkRow[];
653
686
 
654
687
  const targetIdx = allRows.findIndex(
655
688
  (r) => r.turn_id === turnId && r.seq === seq,
@@ -840,20 +873,46 @@ export class SqliteStore {
840
873
 
841
874
  deleteAll(): number {
842
875
  this.db.exec("PRAGMA foreign_keys = OFF");
843
- this.db.prepare("DELETE FROM task_skills").run();
844
- this.db.prepare("DELETE FROM skill_versions").run();
845
- this.db.prepare("DELETE FROM skills").run();
846
- this.db.prepare("DELETE FROM embeddings").run();
847
- this.db.prepare("DELETE FROM chunks").run();
848
- this.db.prepare("DELETE FROM tasks").run();
849
- this.db.prepare("DELETE FROM viewer_events").run();
850
- this.db.prepare("DELETE FROM api_logs").run();
851
- this.db.prepare("DELETE FROM tool_calls").run();
876
+ const tables = [
877
+ "task_skills",
878
+ "skill_embeddings",
879
+ "skill_versions",
880
+ "skills",
881
+ "embeddings",
882
+ "chunks",
883
+ "tasks",
884
+ "viewer_events",
885
+ "api_logs",
886
+ "tool_calls",
887
+ ];
888
+ for (const table of tables) {
889
+ try {
890
+ this.db.prepare(`DELETE FROM ${table}`).run();
891
+ } catch (err) {
892
+ this.log.warn(`deleteAll: failed to clear ${table}: ${err}`);
893
+ }
894
+ }
852
895
  this.db.exec("PRAGMA foreign_keys = ON");
853
896
  const remaining = this.countChunks();
854
897
  return remaining === 0 ? 1 : 0;
855
898
  }
856
899
 
900
+ deleteTask(taskId: string): boolean {
901
+ this.db.prepare("DELETE FROM task_skills WHERE task_id = ?").run(taskId);
902
+ this.db.prepare("UPDATE chunks SET task_id = NULL WHERE task_id = ?").run(taskId);
903
+ const result = this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
904
+ return result.changes > 0;
905
+ }
906
+
907
+ deleteSkill(skillId: string): boolean {
908
+ this.db.prepare("DELETE FROM task_skills WHERE skill_id = ?").run(skillId);
909
+ this.db.prepare("DELETE FROM skill_versions WHERE skill_id = ?").run(skillId);
910
+ this.db.prepare("DELETE FROM skill_embeddings WHERE skill_id = ?").run(skillId);
911
+ this.db.prepare("UPDATE chunks SET skill_id = NULL WHERE skill_id = ?").run(skillId);
912
+ const result = this.db.prepare("DELETE FROM skills WHERE id = ?").run(skillId);
913
+ return result.changes > 0;
914
+ }
915
+
857
916
  // ─── Task CRUD ───
858
917
 
859
918
  insertTask(task: Task): void {
@@ -989,6 +1048,24 @@ export class SqliteStore {
989
1048
  return !!row;
990
1049
  }
991
1050
 
1051
+ /**
1052
+ * Find an active chunk with the same content_hash within the same owner (agent dimension).
1053
+ * Returns the existing chunk ID if found, null otherwise.
1054
+ */
1055
+ findActiveChunkByHash(content: string, owner?: string): string | null {
1056
+ const hash = contentHash(content);
1057
+ if (owner) {
1058
+ const row = this.db.prepare(
1059
+ "SELECT id FROM chunks WHERE content_hash = ? AND dedup_status = 'active' AND owner = ? LIMIT 1",
1060
+ ).get(hash, owner) as { id: string } | undefined;
1061
+ return row?.id ?? null;
1062
+ }
1063
+ const row = this.db.prepare(
1064
+ "SELECT id FROM chunks WHERE content_hash = ? AND dedup_status = 'active' LIMIT 1",
1065
+ ).get(hash) as { id: string } | undefined;
1066
+ return row?.id ?? null;
1067
+ }
1068
+
992
1069
  // ─── Util ───
993
1070
 
994
1071
  getRecentChunkIds(limit: number): string[] {
@@ -1161,6 +1238,10 @@ export class SqliteStore {
1161
1238
  // ─── Task-Skill Links ───
1162
1239
 
1163
1240
  linkTaskSkill(taskId: string, skillId: string, relation: TaskSkillRelation, versionAt: number): void {
1241
+ const skillExists = this.db.prepare("SELECT 1 FROM skills WHERE id = ?").get(skillId);
1242
+ if (!skillExists) return;
1243
+ const taskExists = this.db.prepare("SELECT 1 FROM tasks WHERE id = ?").get(taskId);
1244
+ if (!taskExists) return;
1164
1245
  this.db.prepare(`
1165
1246
  INSERT OR REPLACE INTO task_skills (task_id, skill_id, relation, version_at, created_at)
1166
1247
  VALUES (?, ?, ?, ?, ?)
@@ -1211,6 +1292,17 @@ export class SqliteStore {
1211
1292
  .map(r => r.session_key);
1212
1293
  }
1213
1294
 
1295
+ getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
1296
+ const result = new Map<string, string>();
1297
+ if (sessionKeys.length === 0) return result;
1298
+ const placeholders = sessionKeys.map(() => "?").join(",");
1299
+ const rows = this.db.prepare(
1300
+ `SELECT session_key, owner FROM chunks WHERE session_key IN (${placeholders}) AND owner IS NOT NULL GROUP BY session_key`,
1301
+ ).all(...sessionKeys) as Array<{ session_key: string; owner: string }>;
1302
+ for (const r of rows) result.set(r.session_key, r.owner);
1303
+ return result;
1304
+ }
1305
+
1214
1306
  close(): void {
1215
1307
  this.db.close();
1216
1308
  }
@@ -2,6 +2,11 @@ import type { SqliteStore } from "../storage/sqlite";
2
2
  import type { ToolDefinition, GetResult, ChunkRef } from "../types";
3
3
  import { DEFAULTS } from "../types";
4
4
 
5
+ function resolveOwnerFilter(owner: unknown): string[] {
6
+ const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
7
+ return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
8
+ }
9
+
5
10
  export function createMemoryGetTool(store: SqliteStore): ToolDefinition {
6
11
  return {
7
12
  name: "memory_get",
@@ -36,7 +41,7 @@ export function createMemoryGetTool(store: SqliteStore): ToolDefinition {
36
41
  DEFAULTS.getMaxCharsMax,
37
42
  );
38
43
 
39
- const chunk = store.getChunksByRef(ref);
44
+ const chunk = store.getChunksByRef(ref, resolveOwnerFilter(input.owner));
40
45
 
41
46
  if (!chunk) {
42
47
  return { error: `Chunk not found: ${ref.chunkId}` };
@@ -1,6 +1,11 @@
1
1
  import type { RecallEngine } from "../recall/engine";
2
2
  import type { ToolDefinition } from "../types";
3
3
 
4
+ function resolveOwnerFilter(owner: unknown): string[] {
5
+ const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
6
+ return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
7
+ }
8
+
4
9
  export function createMemorySearchTool(engine: RecallEngine): ToolDefinition {
5
10
  return {
6
11
  name: "memory_search",
@@ -29,6 +34,7 @@ export function createMemorySearchTool(engine: RecallEngine): ToolDefinition {
29
34
  query: (input.query as string) ?? "",
30
35
  maxResults: input.maxResults as number | undefined,
31
36
  minScore: input.minScore as number | undefined,
37
+ ownerFilter: resolveOwnerFilter(input.owner),
32
38
  });
33
39
  return result;
34
40
  },
@@ -2,6 +2,11 @@ import type { SqliteStore } from "../storage/sqlite";
2
2
  import type { ToolDefinition, TimelineResult, TimelineEntry, ChunkRef } from "../types";
3
3
  import { DEFAULTS } from "../types";
4
4
 
5
+ function resolveOwnerFilter(owner: unknown): string[] {
6
+ const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
7
+ return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
8
+ }
9
+
5
10
  export function createMemoryTimelineTool(store: SqliteStore): ToolDefinition {
6
11
  return {
7
12
  name: "memory_timeline",
@@ -33,18 +38,25 @@ export function createMemoryTimelineTool(store: SqliteStore): ToolDefinition {
33
38
  const ref = input.ref as ChunkRef;
34
39
  const window = (input.window as number) ?? DEFAULTS.timelineWindowDefault;
35
40
 
41
+ const ownerFilter = resolveOwnerFilter(input.owner);
42
+ const anchorChunk = store.getChunksByRef(ref, ownerFilter);
43
+ if (!anchorChunk) {
44
+ return { entries: [], anchorRef: ref } satisfies TimelineResult;
45
+ }
46
+
36
47
  const neighbors = store.getNeighborChunks(
37
48
  ref.sessionKey,
38
49
  ref.turnId,
39
50
  ref.seq,
40
51
  window,
52
+ ownerFilter,
41
53
  );
42
54
 
43
55
  const entries: TimelineEntry[] = neighbors.map((chunk) => {
44
56
  let relation: TimelineEntry["relation"] = "before";
45
57
  if (chunk.id === ref.chunkId) {
46
58
  relation = "current";
47
- } else if (chunk.createdAt > (store.getChunk(ref.chunkId)?.createdAt ?? 0)) {
59
+ } else if (chunk.createdAt > anchorChunk.createdAt) {
48
60
  relation = "after";
49
61
  }
50
62
 
package/src/types.ts CHANGED
@@ -247,6 +247,7 @@ export interface SkillEvolutionConfig {
247
247
  minConfidence?: number;
248
248
  maxSkillLines?: number;
249
249
  autoInstall?: boolean;
250
+ summarizer?: SummarizerConfig;
250
251
  }
251
252
 
252
253
  export interface TelemetryConfig {
@@ -293,7 +294,7 @@ export const DEFAULTS = {
293
294
  mmrLambda: 0.7,
294
295
  recencyHalfLifeDays: 14,
295
296
  vectorSearchMaxChunks: 0,
296
- dedupSimilarityThreshold: 0.93,
297
+ dedupSimilarityThreshold: 0.60,
297
298
  evidenceWrapperTag: "STORED_MEMORY",
298
299
  excerptMinChars: 200,
299
300
  excerptMaxChars: 500,