@memtensor/memos-local-openclaw-plugin 0.1.3 → 0.1.5
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/.env.example +13 -5
- package/README.md +283 -91
- package/dist/capture/index.d.ts +5 -7
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +72 -43
- package/dist/capture/index.js.map +1 -1
- package/dist/ingest/dedup.d.ts +8 -0
- package/dist/ingest/dedup.d.ts.map +1 -1
- package/dist/ingest/dedup.js +21 -0
- package/dist/ingest/dedup.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +16 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +214 -1
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +16 -5
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +210 -6
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +16 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +202 -1
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +31 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +134 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +24 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +255 -1
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +65 -0
- package/dist/ingest/task-processor.d.ts.map +1 -0
- package/dist/ingest/task-processor.js +354 -0
- package/dist/ingest/task-processor.js.map +1 -0
- package/dist/ingest/worker.d.ts +3 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +131 -23
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts +1 -0
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +22 -11
- package/dist/recall/engine.js.map +1 -1
- package/dist/recall/mmr.d.ts.map +1 -1
- package/dist/recall/mmr.js +3 -1
- package/dist/recall/mmr.js.map +1 -1
- package/dist/skill/bundled-memory-guide.d.ts +6 -0
- package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
- package/dist/skill/bundled-memory-guide.js +95 -0
- package/dist/skill/bundled-memory-guide.js.map +1 -0
- package/dist/skill/evaluator.d.ts +31 -0
- package/dist/skill/evaluator.d.ts.map +1 -0
- package/dist/skill/evaluator.js +194 -0
- package/dist/skill/evaluator.js.map +1 -0
- package/dist/skill/evolver.d.ts +22 -0
- package/dist/skill/evolver.d.ts.map +1 -0
- package/dist/skill/evolver.js +193 -0
- package/dist/skill/evolver.js.map +1 -0
- package/dist/skill/generator.d.ts +25 -0
- package/dist/skill/generator.d.ts.map +1 -0
- package/dist/skill/generator.js +477 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/installer.d.ts +16 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +89 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/upgrader.d.ts +19 -0
- package/dist/skill/upgrader.d.ts.map +1 -0
- package/dist/skill/upgrader.js +263 -0
- package/dist/skill/upgrader.js.map +1 -0
- package/dist/skill/validator.d.ts +29 -0
- package/dist/skill/validator.d.ts.map +1 -0
- package/dist/skill/validator.js +227 -0
- package/dist/skill/validator.js.map +1 -0
- package/dist/storage/sqlite.d.ts +141 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +664 -7
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2391 -159
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +16 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +346 -3
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +572 -89
- package/openclaw.plugin.json +20 -45
- package/package.json +3 -4
- package/skill/memos-memory-guide/SKILL.md +86 -0
- package/src/capture/index.ts +85 -45
- package/src/ingest/dedup.ts +29 -0
- package/src/ingest/providers/anthropic.ts +258 -1
- package/src/ingest/providers/bedrock.ts +256 -6
- package/src/ingest/providers/gemini.ts +252 -1
- package/src/ingest/providers/index.ts +156 -8
- package/src/ingest/providers/openai.ts +304 -1
- package/src/ingest/task-processor.ts +396 -0
- package/src/ingest/worker.ts +145 -34
- package/src/recall/engine.ts +23 -12
- package/src/recall/mmr.ts +3 -1
- package/src/skill/bundled-memory-guide.ts +91 -0
- package/src/skill/evaluator.ts +220 -0
- package/src/skill/evolver.ts +169 -0
- package/src/skill/generator.ts +506 -0
- package/src/skill/installer.ts +59 -0
- package/src/skill/upgrader.ts +257 -0
- package/src/skill/validator.ts +227 -0
- package/src/storage/sqlite.ts +802 -7
- package/src/types.ts +96 -0
- package/src/viewer/html.ts +2391 -159
- package/src/viewer/server.ts +346 -3
- package/SKILL.md +0 -43
- package/www/index.html +0 -632
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import * as fs from "fs";
|
|
3
4
|
import * as path from "path";
|
|
4
|
-
import type { Chunk, ChunkRef, Logger } from "../types";
|
|
5
|
+
import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
|
|
5
6
|
|
|
6
7
|
export class SqliteStore {
|
|
7
8
|
private db: Database.Database;
|
|
@@ -69,16 +70,437 @@ export class SqliteStore {
|
|
|
69
70
|
dimensions INTEGER NOT NULL,
|
|
70
71
|
updated_at INTEGER NOT NULL
|
|
71
72
|
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS viewer_events (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
event_type TEXT NOT NULL,
|
|
77
|
+
created_at INTEGER NOT NULL
|
|
78
|
+
);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_viewer_events_created ON viewer_events(created_at);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_viewer_events_type ON viewer_events(event_type);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
session_key TEXT NOT NULL,
|
|
85
|
+
title TEXT NOT NULL DEFAULT '',
|
|
86
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
87
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
88
|
+
started_at INTEGER NOT NULL,
|
|
89
|
+
ended_at INTEGER,
|
|
90
|
+
updated_at INTEGER NOT NULL
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_key);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
72
94
|
`);
|
|
95
|
+
|
|
96
|
+
this.migrateTaskId();
|
|
97
|
+
this.migrateContentHash();
|
|
98
|
+
this.migrateSkillTables();
|
|
99
|
+
this.migrateSkillId();
|
|
100
|
+
this.migrateSkillQualityScore();
|
|
101
|
+
this.migrateTaskSkillMeta();
|
|
102
|
+
this.migrateToolCalls();
|
|
103
|
+
this.migrateMergeFields();
|
|
104
|
+
this.migrateApiLogs();
|
|
105
|
+
this.migrateDedupStatus();
|
|
73
106
|
this.log.debug("Database schema initialized");
|
|
74
107
|
}
|
|
75
108
|
|
|
109
|
+
private migrateTaskId(): void {
|
|
110
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
111
|
+
if (!cols.some((c) => c.name === "task_id")) {
|
|
112
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN task_id TEXT REFERENCES tasks(id)");
|
|
113
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_task ON chunks(task_id)");
|
|
114
|
+
this.log.info("Migrated: added task_id column to chunks");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private migrateContentHash(): void {
|
|
119
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
120
|
+
if (!cols.some((c) => c.name === "content_hash")) {
|
|
121
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN content_hash TEXT");
|
|
122
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup ON chunks(session_key, role, content_hash)");
|
|
123
|
+
|
|
124
|
+
// Backfill existing rows
|
|
125
|
+
const rows = this.db.prepare("SELECT id, content FROM chunks WHERE content_hash IS NULL").all() as Array<{ id: string; content: string }>;
|
|
126
|
+
const updateStmt = this.db.prepare("UPDATE chunks SET content_hash = ? WHERE id = ?");
|
|
127
|
+
for (const r of rows) {
|
|
128
|
+
updateStmt.run(contentHash(r.content), r.id);
|
|
129
|
+
}
|
|
130
|
+
if (rows.length > 0) {
|
|
131
|
+
this.log.info(`Migrated: backfilled content_hash for ${rows.length} chunks`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private migrateSkillTables(): void {
|
|
137
|
+
this.db.exec(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
name TEXT NOT NULL UNIQUE,
|
|
141
|
+
description TEXT NOT NULL DEFAULT '',
|
|
142
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
143
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
144
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
145
|
+
source_type TEXT NOT NULL DEFAULT 'task',
|
|
146
|
+
dir_path TEXT NOT NULL DEFAULT '',
|
|
147
|
+
installed INTEGER NOT NULL DEFAULT 0,
|
|
148
|
+
created_at INTEGER NOT NULL,
|
|
149
|
+
updated_at INTEGER NOT NULL
|
|
150
|
+
);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
155
|
+
id TEXT PRIMARY KEY,
|
|
156
|
+
skill_id TEXT NOT NULL REFERENCES skills(id),
|
|
157
|
+
version INTEGER NOT NULL,
|
|
158
|
+
content TEXT NOT NULL,
|
|
159
|
+
changelog TEXT NOT NULL DEFAULT '',
|
|
160
|
+
upgrade_type TEXT NOT NULL DEFAULT 'create',
|
|
161
|
+
source_task_id TEXT,
|
|
162
|
+
metrics TEXT NOT NULL DEFAULT '{}',
|
|
163
|
+
created_at INTEGER NOT NULL,
|
|
164
|
+
UNIQUE(skill_id, version)
|
|
165
|
+
);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_skill_versions_skill ON skill_versions(skill_id);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS task_skills (
|
|
169
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
170
|
+
skill_id TEXT NOT NULL REFERENCES skills(id),
|
|
171
|
+
relation TEXT NOT NULL DEFAULT 'generated_from',
|
|
172
|
+
version_at INTEGER NOT NULL DEFAULT 1,
|
|
173
|
+
created_at INTEGER NOT NULL,
|
|
174
|
+
PRIMARY KEY (task_id, skill_id)
|
|
175
|
+
);
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private migrateSkillId(): void {
|
|
180
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
181
|
+
if (!cols.some((c) => c.name === "skill_id")) {
|
|
182
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN skill_id TEXT");
|
|
183
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_skill ON chunks(skill_id)");
|
|
184
|
+
this.log.info("Migrated: added skill_id column to chunks");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private migrateSkillQualityScore(): void {
|
|
189
|
+
const skillCols = this.db.prepare("PRAGMA table_info(skills)").all() as Array<{ name: string }>;
|
|
190
|
+
if (!skillCols.some((c) => c.name === "quality_score")) {
|
|
191
|
+
this.db.exec("ALTER TABLE skills ADD COLUMN quality_score REAL");
|
|
192
|
+
this.log.info("Migrated: added quality_score column to skills");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const versionCols = this.db.prepare("PRAGMA table_info(skill_versions)").all() as Array<{ name: string }>;
|
|
196
|
+
if (!versionCols.some((c) => c.name === "quality_score")) {
|
|
197
|
+
this.db.exec("ALTER TABLE skill_versions ADD COLUMN quality_score REAL");
|
|
198
|
+
this.log.info("Migrated: added quality_score column to skill_versions");
|
|
199
|
+
}
|
|
200
|
+
if (!versionCols.some((c) => c.name === "change_summary")) {
|
|
201
|
+
this.db.exec("ALTER TABLE skill_versions ADD COLUMN change_summary TEXT NOT NULL DEFAULT ''");
|
|
202
|
+
this.log.info("Migrated: added change_summary column to skill_versions");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private migrateTaskSkillMeta(): void {
|
|
207
|
+
const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
|
208
|
+
if (!cols.some((c) => c.name === "skill_status")) {
|
|
209
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN skill_status TEXT DEFAULT NULL");
|
|
210
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN skill_reason TEXT DEFAULT NULL");
|
|
211
|
+
this.log.info("Migrated: added skill_status/skill_reason columns to tasks");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setTaskSkillMeta(taskId: string, meta: { skillStatus: string; skillReason: string }): void {
|
|
216
|
+
this.db.prepare("UPDATE tasks SET skill_status = ?, skill_reason = ?, updated_at = ? WHERE id = ?")
|
|
217
|
+
.run(meta.skillStatus, meta.skillReason, Date.now(), taskId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private migrateMergeFields(): void {
|
|
221
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
222
|
+
if (!cols.some((c) => c.name === "merge_count")) {
|
|
223
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN merge_count INTEGER NOT NULL DEFAULT 0");
|
|
224
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN last_hit_at INTEGER");
|
|
225
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN merge_history TEXT NOT NULL DEFAULT '[]'");
|
|
226
|
+
this.log.info("Migrated: added merge_count/last_hit_at/merge_history columns to chunks");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private migrateApiLogs(): void {
|
|
231
|
+
this.db.exec(`
|
|
232
|
+
CREATE TABLE IF NOT EXISTS api_logs (
|
|
233
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
234
|
+
tool_name TEXT NOT NULL,
|
|
235
|
+
input_data TEXT NOT NULL DEFAULT '{}',
|
|
236
|
+
output_data TEXT NOT NULL DEFAULT '',
|
|
237
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
239
|
+
called_at INTEGER NOT NULL
|
|
240
|
+
);
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_api_logs_at ON api_logs(called_at);
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_api_logs_name ON api_logs(tool_name);
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private migrateDedupStatus(): void {
|
|
247
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
248
|
+
if (!cols.some((c) => c.name === "dedup_status")) {
|
|
249
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_status TEXT NOT NULL DEFAULT 'active'");
|
|
250
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_target TEXT DEFAULT NULL");
|
|
251
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_reason TEXT DEFAULT NULL");
|
|
252
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_status ON chunks(dedup_status)");
|
|
253
|
+
this.log.info("Migrated: added dedup_status/dedup_target/dedup_reason columns to chunks");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
recordApiLog(toolName: string, input: unknown, output: string, durationMs: number, success: boolean): void {
|
|
258
|
+
const inputStr = typeof input === "string" ? input : JSON.stringify(input ?? {});
|
|
259
|
+
this.db.prepare(
|
|
260
|
+
"INSERT INTO api_logs (tool_name, input_data, output_data, duration_ms, success, called_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
261
|
+
).run(toolName, inputStr, output, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getApiLogs(limit: number = 50, offset: number = 0, toolFilter?: string): {
|
|
265
|
+
logs: Array<{ id: number; toolName: string; input: string; output: string; durationMs: number; success: boolean; calledAt: number }>;
|
|
266
|
+
total: number;
|
|
267
|
+
} {
|
|
268
|
+
const whereClause = toolFilter ? " WHERE tool_name = ?" : "";
|
|
269
|
+
const filterParams: unknown[] = toolFilter ? [toolFilter] : [];
|
|
270
|
+
|
|
271
|
+
const countRow = this.db.prepare("SELECT COUNT(*) as c FROM api_logs" + whereClause).get(...filterParams) as { c: number };
|
|
272
|
+
|
|
273
|
+
const rows = this.db.prepare(
|
|
274
|
+
"SELECT id, tool_name, input_data, output_data, duration_ms, success, called_at FROM api_logs" +
|
|
275
|
+
whereClause + " ORDER BY called_at DESC LIMIT ? OFFSET ?",
|
|
276
|
+
).all(...filterParams, limit, offset) as Array<{
|
|
277
|
+
id: number; tool_name: string; input_data: string; output_data: string;
|
|
278
|
+
duration_ms: number; success: number; called_at: number;
|
|
279
|
+
}>;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
logs: rows.map((r) => ({
|
|
283
|
+
id: r.id,
|
|
284
|
+
toolName: r.tool_name,
|
|
285
|
+
input: r.input_data,
|
|
286
|
+
output: r.output_data,
|
|
287
|
+
durationMs: r.duration_ms,
|
|
288
|
+
success: r.success === 1,
|
|
289
|
+
calledAt: r.called_at,
|
|
290
|
+
})),
|
|
291
|
+
total: countRow.c,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getApiLogToolNames(): string[] {
|
|
296
|
+
const rows = this.db.prepare("SELECT DISTINCT tool_name FROM api_logs ORDER BY tool_name").all() as Array<{ tool_name: string }>;
|
|
297
|
+
return rows.map((r) => r.tool_name);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
recordMergeHit(chunkId: string, action: "DUPLICATE" | "UPDATE", reason: string, oldSummary?: string, newSummary?: string): void {
|
|
301
|
+
const chunk = this.getChunk(chunkId);
|
|
302
|
+
if (!chunk) return;
|
|
303
|
+
|
|
304
|
+
const history = JSON.parse(chunk.mergeHistory || "[]") as any[];
|
|
305
|
+
const entry: Record<string, unknown> = { at: Date.now(), action, reason };
|
|
306
|
+
if (action === "UPDATE" && oldSummary && newSummary) {
|
|
307
|
+
entry.from = oldSummary;
|
|
308
|
+
entry.to = newSummary;
|
|
309
|
+
}
|
|
310
|
+
history.push(entry);
|
|
311
|
+
|
|
312
|
+
this.db.prepare(`
|
|
313
|
+
UPDATE chunks SET merge_count = merge_count + 1, last_hit_at = ?, merge_history = ?, updated_at = ?
|
|
314
|
+
WHERE id = ?
|
|
315
|
+
`).run(Date.now(), JSON.stringify(history), Date.now(), chunkId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
updateChunkSummaryAndContent(chunkId: string, newSummary: string, appendContent: string): void {
|
|
319
|
+
this.db.prepare(`
|
|
320
|
+
UPDATE chunks SET summary = ?, content = content || ? || ?, updated_at = ? WHERE id = ?
|
|
321
|
+
`).run(newSummary, "\n\n---\n\n", appendContent, Date.now(), chunkId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private migrateToolCalls(): void {
|
|
325
|
+
this.db.exec(`
|
|
326
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
327
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
328
|
+
tool_name TEXT NOT NULL,
|
|
329
|
+
duration_ms INTEGER NOT NULL,
|
|
330
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
331
|
+
called_at INTEGER NOT NULL
|
|
332
|
+
);
|
|
333
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_at ON tool_calls(called_at);
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
recordToolCall(toolName: string, durationMs: number, success: boolean): void {
|
|
339
|
+
this.db.prepare(
|
|
340
|
+
"INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)",
|
|
341
|
+
).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getToolMetrics(minutes: number): {
|
|
345
|
+
tools: string[];
|
|
346
|
+
series: Array<{ minute: string; [tool: string]: number | string }>;
|
|
347
|
+
aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
|
|
348
|
+
} {
|
|
349
|
+
const since = Date.now() - minutes * 60 * 1000;
|
|
350
|
+
|
|
351
|
+
const rows = this.db.prepare(
|
|
352
|
+
`SELECT tool_name,
|
|
353
|
+
duration_ms,
|
|
354
|
+
success,
|
|
355
|
+
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
356
|
+
FROM tool_calls
|
|
357
|
+
WHERE called_at >= ?
|
|
358
|
+
ORDER BY called_at`,
|
|
359
|
+
).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
360
|
+
|
|
361
|
+
const toolSet = new Set<string>();
|
|
362
|
+
const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
|
|
363
|
+
const aggMap = new Map<string, { durations: number[]; errors: number }>();
|
|
364
|
+
|
|
365
|
+
for (const r of rows) {
|
|
366
|
+
toolSet.add(r.tool_name);
|
|
367
|
+
|
|
368
|
+
if (!aggMap.has(r.tool_name)) aggMap.set(r.tool_name, { durations: [], errors: 0 });
|
|
369
|
+
const agg = aggMap.get(r.tool_name)!;
|
|
370
|
+
agg.durations.push(r.duration_ms);
|
|
371
|
+
if (!r.success) agg.errors++;
|
|
372
|
+
|
|
373
|
+
if (!minuteMap.has(r.minute_key)) minuteMap.set(r.minute_key, new Map());
|
|
374
|
+
const toolMap = minuteMap.get(r.minute_key)!;
|
|
375
|
+
if (!toolMap.has(r.tool_name)) toolMap.set(r.tool_name, { total: 0, count: 0 });
|
|
376
|
+
const entry = toolMap.get(r.tool_name)!;
|
|
377
|
+
entry.total += r.duration_ms;
|
|
378
|
+
entry.count++;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const tools = Array.from(toolSet).sort();
|
|
382
|
+
|
|
383
|
+
const allMinutes: string[] = [];
|
|
384
|
+
if (minutes > 0) {
|
|
385
|
+
const startMinute = new Date(since);
|
|
386
|
+
startMinute.setSeconds(0, 0);
|
|
387
|
+
const now = new Date();
|
|
388
|
+
for (let t = startMinute.getTime(); t <= now.getTime(); t += 60000) {
|
|
389
|
+
const d = new Date(t);
|
|
390
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
391
|
+
allMinutes.push(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const series = allMinutes.map((m) => {
|
|
396
|
+
const entry: { minute: string; [tool: string]: number | string } = { minute: m };
|
|
397
|
+
const toolMap = minuteMap.get(m);
|
|
398
|
+
for (const t of tools) {
|
|
399
|
+
const data = toolMap?.get(t);
|
|
400
|
+
entry[t] = data ? Math.round(data.total / data.count) : 0;
|
|
401
|
+
}
|
|
402
|
+
return entry;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const p95 = (arr: number[]) => {
|
|
406
|
+
if (arr.length === 0) return 0;
|
|
407
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
408
|
+
return sorted[Math.floor(sorted.length * 0.95)] ?? sorted[sorted.length - 1];
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const aggregated = tools.map((t) => {
|
|
412
|
+
const agg = aggMap.get(t)!;
|
|
413
|
+
return {
|
|
414
|
+
tool: t,
|
|
415
|
+
totalCalls: agg.durations.length,
|
|
416
|
+
avgMs: Math.round(agg.durations.reduce((s, v) => s + v, 0) / agg.durations.length),
|
|
417
|
+
p95Ms: p95(agg.durations),
|
|
418
|
+
errorCount: agg.errors,
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return { tools, series, aggregated };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Record a viewer API call for analytics (list, search, etc.). */
|
|
426
|
+
recordViewerEvent(eventType: string): void {
|
|
427
|
+
this.db.prepare("INSERT INTO viewer_events (event_type, created_at) VALUES (?, ?)").run(eventType, Date.now());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Return metrics for the last N days: writes per day (from chunks), viewer calls per day.
|
|
432
|
+
*/
|
|
433
|
+
getMetrics(days: number): {
|
|
434
|
+
writesPerDay: Array<{ date: string; count: number }>;
|
|
435
|
+
viewerCallsPerDay: Array<{ date: string; list: number; search: number; total: number }>;
|
|
436
|
+
roleBreakdown: Record<string, number>;
|
|
437
|
+
kindBreakdown: Record<string, number>;
|
|
438
|
+
totals: { memories: number; sessions: number; embeddings: number; todayWrites: number; todayViewerCalls: number };
|
|
439
|
+
} {
|
|
440
|
+
const since = Date.now() - days * 86400 * 1000;
|
|
441
|
+
const now = new Date();
|
|
442
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
443
|
+
|
|
444
|
+
const writesRows = this.db
|
|
445
|
+
.prepare(
|
|
446
|
+
`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, COUNT(*) as c
|
|
447
|
+
FROM chunks WHERE created_at >= ? GROUP BY d ORDER BY d`,
|
|
448
|
+
)
|
|
449
|
+
.all(since) as Array<{ d: string; c: number }>;
|
|
450
|
+
const writesPerDay = writesRows.map((r) => ({ date: r.d, count: r.c }));
|
|
451
|
+
|
|
452
|
+
const eventsRows = this.db
|
|
453
|
+
.prepare(
|
|
454
|
+
`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, event_type, COUNT(*) as c
|
|
455
|
+
FROM viewer_events WHERE created_at >= ? GROUP BY d, event_type ORDER BY d`,
|
|
456
|
+
)
|
|
457
|
+
.all(since) as Array<{ d: string; event_type: string; c: number }>;
|
|
458
|
+
const byDate = new Map<string, { list: number; search: number }>();
|
|
459
|
+
for (const r of eventsRows) {
|
|
460
|
+
let row = byDate.get(r.d);
|
|
461
|
+
if (!row) {
|
|
462
|
+
row = { list: 0, search: 0 };
|
|
463
|
+
byDate.set(r.d, row);
|
|
464
|
+
}
|
|
465
|
+
if (r.event_type === "list") row.list += r.c;
|
|
466
|
+
else if (r.event_type === "search") row.search += r.c;
|
|
467
|
+
}
|
|
468
|
+
const viewerCallsPerDay = Array.from(byDate.entries())
|
|
469
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
470
|
+
.map(([date, v]) => ({ date, list: v.list, search: v.search, total: v.list + v.search }));
|
|
471
|
+
|
|
472
|
+
const roles = this.db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as Array<{ role: string; count: number }>;
|
|
473
|
+
const kinds = this.db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as Array<{ kind: string; count: number }>;
|
|
474
|
+
const roleBreakdown = Object.fromEntries(roles.map((r) => [r.role, r.count]));
|
|
475
|
+
const kindBreakdown = Object.fromEntries(kinds.map((k) => [k.kind, k.count]));
|
|
476
|
+
|
|
477
|
+
const totalChunks = (this.db.prepare("SELECT COUNT(*) as c FROM chunks").get() as { c: number }).c;
|
|
478
|
+
const totalSessions = (this.db.prepare("SELECT COUNT(DISTINCT session_key) as c FROM chunks").get() as { c: number }).c;
|
|
479
|
+
const totalEmbeddings = (this.db.prepare("SELECT COUNT(*) as c FROM embeddings").get() as { c: number }).c;
|
|
480
|
+
const todayWrites = (this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE created_at >= ?").get(todayStart) as { c: number }).c;
|
|
481
|
+
const todayViewerCalls = (this.db.prepare("SELECT COUNT(*) as c FROM viewer_events WHERE created_at >= ?").get(todayStart) as { c: number }).c;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
writesPerDay,
|
|
485
|
+
viewerCallsPerDay,
|
|
486
|
+
roleBreakdown,
|
|
487
|
+
kindBreakdown,
|
|
488
|
+
totals: {
|
|
489
|
+
memories: totalChunks,
|
|
490
|
+
sessions: totalSessions,
|
|
491
|
+
embeddings: totalEmbeddings,
|
|
492
|
+
todayWrites,
|
|
493
|
+
todayViewerCalls,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
76
498
|
// ─── Write ───
|
|
77
499
|
|
|
78
500
|
insertChunk(chunk: Chunk): void {
|
|
79
501
|
const stmt = this.db.prepare(`
|
|
80
|
-
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, created_at, updated_at)
|
|
81
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
502
|
+
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, dedup_status, dedup_target, dedup_reason, created_at, updated_at)
|
|
503
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
504
|
`);
|
|
83
505
|
stmt.run(
|
|
84
506
|
chunk.id,
|
|
@@ -89,11 +511,22 @@ export class SqliteStore {
|
|
|
89
511
|
chunk.content,
|
|
90
512
|
chunk.kind,
|
|
91
513
|
chunk.summary,
|
|
514
|
+
chunk.taskId,
|
|
515
|
+
contentHash(chunk.content),
|
|
516
|
+
chunk.dedupStatus ?? "active",
|
|
517
|
+
chunk.dedupTarget ?? null,
|
|
518
|
+
chunk.dedupReason ?? null,
|
|
92
519
|
chunk.createdAt,
|
|
93
520
|
chunk.updatedAt,
|
|
94
521
|
);
|
|
95
522
|
}
|
|
96
523
|
|
|
524
|
+
markDedupStatus(chunkId: string, status: "duplicate" | "merged", targetChunkId: string | null, reason: string): void {
|
|
525
|
+
this.db.prepare(
|
|
526
|
+
"UPDATE chunks SET dedup_status = ?, dedup_target = ?, dedup_reason = ?, updated_at = ? WHERE id = ?",
|
|
527
|
+
).run(status, targetChunkId, reason, Date.now(), chunkId);
|
|
528
|
+
}
|
|
529
|
+
|
|
97
530
|
updateSummary(chunkId: string, summary: string): void {
|
|
98
531
|
this.db.prepare("UPDATE chunks SET summary = ?, updated_at = ? WHERE id = ?").run(
|
|
99
532
|
summary,
|
|
@@ -150,7 +583,7 @@ export class SqliteStore {
|
|
|
150
583
|
SELECT c.id as chunk_id, rank
|
|
151
584
|
FROM chunks_fts f
|
|
152
585
|
JOIN chunks c ON c.rowid = f.rowid
|
|
153
|
-
WHERE chunks_fts MATCH ?
|
|
586
|
+
WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'
|
|
154
587
|
ORDER BY rank
|
|
155
588
|
LIMIT ?
|
|
156
589
|
`).all(sanitized, limit) as Array<{ chunk_id: string; rank: number }>;
|
|
@@ -167,11 +600,46 @@ export class SqliteStore {
|
|
|
167
600
|
}
|
|
168
601
|
}
|
|
169
602
|
|
|
603
|
+
// ─── Pattern Search (LIKE-based, for CJK text where FTS tokenization is weak) ───
|
|
604
|
+
|
|
605
|
+
patternSearch(patterns: string[], opts: { role?: string; limit?: number } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
|
|
606
|
+
if (patterns.length === 0) return [];
|
|
607
|
+
const limit = opts.limit ?? 10;
|
|
608
|
+
|
|
609
|
+
const conditions = patterns.map(() => "c.content LIKE ?");
|
|
610
|
+
const whereClause = conditions.join(" OR ");
|
|
611
|
+
const roleClause = opts.role ? " AND c.role = ?" : "";
|
|
612
|
+
const params: (string | number)[] = patterns.map(p => `%${p}%`);
|
|
613
|
+
if (opts.role) params.push(opts.role);
|
|
614
|
+
params.push(limit);
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const rows = this.db.prepare(`
|
|
618
|
+
SELECT c.id as chunk_id, c.content, c.role, c.created_at
|
|
619
|
+
FROM chunks c
|
|
620
|
+
WHERE (${whereClause})${roleClause} AND c.dedup_status = 'active'
|
|
621
|
+
ORDER BY c.created_at DESC
|
|
622
|
+
LIMIT ?
|
|
623
|
+
`).all(...params) as Array<{ chunk_id: string; content: string; role: string; created_at: number }>;
|
|
624
|
+
|
|
625
|
+
return rows.map(r => ({
|
|
626
|
+
chunkId: r.chunk_id,
|
|
627
|
+
content: r.content,
|
|
628
|
+
role: r.role,
|
|
629
|
+
createdAt: r.created_at,
|
|
630
|
+
}));
|
|
631
|
+
} catch {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
170
636
|
// ─── Vector Search ───
|
|
171
637
|
|
|
172
638
|
getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
|
|
173
639
|
const rows = this.db.prepare(
|
|
174
|
-
|
|
640
|
+
`SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
|
|
641
|
+
JOIN chunks c ON c.id = e.chunk_id
|
|
642
|
+
WHERE c.dedup_status = 'active'`,
|
|
175
643
|
).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
176
644
|
|
|
177
645
|
return rows.map((r) => ({
|
|
@@ -235,8 +703,111 @@ export class SqliteStore {
|
|
|
235
703
|
}
|
|
236
704
|
|
|
237
705
|
deleteAll(): number {
|
|
238
|
-
|
|
239
|
-
|
|
706
|
+
this.db.exec("PRAGMA foreign_keys = OFF");
|
|
707
|
+
this.db.prepare("DELETE FROM task_skills").run();
|
|
708
|
+
this.db.prepare("DELETE FROM skill_versions").run();
|
|
709
|
+
this.db.prepare("DELETE FROM skills").run();
|
|
710
|
+
this.db.prepare("DELETE FROM chunks").run();
|
|
711
|
+
this.db.prepare("DELETE FROM tasks").run();
|
|
712
|
+
this.db.prepare("DELETE FROM viewer_events").run();
|
|
713
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
714
|
+
const remaining = this.countChunks();
|
|
715
|
+
return remaining === 0 ? 1 : 0;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─── Task CRUD ───
|
|
719
|
+
|
|
720
|
+
insertTask(task: Task): void {
|
|
721
|
+
this.db.prepare(`
|
|
722
|
+
INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, started_at, ended_at, updated_at)
|
|
723
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
724
|
+
`).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.startedAt, task.endedAt, task.updatedAt);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
getTask(taskId: string): Task | null {
|
|
728
|
+
const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId) as TaskRow | undefined;
|
|
729
|
+
return row ? rowToTask(row) : null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
getActiveTask(sessionKey: string): Task | null {
|
|
733
|
+
const row = this.db.prepare(
|
|
734
|
+
"SELECT * FROM tasks WHERE session_key = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1",
|
|
735
|
+
).get(sessionKey) as TaskRow | undefined;
|
|
736
|
+
return row ? rowToTask(row) : null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
getAllActiveTasks(): Task[] {
|
|
740
|
+
const rows = this.db.prepare(
|
|
741
|
+
"SELECT * FROM tasks WHERE status = 'active' ORDER BY started_at DESC",
|
|
742
|
+
).all() as TaskRow[];
|
|
743
|
+
return rows.map(rowToTask);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
updateTask(taskId: string, fields: { title?: string; summary?: string; status?: TaskStatus; endedAt?: number }): boolean {
|
|
747
|
+
const sets: string[] = [];
|
|
748
|
+
const params: unknown[] = [];
|
|
749
|
+
if (fields.title !== undefined) { sets.push("title = ?"); params.push(fields.title); }
|
|
750
|
+
if (fields.summary !== undefined) { sets.push("summary = ?"); params.push(fields.summary); }
|
|
751
|
+
if (fields.status !== undefined) { sets.push("status = ?"); params.push(fields.status); }
|
|
752
|
+
if (fields.endedAt !== undefined) { sets.push("ended_at = ?"); params.push(fields.endedAt); }
|
|
753
|
+
if (sets.length === 0) return false;
|
|
754
|
+
sets.push("updated_at = ?");
|
|
755
|
+
params.push(Date.now());
|
|
756
|
+
params.push(taskId);
|
|
757
|
+
const result = this.db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
758
|
+
return result.changes > 0;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
getChunksByTask(taskId: string): Chunk[] {
|
|
762
|
+
const rows = this.db.prepare("SELECT * FROM chunks WHERE task_id = ? ORDER BY created_at, seq").all(taskId) as ChunkRow[];
|
|
763
|
+
return rows.map(rowToChunk);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
listTasks(opts: { status?: string; limit?: number; offset?: number } = {}): { tasks: Task[]; total: number } {
|
|
767
|
+
const conditions: string[] = [];
|
|
768
|
+
const params: unknown[] = [];
|
|
769
|
+
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
770
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
771
|
+
|
|
772
|
+
const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
|
|
773
|
+
const total = countRow.c;
|
|
774
|
+
|
|
775
|
+
const limit = opts.limit ?? 50;
|
|
776
|
+
const offset = opts.offset ?? 0;
|
|
777
|
+
const rows = this.db.prepare(
|
|
778
|
+
`SELECT * FROM tasks ${whereClause} ORDER BY started_at DESC LIMIT ? OFFSET ?`,
|
|
779
|
+
).all(...params, limit, offset) as TaskRow[];
|
|
780
|
+
|
|
781
|
+
return { tasks: rows.map(rowToTask), total };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
countChunksByTask(taskId: string): number {
|
|
785
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE task_id = ?").get(taskId) as { c: number };
|
|
786
|
+
return row.c;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
setChunkTaskId(chunkId: string, taskId: string): void {
|
|
790
|
+
this.db.prepare("UPDATE chunks SET task_id = ?, updated_at = ? WHERE id = ?").run(taskId, Date.now(), chunkId);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
getUnassignedChunks(sessionKey: string): Chunk[] {
|
|
794
|
+
const rows = this.db.prepare(
|
|
795
|
+
"SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL ORDER BY created_at, seq",
|
|
796
|
+
).all(sessionKey) as ChunkRow[];
|
|
797
|
+
return rows.map(rowToChunk);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Check if a chunk with the same (session_key, role, content_hash) already exists.
|
|
802
|
+
* Uses indexed content_hash for O(1) lookup to prevent duplicate ingestion
|
|
803
|
+
* when agent_end sends the full conversation history every turn.
|
|
804
|
+
*/
|
|
805
|
+
chunkExistsByContent(sessionKey: string, role: string, content: string): boolean {
|
|
806
|
+
const hash = contentHash(content);
|
|
807
|
+
const row = this.db.prepare(
|
|
808
|
+
"SELECT 1 FROM chunks WHERE session_key = ? AND role = ? AND content_hash = ? LIMIT 1",
|
|
809
|
+
).get(sessionKey, role, hash);
|
|
810
|
+
return !!row;
|
|
240
811
|
}
|
|
241
812
|
|
|
242
813
|
// ─── Util ───
|
|
@@ -248,6 +819,124 @@ export class SqliteStore {
|
|
|
248
819
|
return rows.map((r) => r.id);
|
|
249
820
|
}
|
|
250
821
|
|
|
822
|
+
countChunks(): number {
|
|
823
|
+
const row = this.db.prepare("SELECT COUNT(*) AS cnt FROM chunks").get() as { cnt: number };
|
|
824
|
+
return row.cnt;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ─── Skill CRUD ───
|
|
828
|
+
|
|
829
|
+
insertSkill(skill: Skill): void {
|
|
830
|
+
this.db.prepare(`
|
|
831
|
+
INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, quality_score, created_at, updated_at)
|
|
832
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
833
|
+
`).run(skill.id, skill.name, skill.description, skill.version, skill.status, skill.tags, skill.sourceType, skill.dirPath, skill.installed, skill.qualityScore, skill.createdAt, skill.updatedAt);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
getSkill(skillId: string): Skill | null {
|
|
837
|
+
const row = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(skillId) as SkillRow | undefined;
|
|
838
|
+
return row ? rowToSkill(row) : null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
getSkillByName(name: string): Skill | null {
|
|
842
|
+
const row = this.db.prepare("SELECT * FROM skills WHERE name = ?").get(name) as SkillRow | undefined;
|
|
843
|
+
return row ? rowToSkill(row) : null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
updateSkill(skillId: string, fields: { description?: string; version?: number; status?: SkillStatus; installed?: number; qualityScore?: number | null; updatedAt?: number }): void {
|
|
847
|
+
const sets: string[] = [];
|
|
848
|
+
const params: unknown[] = [];
|
|
849
|
+
if (fields.description !== undefined) { sets.push("description = ?"); params.push(fields.description); }
|
|
850
|
+
if (fields.version !== undefined) { sets.push("version = ?"); params.push(fields.version); }
|
|
851
|
+
if (fields.status !== undefined) { sets.push("status = ?"); params.push(fields.status); }
|
|
852
|
+
if (fields.installed !== undefined) { sets.push("installed = ?"); params.push(fields.installed); }
|
|
853
|
+
if (fields.qualityScore !== undefined) { sets.push("quality_score = ?"); params.push(fields.qualityScore); }
|
|
854
|
+
if (sets.length === 0) return;
|
|
855
|
+
sets.push("updated_at = ?");
|
|
856
|
+
params.push(fields.updatedAt ?? Date.now());
|
|
857
|
+
params.push(skillId);
|
|
858
|
+
this.db.prepare(`UPDATE skills SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
listSkills(opts: { status?: string } = {}): Skill[] {
|
|
862
|
+
const cond = opts.status ? "WHERE status = ?" : "";
|
|
863
|
+
const params = opts.status ? [opts.status] : [];
|
|
864
|
+
const rows = this.db.prepare(`SELECT * FROM skills ${cond} ORDER BY updated_at DESC`).all(...params) as SkillRow[];
|
|
865
|
+
return rows.map(rowToSkill);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ─── Skill Versions ───
|
|
869
|
+
|
|
870
|
+
insertSkillVersion(sv: SkillVersion): void {
|
|
871
|
+
this.db.prepare(`
|
|
872
|
+
INSERT OR REPLACE INTO skill_versions (id, skill_id, version, content, changelog, change_summary, upgrade_type, source_task_id, metrics, quality_score, created_at)
|
|
873
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
874
|
+
`).run(sv.id, sv.skillId, sv.version, sv.content, sv.changelog, sv.changeSummary, sv.upgradeType, sv.sourceTaskId, sv.metrics, sv.qualityScore, sv.createdAt);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
getLatestSkillVersion(skillId: string): SkillVersion | null {
|
|
878
|
+
const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC LIMIT 1").get(skillId) as SkillVersionRow | undefined;
|
|
879
|
+
return row ? rowToSkillVersion(row) : null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
getSkillVersions(skillId: string): SkillVersion[] {
|
|
883
|
+
const rows = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC").all(skillId) as SkillVersionRow[];
|
|
884
|
+
return rows.map(rowToSkillVersion);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
getSkillVersion(skillId: string, version: number): SkillVersion | null {
|
|
888
|
+
const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? AND version = ?").get(skillId, version) as SkillVersionRow | undefined;
|
|
889
|
+
return row ? rowToSkillVersion(row) : null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ─── Task-Skill Links ───
|
|
893
|
+
|
|
894
|
+
linkTaskSkill(taskId: string, skillId: string, relation: TaskSkillRelation, versionAt: number): void {
|
|
895
|
+
this.db.prepare(`
|
|
896
|
+
INSERT OR REPLACE INTO task_skills (task_id, skill_id, relation, version_at, created_at)
|
|
897
|
+
VALUES (?, ?, ?, ?, ?)
|
|
898
|
+
`).run(taskId, skillId, relation, versionAt, Date.now());
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
getSkillsByTask(taskId: string): Array<{ skill: Skill; relation: TaskSkillRelation; versionAt: number }> {
|
|
902
|
+
const rows = this.db.prepare(`
|
|
903
|
+
SELECT s.*, ts.relation, ts.version_at
|
|
904
|
+
FROM task_skills ts JOIN skills s ON s.id = ts.skill_id
|
|
905
|
+
WHERE ts.task_id = ?
|
|
906
|
+
`).all(taskId) as Array<SkillRow & { relation: string; version_at: number }>;
|
|
907
|
+
return rows.map(r => ({
|
|
908
|
+
skill: rowToSkill(r),
|
|
909
|
+
relation: r.relation as TaskSkillRelation,
|
|
910
|
+
versionAt: r.version_at,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
getTasksBySkill(skillId: string): Array<{ task: Task; relation: TaskSkillRelation }> {
|
|
915
|
+
const rows = this.db.prepare(`
|
|
916
|
+
SELECT t.*, ts.relation
|
|
917
|
+
FROM task_skills ts JOIN tasks t ON t.id = ts.task_id
|
|
918
|
+
WHERE ts.skill_id = ?
|
|
919
|
+
ORDER BY t.started_at DESC
|
|
920
|
+
`).all(skillId) as Array<TaskRow & { relation: string }>;
|
|
921
|
+
return rows.map(r => ({
|
|
922
|
+
task: rowToTask(r),
|
|
923
|
+
relation: r.relation as TaskSkillRelation,
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
countSkills(status?: string): number {
|
|
928
|
+
const cond = status ? "WHERE status = ?" : "";
|
|
929
|
+
const params = status ? [status] : [];
|
|
930
|
+
const row = this.db.prepare(`SELECT COUNT(*) as c FROM skills ${cond}`).get(...params) as { c: number };
|
|
931
|
+
return row.c;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ─── Chunk-Skill ───
|
|
935
|
+
|
|
936
|
+
setChunkSkillId(chunkId: string, skillId: string): void {
|
|
937
|
+
this.db.prepare("UPDATE chunks SET skill_id = ?, updated_at = ? WHERE id = ?").run(skillId, Date.now(), chunkId);
|
|
938
|
+
}
|
|
939
|
+
|
|
251
940
|
close(): void {
|
|
252
941
|
this.db.close();
|
|
253
942
|
}
|
|
@@ -284,6 +973,14 @@ interface ChunkRow {
|
|
|
284
973
|
content: string;
|
|
285
974
|
kind: string;
|
|
286
975
|
summary: string;
|
|
976
|
+
task_id: string | null;
|
|
977
|
+
skill_id: string | null;
|
|
978
|
+
dedup_status: string;
|
|
979
|
+
dedup_target: string | null;
|
|
980
|
+
dedup_reason: string | null;
|
|
981
|
+
merge_count: number;
|
|
982
|
+
last_hit_at: number | null;
|
|
983
|
+
merge_history: string;
|
|
287
984
|
created_at: number;
|
|
288
985
|
updated_at: number;
|
|
289
986
|
}
|
|
@@ -299,7 +996,105 @@ function rowToChunk(row: ChunkRow): Chunk {
|
|
|
299
996
|
kind: row.kind as Chunk["kind"],
|
|
300
997
|
summary: row.summary,
|
|
301
998
|
embedding: null,
|
|
999
|
+
taskId: row.task_id,
|
|
1000
|
+
skillId: row.skill_id ?? null,
|
|
1001
|
+
dedupStatus: (row.dedup_status ?? "active") as DedupStatus,
|
|
1002
|
+
dedupTarget: row.dedup_target ?? null,
|
|
1003
|
+
dedupReason: row.dedup_reason ?? null,
|
|
1004
|
+
mergeCount: row.merge_count ?? 0,
|
|
1005
|
+
lastHitAt: row.last_hit_at ?? null,
|
|
1006
|
+
mergeHistory: row.merge_history ?? "[]",
|
|
1007
|
+
createdAt: row.created_at,
|
|
1008
|
+
updatedAt: row.updated_at,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
interface TaskRow {
|
|
1013
|
+
id: string;
|
|
1014
|
+
session_key: string;
|
|
1015
|
+
title: string;
|
|
1016
|
+
summary: string;
|
|
1017
|
+
status: string;
|
|
1018
|
+
started_at: number;
|
|
1019
|
+
ended_at: number | null;
|
|
1020
|
+
updated_at: number;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function rowToTask(row: TaskRow): Task {
|
|
1024
|
+
return {
|
|
1025
|
+
id: row.id,
|
|
1026
|
+
sessionKey: row.session_key,
|
|
1027
|
+
title: row.title,
|
|
1028
|
+
summary: row.summary,
|
|
1029
|
+
status: row.status as Task["status"],
|
|
1030
|
+
startedAt: row.started_at,
|
|
1031
|
+
endedAt: row.ended_at,
|
|
1032
|
+
updatedAt: row.updated_at,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
interface SkillRow {
|
|
1037
|
+
id: string;
|
|
1038
|
+
name: string;
|
|
1039
|
+
description: string;
|
|
1040
|
+
version: number;
|
|
1041
|
+
status: string;
|
|
1042
|
+
tags: string;
|
|
1043
|
+
source_type: string;
|
|
1044
|
+
dir_path: string;
|
|
1045
|
+
installed: number;
|
|
1046
|
+
quality_score: number | null;
|
|
1047
|
+
created_at: number;
|
|
1048
|
+
updated_at: number;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function rowToSkill(row: SkillRow): Skill {
|
|
1052
|
+
return {
|
|
1053
|
+
id: row.id,
|
|
1054
|
+
name: row.name,
|
|
1055
|
+
description: row.description,
|
|
1056
|
+
version: row.version,
|
|
1057
|
+
status: row.status as Skill["status"],
|
|
1058
|
+
tags: row.tags,
|
|
1059
|
+
sourceType: row.source_type as Skill["sourceType"],
|
|
1060
|
+
dirPath: row.dir_path,
|
|
1061
|
+
installed: row.installed,
|
|
1062
|
+
qualityScore: row.quality_score ?? null,
|
|
302
1063
|
createdAt: row.created_at,
|
|
303
1064
|
updatedAt: row.updated_at,
|
|
304
1065
|
};
|
|
305
1066
|
}
|
|
1067
|
+
|
|
1068
|
+
interface SkillVersionRow {
|
|
1069
|
+
id: string;
|
|
1070
|
+
skill_id: string;
|
|
1071
|
+
version: number;
|
|
1072
|
+
content: string;
|
|
1073
|
+
changelog: string;
|
|
1074
|
+
change_summary: string;
|
|
1075
|
+
upgrade_type: string;
|
|
1076
|
+
source_task_id: string | null;
|
|
1077
|
+
metrics: string;
|
|
1078
|
+
quality_score: number | null;
|
|
1079
|
+
created_at: number;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function rowToSkillVersion(row: SkillVersionRow): SkillVersion {
|
|
1083
|
+
return {
|
|
1084
|
+
id: row.id,
|
|
1085
|
+
skillId: row.skill_id,
|
|
1086
|
+
version: row.version,
|
|
1087
|
+
content: row.content,
|
|
1088
|
+
changelog: row.changelog,
|
|
1089
|
+
changeSummary: row.change_summary ?? "",
|
|
1090
|
+
upgradeType: row.upgrade_type as SkillVersion["upgradeType"],
|
|
1091
|
+
sourceTaskId: row.source_task_id,
|
|
1092
|
+
metrics: row.metrics,
|
|
1093
|
+
qualityScore: row.quality_score ?? null,
|
|
1094
|
+
createdAt: row.created_at,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function contentHash(content: string): string {
|
|
1099
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
1100
|
+
}
|