@memtensor/memos-local-openclaw-plugin 0.1.2 → 0.1.4
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 +180 -68
- 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/providers/anthropic.d.ts +2 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +110 -1
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +2 -5
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +110 -6
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +2 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +106 -1
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +9 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +66 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +2 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +112 -1
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +63 -0
- package/dist/ingest/task-processor.d.ts.map +1 -0
- package/dist/ingest/task-processor.js +339 -0
- package/dist/ingest/task-processor.js.map +1 -0
- package/dist/ingest/worker.d.ts +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +18 -13
- 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 +21 -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/storage/sqlite.d.ts +67 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +251 -5
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -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 +955 -115
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +3 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +59 -1
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +221 -45
- package/openclaw.plugin.json +20 -45
- package/package.json +3 -4
- package/skill/SKILL.md +59 -0
- package/src/capture/index.ts +85 -45
- package/src/ingest/providers/anthropic.ts +128 -1
- package/src/ingest/providers/bedrock.ts +130 -6
- package/src/ingest/providers/gemini.ts +128 -1
- package/src/ingest/providers/index.ts +74 -8
- package/src/ingest/providers/openai.ts +130 -1
- package/src/ingest/task-processor.ts +380 -0
- package/src/ingest/worker.ts +21 -15
- package/src/recall/engine.ts +22 -12
- package/src/recall/mmr.ts +3 -1
- package/src/storage/sqlite.ts +298 -5
- package/src/types.ts +19 -0
- package/src/viewer/html.ts +955 -115
- package/src/viewer/server.ts +63 -1
- package/SKILL.md +0 -43
- package/www/index.html +0 -606
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, Task, TaskStatus, Logger } from "../types";
|
|
5
6
|
|
|
6
7
|
export class SqliteStore {
|
|
7
8
|
private db: Database.Database;
|
|
@@ -69,16 +70,140 @@ 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();
|
|
73
98
|
this.log.debug("Database schema initialized");
|
|
74
99
|
}
|
|
75
100
|
|
|
101
|
+
private migrateTaskId(): void {
|
|
102
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
103
|
+
if (!cols.some((c) => c.name === "task_id")) {
|
|
104
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN task_id TEXT REFERENCES tasks(id)");
|
|
105
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_task ON chunks(task_id)");
|
|
106
|
+
this.log.info("Migrated: added task_id column to chunks");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private migrateContentHash(): void {
|
|
111
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
112
|
+
if (!cols.some((c) => c.name === "content_hash")) {
|
|
113
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN content_hash TEXT");
|
|
114
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup ON chunks(session_key, role, content_hash)");
|
|
115
|
+
|
|
116
|
+
// Backfill existing rows
|
|
117
|
+
const rows = this.db.prepare("SELECT id, content FROM chunks WHERE content_hash IS NULL").all() as Array<{ id: string; content: string }>;
|
|
118
|
+
const updateStmt = this.db.prepare("UPDATE chunks SET content_hash = ? WHERE id = ?");
|
|
119
|
+
for (const r of rows) {
|
|
120
|
+
updateStmt.run(contentHash(r.content), r.id);
|
|
121
|
+
}
|
|
122
|
+
if (rows.length > 0) {
|
|
123
|
+
this.log.info(`Migrated: backfilled content_hash for ${rows.length} chunks`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Record a viewer API call for analytics (list, search, etc.). */
|
|
129
|
+
recordViewerEvent(eventType: string): void {
|
|
130
|
+
this.db.prepare("INSERT INTO viewer_events (event_type, created_at) VALUES (?, ?)").run(eventType, Date.now());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Return metrics for the last N days: writes per day (from chunks), viewer calls per day.
|
|
135
|
+
*/
|
|
136
|
+
getMetrics(days: number): {
|
|
137
|
+
writesPerDay: Array<{ date: string; count: number }>;
|
|
138
|
+
viewerCallsPerDay: Array<{ date: string; list: number; search: number; total: number }>;
|
|
139
|
+
roleBreakdown: Record<string, number>;
|
|
140
|
+
kindBreakdown: Record<string, number>;
|
|
141
|
+
totals: { memories: number; sessions: number; embeddings: number; todayWrites: number; todayViewerCalls: number };
|
|
142
|
+
} {
|
|
143
|
+
const since = Date.now() - days * 86400 * 1000;
|
|
144
|
+
const now = new Date();
|
|
145
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
146
|
+
|
|
147
|
+
const writesRows = this.db
|
|
148
|
+
.prepare(
|
|
149
|
+
`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, COUNT(*) as c
|
|
150
|
+
FROM chunks WHERE created_at >= ? GROUP BY d ORDER BY d`,
|
|
151
|
+
)
|
|
152
|
+
.all(since) as Array<{ d: string; c: number }>;
|
|
153
|
+
const writesPerDay = writesRows.map((r) => ({ date: r.d, count: r.c }));
|
|
154
|
+
|
|
155
|
+
const eventsRows = this.db
|
|
156
|
+
.prepare(
|
|
157
|
+
`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, event_type, COUNT(*) as c
|
|
158
|
+
FROM viewer_events WHERE created_at >= ? GROUP BY d, event_type ORDER BY d`,
|
|
159
|
+
)
|
|
160
|
+
.all(since) as Array<{ d: string; event_type: string; c: number }>;
|
|
161
|
+
const byDate = new Map<string, { list: number; search: number }>();
|
|
162
|
+
for (const r of eventsRows) {
|
|
163
|
+
let row = byDate.get(r.d);
|
|
164
|
+
if (!row) {
|
|
165
|
+
row = { list: 0, search: 0 };
|
|
166
|
+
byDate.set(r.d, row);
|
|
167
|
+
}
|
|
168
|
+
if (r.event_type === "list") row.list += r.c;
|
|
169
|
+
else if (r.event_type === "search") row.search += r.c;
|
|
170
|
+
}
|
|
171
|
+
const viewerCallsPerDay = Array.from(byDate.entries())
|
|
172
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
173
|
+
.map(([date, v]) => ({ date, list: v.list, search: v.search, total: v.list + v.search }));
|
|
174
|
+
|
|
175
|
+
const roles = this.db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as Array<{ role: string; count: number }>;
|
|
176
|
+
const kinds = this.db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as Array<{ kind: string; count: number }>;
|
|
177
|
+
const roleBreakdown = Object.fromEntries(roles.map((r) => [r.role, r.count]));
|
|
178
|
+
const kindBreakdown = Object.fromEntries(kinds.map((k) => [k.kind, k.count]));
|
|
179
|
+
|
|
180
|
+
const totalChunks = (this.db.prepare("SELECT COUNT(*) as c FROM chunks").get() as { c: number }).c;
|
|
181
|
+
const totalSessions = (this.db.prepare("SELECT COUNT(DISTINCT session_key) as c FROM chunks").get() as { c: number }).c;
|
|
182
|
+
const totalEmbeddings = (this.db.prepare("SELECT COUNT(*) as c FROM embeddings").get() as { c: number }).c;
|
|
183
|
+
const todayWrites = (this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE created_at >= ?").get(todayStart) as { c: number }).c;
|
|
184
|
+
const todayViewerCalls = (this.db.prepare("SELECT COUNT(*) as c FROM viewer_events WHERE created_at >= ?").get(todayStart) as { c: number }).c;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
writesPerDay,
|
|
188
|
+
viewerCallsPerDay,
|
|
189
|
+
roleBreakdown,
|
|
190
|
+
kindBreakdown,
|
|
191
|
+
totals: {
|
|
192
|
+
memories: totalChunks,
|
|
193
|
+
sessions: totalSessions,
|
|
194
|
+
embeddings: totalEmbeddings,
|
|
195
|
+
todayWrites,
|
|
196
|
+
todayViewerCalls,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
76
201
|
// ─── Write ───
|
|
77
202
|
|
|
78
203
|
insertChunk(chunk: Chunk): void {
|
|
79
204
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
205
|
+
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, created_at, updated_at)
|
|
206
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
207
|
`);
|
|
83
208
|
stmt.run(
|
|
84
209
|
chunk.id,
|
|
@@ -89,6 +214,8 @@ export class SqliteStore {
|
|
|
89
214
|
chunk.content,
|
|
90
215
|
chunk.kind,
|
|
91
216
|
chunk.summary,
|
|
217
|
+
chunk.taskId,
|
|
218
|
+
contentHash(chunk.content),
|
|
92
219
|
chunk.createdAt,
|
|
93
220
|
chunk.updatedAt,
|
|
94
221
|
);
|
|
@@ -167,6 +294,39 @@ export class SqliteStore {
|
|
|
167
294
|
}
|
|
168
295
|
}
|
|
169
296
|
|
|
297
|
+
// ─── Pattern Search (LIKE-based, for CJK text where FTS tokenization is weak) ───
|
|
298
|
+
|
|
299
|
+
patternSearch(patterns: string[], opts: { role?: string; limit?: number } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
|
|
300
|
+
if (patterns.length === 0) return [];
|
|
301
|
+
const limit = opts.limit ?? 10;
|
|
302
|
+
|
|
303
|
+
const conditions = patterns.map(() => "c.content LIKE ?");
|
|
304
|
+
const whereClause = conditions.join(" OR ");
|
|
305
|
+
const roleClause = opts.role ? " AND c.role = ?" : "";
|
|
306
|
+
const params: (string | number)[] = patterns.map(p => `%${p}%`);
|
|
307
|
+
if (opts.role) params.push(opts.role);
|
|
308
|
+
params.push(limit);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const rows = this.db.prepare(`
|
|
312
|
+
SELECT c.id as chunk_id, c.content, c.role, c.created_at
|
|
313
|
+
FROM chunks c
|
|
314
|
+
WHERE (${whereClause})${roleClause}
|
|
315
|
+
ORDER BY c.created_at DESC
|
|
316
|
+
LIMIT ?
|
|
317
|
+
`).all(...params) as Array<{ chunk_id: string; content: string; role: string; created_at: number }>;
|
|
318
|
+
|
|
319
|
+
return rows.map(r => ({
|
|
320
|
+
chunkId: r.chunk_id,
|
|
321
|
+
content: r.content,
|
|
322
|
+
role: r.role,
|
|
323
|
+
createdAt: r.created_at,
|
|
324
|
+
}));
|
|
325
|
+
} catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
170
330
|
// ─── Vector Search ───
|
|
171
331
|
|
|
172
332
|
getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
|
|
@@ -235,8 +395,106 @@ export class SqliteStore {
|
|
|
235
395
|
}
|
|
236
396
|
|
|
237
397
|
deleteAll(): number {
|
|
238
|
-
|
|
239
|
-
|
|
398
|
+
this.db.prepare("DELETE FROM chunks").run();
|
|
399
|
+
this.db.prepare("DELETE FROM tasks").run();
|
|
400
|
+
this.db.prepare("DELETE FROM viewer_events").run();
|
|
401
|
+
const remaining = this.countChunks();
|
|
402
|
+
return remaining === 0 ? 1 : 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ─── Task CRUD ───
|
|
406
|
+
|
|
407
|
+
insertTask(task: Task): void {
|
|
408
|
+
this.db.prepare(`
|
|
409
|
+
INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, started_at, ended_at, updated_at)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
411
|
+
`).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.startedAt, task.endedAt, task.updatedAt);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getTask(taskId: string): Task | null {
|
|
415
|
+
const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId) as TaskRow | undefined;
|
|
416
|
+
return row ? rowToTask(row) : null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
getActiveTask(sessionKey: string): Task | null {
|
|
420
|
+
const row = this.db.prepare(
|
|
421
|
+
"SELECT * FROM tasks WHERE session_key = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1",
|
|
422
|
+
).get(sessionKey) as TaskRow | undefined;
|
|
423
|
+
return row ? rowToTask(row) : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
getAllActiveTasks(): Task[] {
|
|
427
|
+
const rows = this.db.prepare(
|
|
428
|
+
"SELECT * FROM tasks WHERE status = 'active' ORDER BY started_at DESC",
|
|
429
|
+
).all() as TaskRow[];
|
|
430
|
+
return rows.map(rowToTask);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
updateTask(taskId: string, fields: { title?: string; summary?: string; status?: TaskStatus; endedAt?: number }): boolean {
|
|
434
|
+
const sets: string[] = [];
|
|
435
|
+
const params: unknown[] = [];
|
|
436
|
+
if (fields.title !== undefined) { sets.push("title = ?"); params.push(fields.title); }
|
|
437
|
+
if (fields.summary !== undefined) { sets.push("summary = ?"); params.push(fields.summary); }
|
|
438
|
+
if (fields.status !== undefined) { sets.push("status = ?"); params.push(fields.status); }
|
|
439
|
+
if (fields.endedAt !== undefined) { sets.push("ended_at = ?"); params.push(fields.endedAt); }
|
|
440
|
+
if (sets.length === 0) return false;
|
|
441
|
+
sets.push("updated_at = ?");
|
|
442
|
+
params.push(Date.now());
|
|
443
|
+
params.push(taskId);
|
|
444
|
+
const result = this.db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
445
|
+
return result.changes > 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
getChunksByTask(taskId: string): Chunk[] {
|
|
449
|
+
const rows = this.db.prepare("SELECT * FROM chunks WHERE task_id = ? ORDER BY created_at, seq").all(taskId) as ChunkRow[];
|
|
450
|
+
return rows.map(rowToChunk);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
listTasks(opts: { status?: string; limit?: number; offset?: number } = {}): { tasks: Task[]; total: number } {
|
|
454
|
+
const conditions: string[] = [];
|
|
455
|
+
const params: unknown[] = [];
|
|
456
|
+
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
457
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
458
|
+
|
|
459
|
+
const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
|
|
460
|
+
const total = countRow.c;
|
|
461
|
+
|
|
462
|
+
const limit = opts.limit ?? 50;
|
|
463
|
+
const offset = opts.offset ?? 0;
|
|
464
|
+
const rows = this.db.prepare(
|
|
465
|
+
`SELECT * FROM tasks ${whereClause} ORDER BY started_at DESC LIMIT ? OFFSET ?`,
|
|
466
|
+
).all(...params, limit, offset) as TaskRow[];
|
|
467
|
+
|
|
468
|
+
return { tasks: rows.map(rowToTask), total };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
countChunksByTask(taskId: string): number {
|
|
472
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE task_id = ?").get(taskId) as { c: number };
|
|
473
|
+
return row.c;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
setChunkTaskId(chunkId: string, taskId: string): void {
|
|
477
|
+
this.db.prepare("UPDATE chunks SET task_id = ?, updated_at = ? WHERE id = ?").run(taskId, Date.now(), chunkId);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
getUnassignedChunks(sessionKey: string): Chunk[] {
|
|
481
|
+
const rows = this.db.prepare(
|
|
482
|
+
"SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL ORDER BY created_at, seq",
|
|
483
|
+
).all(sessionKey) as ChunkRow[];
|
|
484
|
+
return rows.map(rowToChunk);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Check if a chunk with the same (session_key, role, content_hash) already exists.
|
|
489
|
+
* Uses indexed content_hash for O(1) lookup to prevent duplicate ingestion
|
|
490
|
+
* when agent_end sends the full conversation history every turn.
|
|
491
|
+
*/
|
|
492
|
+
chunkExistsByContent(sessionKey: string, role: string, content: string): boolean {
|
|
493
|
+
const hash = contentHash(content);
|
|
494
|
+
const row = this.db.prepare(
|
|
495
|
+
"SELECT 1 FROM chunks WHERE session_key = ? AND role = ? AND content_hash = ? LIMIT 1",
|
|
496
|
+
).get(sessionKey, role, hash);
|
|
497
|
+
return !!row;
|
|
240
498
|
}
|
|
241
499
|
|
|
242
500
|
// ─── Util ───
|
|
@@ -248,6 +506,11 @@ export class SqliteStore {
|
|
|
248
506
|
return rows.map((r) => r.id);
|
|
249
507
|
}
|
|
250
508
|
|
|
509
|
+
countChunks(): number {
|
|
510
|
+
const row = this.db.prepare("SELECT COUNT(*) AS cnt FROM chunks").get() as { cnt: number };
|
|
511
|
+
return row.cnt;
|
|
512
|
+
}
|
|
513
|
+
|
|
251
514
|
close(): void {
|
|
252
515
|
this.db.close();
|
|
253
516
|
}
|
|
@@ -284,6 +547,7 @@ interface ChunkRow {
|
|
|
284
547
|
content: string;
|
|
285
548
|
kind: string;
|
|
286
549
|
summary: string;
|
|
550
|
+
task_id: string | null;
|
|
287
551
|
created_at: number;
|
|
288
552
|
updated_at: number;
|
|
289
553
|
}
|
|
@@ -299,7 +563,36 @@ function rowToChunk(row: ChunkRow): Chunk {
|
|
|
299
563
|
kind: row.kind as Chunk["kind"],
|
|
300
564
|
summary: row.summary,
|
|
301
565
|
embedding: null,
|
|
566
|
+
taskId: row.task_id,
|
|
302
567
|
createdAt: row.created_at,
|
|
303
568
|
updatedAt: row.updated_at,
|
|
304
569
|
};
|
|
305
570
|
}
|
|
571
|
+
|
|
572
|
+
interface TaskRow {
|
|
573
|
+
id: string;
|
|
574
|
+
session_key: string;
|
|
575
|
+
title: string;
|
|
576
|
+
summary: string;
|
|
577
|
+
status: string;
|
|
578
|
+
started_at: number;
|
|
579
|
+
ended_at: number | null;
|
|
580
|
+
updated_at: number;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function rowToTask(row: TaskRow): Task {
|
|
584
|
+
return {
|
|
585
|
+
id: row.id,
|
|
586
|
+
sessionKey: row.session_key,
|
|
587
|
+
title: row.title,
|
|
588
|
+
summary: row.summary,
|
|
589
|
+
status: row.status as Task["status"],
|
|
590
|
+
startedAt: row.started_at,
|
|
591
|
+
endedAt: row.ended_at,
|
|
592
|
+
updatedAt: row.updated_at,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function contentHash(content: string): string {
|
|
597
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
598
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -23,10 +23,26 @@ export interface Chunk {
|
|
|
23
23
|
kind: ChunkKind;
|
|
24
24
|
summary: string;
|
|
25
25
|
embedding: number[] | null;
|
|
26
|
+
taskId: string | null;
|
|
26
27
|
createdAt: number;
|
|
27
28
|
updatedAt: number;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// ─── Task ───
|
|
32
|
+
|
|
33
|
+
export type TaskStatus = "active" | "completed" | "skipped";
|
|
34
|
+
|
|
35
|
+
export interface Task {
|
|
36
|
+
id: string;
|
|
37
|
+
sessionKey: string;
|
|
38
|
+
title: string;
|
|
39
|
+
summary: string;
|
|
40
|
+
status: TaskStatus;
|
|
41
|
+
startedAt: number;
|
|
42
|
+
endedAt: number | null;
|
|
43
|
+
updatedAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
export type ChunkKind =
|
|
31
47
|
| "paragraph"
|
|
32
48
|
| "code_block"
|
|
@@ -50,6 +66,7 @@ export interface SearchHit {
|
|
|
50
66
|
original_excerpt: string;
|
|
51
67
|
ref: ChunkRef;
|
|
52
68
|
score: number;
|
|
69
|
+
taskId: string | null;
|
|
53
70
|
source: {
|
|
54
71
|
ts: number;
|
|
55
72
|
role: Role;
|
|
@@ -188,6 +205,8 @@ export const DEFAULTS = {
|
|
|
188
205
|
localEmbeddingModel: "Xenova/all-MiniLM-L6-v2",
|
|
189
206
|
localEmbeddingDimensions: 384,
|
|
190
207
|
toolResultMaxChars: 2000,
|
|
208
|
+
taskIdleTimeoutMs: 2 * 60 * 60 * 1000, // 2 hour gap → new task
|
|
209
|
+
taskSummaryMaxTokens: 2000,
|
|
191
210
|
} as const;
|
|
192
211
|
|
|
193
212
|
// ─── Plugin Hooks (OpenClaw integration) ───
|