@memtensor/memos-local-openclaw-plugin 0.3.17 → 0.3.19
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/README.md +21 -11
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +7 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/dedup.d.ts +2 -2
- package/dist/ingest/dedup.d.ts.map +1 -1
- package/dist/ingest/dedup.js +4 -4
- package/dist/ingest/dedup.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +1 -1
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +14 -13
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +7 -3
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts +5 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +77 -2
- package/dist/recall/engine.js.map +1 -1
- package/dist/skill/evolver.d.ts +2 -1
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +2 -2
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +3 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +15 -1
- package/dist/skill/generator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +24 -8
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +233 -28
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/vector.d.ts +1 -1
- package/dist/storage/vector.d.ts.map +1 -1
- package/dist/storage/vector.js +3 -3
- package/dist/storage/vector.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -3
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- 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 +107 -1
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +1 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +52 -3
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +171 -7
- package/package.json +1 -1
- package/scripts/postinstall.cjs +31 -15
- package/skill/browserwing-admin/SKILL.md +521 -0
- package/skill/browserwing-executor/SKILL.md +510 -0
- package/skill/memos-memory-guide/SKILL.md +62 -36
- package/src/capture/index.ts +7 -1
- package/src/index.ts +3 -2
- package/src/ingest/dedup.ts +4 -2
- package/src/ingest/task-processor.ts +14 -13
- package/src/ingest/worker.ts +7 -3
- package/src/recall/engine.ts +94 -4
- package/src/skill/evolver.ts +3 -1
- package/src/skill/generator.ts +15 -0
- package/src/storage/sqlite.ts +262 -34
- package/src/storage/vector.ts +3 -2
- package/src/telemetry.ts +3 -3
- package/src/types.ts +18 -0
- package/src/viewer/html.ts +107 -1
- package/src/viewer/server.ts +48 -3
package/src/storage/sqlite.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
|
|
5
|
+
import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVisibility, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
|
|
6
6
|
|
|
7
7
|
export class SqliteStore {
|
|
8
8
|
private db: Database.Database;
|
|
@@ -106,6 +106,9 @@ export class SqliteStore {
|
|
|
106
106
|
this.migrateApiLogs();
|
|
107
107
|
this.migrateDedupStatus();
|
|
108
108
|
this.migrateChunksIndexesForRecall();
|
|
109
|
+
this.migrateOwnerFields();
|
|
110
|
+
this.migrateSkillVisibility();
|
|
111
|
+
this.migrateSkillEmbeddingsAndFts();
|
|
109
112
|
this.log.debug("Database schema initialized");
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -113,6 +116,85 @@ export class SqliteStore {
|
|
|
113
116
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
private migrateOwnerFields(): void {
|
|
120
|
+
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
121
|
+
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
122
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
123
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_owner ON chunks(owner)");
|
|
124
|
+
this.log.info("Migrated: added owner column to chunks");
|
|
125
|
+
}
|
|
126
|
+
const taskCols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
|
127
|
+
if (!taskCols.some((c) => c.name === "owner")) {
|
|
128
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
129
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner)");
|
|
130
|
+
this.log.info("Migrated: added owner column to tasks");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private migrateSkillVisibility(): void {
|
|
135
|
+
const cols = this.db.prepare("PRAGMA table_info(skills)").all() as Array<{ name: string }>;
|
|
136
|
+
if (!cols.some((c) => c.name === "owner")) {
|
|
137
|
+
this.db.exec("ALTER TABLE skills ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
138
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner)");
|
|
139
|
+
this.log.info("Migrated: added owner column to skills");
|
|
140
|
+
}
|
|
141
|
+
if (!cols.some((c) => c.name === "visibility")) {
|
|
142
|
+
this.db.exec("ALTER TABLE skills ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private'");
|
|
143
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility)");
|
|
144
|
+
this.log.info("Migrated: added visibility column to skills");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private migrateSkillEmbeddingsAndFts(): void {
|
|
149
|
+
this.db.exec(`
|
|
150
|
+
CREATE TABLE IF NOT EXISTS skill_embeddings (
|
|
151
|
+
skill_id TEXT PRIMARY KEY REFERENCES skills(id) ON DELETE CASCADE,
|
|
152
|
+
vector BLOB NOT NULL,
|
|
153
|
+
dimensions INTEGER NOT NULL,
|
|
154
|
+
updated_at INTEGER NOT NULL
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
158
|
+
name,
|
|
159
|
+
description,
|
|
160
|
+
content='skills',
|
|
161
|
+
content_rowid='rowid',
|
|
162
|
+
tokenize='porter unicode61'
|
|
163
|
+
);
|
|
164
|
+
`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
this.db.exec(`
|
|
168
|
+
CREATE TRIGGER IF NOT EXISTS skills_ai AFTER INSERT ON skills BEGIN
|
|
169
|
+
INSERT INTO skills_fts(rowid, name, description)
|
|
170
|
+
VALUES (new.rowid, new.name, new.description);
|
|
171
|
+
END;
|
|
172
|
+
CREATE TRIGGER IF NOT EXISTS skills_ad AFTER DELETE ON skills BEGIN
|
|
173
|
+
INSERT INTO skills_fts(skills_fts, rowid, name, description)
|
|
174
|
+
VALUES ('delete', old.rowid, old.name, old.description);
|
|
175
|
+
END;
|
|
176
|
+
CREATE TRIGGER IF NOT EXISTS skills_au AFTER UPDATE ON skills BEGIN
|
|
177
|
+
INSERT INTO skills_fts(skills_fts, rowid, name, description)
|
|
178
|
+
VALUES ('delete', old.rowid, old.name, old.description);
|
|
179
|
+
INSERT INTO skills_fts(rowid, name, description)
|
|
180
|
+
VALUES (new.rowid, new.name, new.description);
|
|
181
|
+
END;
|
|
182
|
+
`);
|
|
183
|
+
} catch {
|
|
184
|
+
// triggers may already exist
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Backfill FTS for existing skills
|
|
188
|
+
try {
|
|
189
|
+
const count = (this.db.prepare("SELECT COUNT(*) as c FROM skills_fts").get() as { c: number }).c;
|
|
190
|
+
const skillCount = (this.db.prepare("SELECT COUNT(*) as c FROM skills").get() as { c: number }).c;
|
|
191
|
+
if (count === 0 && skillCount > 0) {
|
|
192
|
+
this.db.exec("INSERT INTO skills_fts(rowid, name, description) SELECT rowid, name, description FROM skills");
|
|
193
|
+
this.log.info(`Migrated: backfilled skills_fts for ${skillCount} skills`);
|
|
194
|
+
}
|
|
195
|
+
} catch { /* best-effort */ }
|
|
196
|
+
}
|
|
197
|
+
|
|
116
198
|
private migrateTaskId(): void {
|
|
117
199
|
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
118
200
|
if (!cols.some((c) => c.name === "task_id")) {
|
|
@@ -506,8 +588,8 @@ export class SqliteStore {
|
|
|
506
588
|
|
|
507
589
|
insertChunk(chunk: Chunk): void {
|
|
508
590
|
const stmt = this.db.prepare(`
|
|
509
|
-
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)
|
|
510
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
591
|
+
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, owner, dedup_status, dedup_target, dedup_reason, created_at, updated_at)
|
|
592
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
593
|
`);
|
|
512
594
|
stmt.run(
|
|
513
595
|
chunk.id,
|
|
@@ -520,6 +602,7 @@ export class SqliteStore {
|
|
|
520
602
|
chunk.summary,
|
|
521
603
|
chunk.taskId,
|
|
522
604
|
contentHash(chunk.content),
|
|
605
|
+
chunk.owner ?? "agent:main",
|
|
523
606
|
chunk.dedupStatus ?? "active",
|
|
524
607
|
chunk.dedupTarget ?? null,
|
|
525
608
|
chunk.dedupReason ?? null,
|
|
@@ -581,19 +664,28 @@ export class SqliteStore {
|
|
|
581
664
|
|
|
582
665
|
// ─── FTS Search ───
|
|
583
666
|
|
|
584
|
-
ftsSearch(query: string, limit: number): Array<{ chunkId: string; score: number }> {
|
|
667
|
+
ftsSearch(query: string, limit: number, ownerFilter?: string[]): Array<{ chunkId: string; score: number }> {
|
|
585
668
|
const sanitized = sanitizeFtsQuery(query);
|
|
586
669
|
if (!sanitized) return [];
|
|
587
670
|
|
|
588
671
|
try {
|
|
589
|
-
|
|
672
|
+
let sql = `
|
|
590
673
|
SELECT c.id as chunk_id, rank
|
|
591
674
|
FROM chunks_fts f
|
|
592
675
|
JOIN chunks c ON c.rowid = f.rowid
|
|
593
|
-
WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
676
|
+
WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'`;
|
|
677
|
+
const params: any[] = [sanitized];
|
|
678
|
+
|
|
679
|
+
if (ownerFilter && ownerFilter.length > 0) {
|
|
680
|
+
const placeholders = ownerFilter.map(() => "?").join(",");
|
|
681
|
+
sql += ` AND c.owner IN (${placeholders})`;
|
|
682
|
+
params.push(...ownerFilter);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
686
|
+
params.push(limit);
|
|
687
|
+
|
|
688
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; rank: number }>;
|
|
597
689
|
|
|
598
690
|
if (rows.length === 0) return [];
|
|
599
691
|
const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
|
|
@@ -642,12 +734,19 @@ export class SqliteStore {
|
|
|
642
734
|
|
|
643
735
|
// ─── Vector Search ───
|
|
644
736
|
|
|
645
|
-
getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
|
|
646
|
-
|
|
647
|
-
`SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
|
|
737
|
+
getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
738
|
+
let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
|
|
648
739
|
JOIN chunks c ON c.id = e.chunk_id
|
|
649
|
-
WHERE c.dedup_status = 'active'
|
|
650
|
-
|
|
740
|
+
WHERE c.dedup_status = 'active'`;
|
|
741
|
+
const params: any[] = [];
|
|
742
|
+
|
|
743
|
+
if (ownerFilter && ownerFilter.length > 0) {
|
|
744
|
+
const placeholders = ownerFilter.map(() => "?").join(",");
|
|
745
|
+
sql += ` AND c.owner IN (${placeholders})`;
|
|
746
|
+
params.push(...ownerFilter);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
651
750
|
|
|
652
751
|
return rows.map((r) => ({
|
|
653
752
|
chunkId: r.chunk_id,
|
|
@@ -655,17 +754,25 @@ export class SqliteStore {
|
|
|
655
754
|
}));
|
|
656
755
|
}
|
|
657
756
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
`SELECT e.chunk_id, e.vector, e.dimensions
|
|
757
|
+
getRecentEmbeddings(limit: number, ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
758
|
+
if (limit <= 0) return this.getAllEmbeddings(ownerFilter);
|
|
759
|
+
|
|
760
|
+
let sql = `SELECT e.chunk_id, e.vector, e.dimensions
|
|
663
761
|
FROM chunks c
|
|
664
762
|
JOIN embeddings e ON e.chunk_id = c.id
|
|
665
|
-
WHERE c.dedup_status = 'active'
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
763
|
+
WHERE c.dedup_status = 'active'`;
|
|
764
|
+
const params: any[] = [];
|
|
765
|
+
|
|
766
|
+
if (ownerFilter && ownerFilter.length > 0) {
|
|
767
|
+
const placeholders = ownerFilter.map(() => "?").join(",");
|
|
768
|
+
sql += ` AND c.owner IN (${placeholders})`;
|
|
769
|
+
params.push(...ownerFilter);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
sql += ` ORDER BY c.created_at DESC LIMIT ?`;
|
|
773
|
+
params.push(limit);
|
|
774
|
+
|
|
775
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
669
776
|
|
|
670
777
|
return rows.map((r) => ({
|
|
671
778
|
chunkId: r.chunk_id,
|
|
@@ -683,7 +790,7 @@ export class SqliteStore {
|
|
|
683
790
|
|
|
684
791
|
// ─── Update ───
|
|
685
792
|
|
|
686
|
-
updateChunk(chunkId: string, fields: { summary?: string; content?: string; role?: string; kind?: string }): boolean {
|
|
793
|
+
updateChunk(chunkId: string, fields: { summary?: string; content?: string; role?: string; kind?: string; owner?: string }): boolean {
|
|
687
794
|
const sets: string[] = [];
|
|
688
795
|
const params: unknown[] = [];
|
|
689
796
|
|
|
@@ -703,6 +810,10 @@ export class SqliteStore {
|
|
|
703
810
|
sets.push("kind = ?");
|
|
704
811
|
params.push(fields.kind);
|
|
705
812
|
}
|
|
813
|
+
if (fields.owner !== undefined) {
|
|
814
|
+
sets.push("owner = ?");
|
|
815
|
+
params.push(fields.owner);
|
|
816
|
+
}
|
|
706
817
|
if (sets.length === 0) return false;
|
|
707
818
|
|
|
708
819
|
sets.push("updated_at = ?");
|
|
@@ -747,9 +858,9 @@ export class SqliteStore {
|
|
|
747
858
|
|
|
748
859
|
insertTask(task: Task): void {
|
|
749
860
|
this.db.prepare(`
|
|
750
|
-
INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, started_at, ended_at, updated_at)
|
|
751
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
752
|
-
`).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.startedAt, task.endedAt, task.updatedAt);
|
|
861
|
+
INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, owner, started_at, ended_at, updated_at)
|
|
862
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
863
|
+
`).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.owner ?? "agent:main", task.startedAt, task.endedAt, task.updatedAt);
|
|
753
864
|
}
|
|
754
865
|
|
|
755
866
|
getTask(taskId: string): Task | null {
|
|
@@ -757,7 +868,13 @@ export class SqliteStore {
|
|
|
757
868
|
return row ? rowToTask(row) : null;
|
|
758
869
|
}
|
|
759
870
|
|
|
760
|
-
getActiveTask(sessionKey: string): Task | null {
|
|
871
|
+
getActiveTask(sessionKey: string, owner?: string): Task | null {
|
|
872
|
+
if (owner) {
|
|
873
|
+
const row = this.db.prepare(
|
|
874
|
+
"SELECT * FROM tasks WHERE session_key = ? AND status = 'active' AND owner = ? ORDER BY started_at DESC LIMIT 1",
|
|
875
|
+
).get(sessionKey, owner) as TaskRow | undefined;
|
|
876
|
+
return row ? rowToTask(row) : null;
|
|
877
|
+
}
|
|
761
878
|
const row = this.db.prepare(
|
|
762
879
|
"SELECT * FROM tasks WHERE session_key = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1",
|
|
763
880
|
).get(sessionKey) as TaskRow | undefined;
|
|
@@ -785,7 +902,13 @@ export class SqliteStore {
|
|
|
785
902
|
return rows.map(rowToTask);
|
|
786
903
|
}
|
|
787
904
|
|
|
788
|
-
getAllActiveTasks(): Task[] {
|
|
905
|
+
getAllActiveTasks(owner?: string): Task[] {
|
|
906
|
+
if (owner) {
|
|
907
|
+
const rows = this.db.prepare(
|
|
908
|
+
"SELECT * FROM tasks WHERE status = 'active' AND owner = ? ORDER BY started_at DESC",
|
|
909
|
+
).all(owner) as TaskRow[];
|
|
910
|
+
return rows.map(rowToTask);
|
|
911
|
+
}
|
|
789
912
|
const rows = this.db.prepare(
|
|
790
913
|
"SELECT * FROM tasks WHERE status = 'active' ORDER BY started_at DESC",
|
|
791
914
|
).all() as TaskRow[];
|
|
@@ -812,10 +935,11 @@ export class SqliteStore {
|
|
|
812
935
|
return rows.map(rowToChunk);
|
|
813
936
|
}
|
|
814
937
|
|
|
815
|
-
listTasks(opts: { status?: string; limit?: number; offset?: number } = {}): { tasks: Task[]; total: number } {
|
|
938
|
+
listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string } = {}): { tasks: Task[]; total: number } {
|
|
816
939
|
const conditions: string[] = [];
|
|
817
940
|
const params: unknown[] = [];
|
|
818
941
|
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
942
|
+
if (opts.owner) { conditions.push("owner = ?"); params.push(opts.owner); }
|
|
819
943
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
820
944
|
|
|
821
945
|
const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
|
|
@@ -839,7 +963,13 @@ export class SqliteStore {
|
|
|
839
963
|
this.db.prepare("UPDATE chunks SET task_id = ?, updated_at = ? WHERE id = ?").run(taskId, Date.now(), chunkId);
|
|
840
964
|
}
|
|
841
965
|
|
|
842
|
-
getUnassignedChunks(sessionKey: string): Chunk[] {
|
|
966
|
+
getUnassignedChunks(sessionKey: string, owner?: string): Chunk[] {
|
|
967
|
+
if (owner) {
|
|
968
|
+
const rows = this.db.prepare(
|
|
969
|
+
"SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL AND owner = ? ORDER BY created_at, seq",
|
|
970
|
+
).all(sessionKey, owner) as ChunkRow[];
|
|
971
|
+
return rows.map(rowToChunk);
|
|
972
|
+
}
|
|
843
973
|
const rows = this.db.prepare(
|
|
844
974
|
"SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL ORDER BY created_at, seq",
|
|
845
975
|
).all(sessionKey) as ChunkRow[];
|
|
@@ -877,9 +1007,9 @@ export class SqliteStore {
|
|
|
877
1007
|
|
|
878
1008
|
insertSkill(skill: Skill): void {
|
|
879
1009
|
this.db.prepare(`
|
|
880
|
-
INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, quality_score, created_at, updated_at)
|
|
881
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
882
|
-
`).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);
|
|
1010
|
+
INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, owner, visibility, quality_score, created_at, updated_at)
|
|
1011
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1012
|
+
`).run(skill.id, skill.name, skill.description, skill.version, skill.status, skill.tags, skill.sourceType, skill.dirPath, skill.installed, skill.owner ?? "agent:main", skill.visibility ?? "private", skill.qualityScore, skill.createdAt, skill.updatedAt);
|
|
883
1013
|
}
|
|
884
1014
|
|
|
885
1015
|
getSkill(skillId: string): Skill | null {
|
|
@@ -914,6 +1044,96 @@ export class SqliteStore {
|
|
|
914
1044
|
return rows.map(rowToSkill);
|
|
915
1045
|
}
|
|
916
1046
|
|
|
1047
|
+
// ─── Skill Visibility & Embeddings ───
|
|
1048
|
+
|
|
1049
|
+
setSkillVisibility(skillId: string, visibility: SkillVisibility): void {
|
|
1050
|
+
this.db.prepare("UPDATE skills SET visibility = ?, updated_at = ? WHERE id = ?")
|
|
1051
|
+
.run(visibility, Date.now(), skillId);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
upsertSkillEmbedding(skillId: string, vector: number[]): void {
|
|
1055
|
+
const buf = Buffer.from(new Float32Array(vector).buffer);
|
|
1056
|
+
this.db.prepare(`
|
|
1057
|
+
INSERT OR REPLACE INTO skill_embeddings (skill_id, vector, dimensions, updated_at)
|
|
1058
|
+
VALUES (?, ?, ?, ?)
|
|
1059
|
+
`).run(skillId, buf, vector.length, Date.now());
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
getSkillEmbedding(skillId: string): number[] | null {
|
|
1063
|
+
const row = this.db.prepare(
|
|
1064
|
+
"SELECT vector, dimensions FROM skill_embeddings WHERE skill_id = ?",
|
|
1065
|
+
).get(skillId) as { vector: Buffer; dimensions: number } | undefined;
|
|
1066
|
+
if (!row) return null;
|
|
1067
|
+
return Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
getSkillEmbeddings(scope: "self" | "public" | "mix", currentOwner: string): Array<{ skillId: string; vector: number[] }> {
|
|
1071
|
+
let sql = `SELECT se.skill_id, se.vector, se.dimensions
|
|
1072
|
+
FROM skill_embeddings se
|
|
1073
|
+
JOIN skills s ON s.id = se.skill_id
|
|
1074
|
+
WHERE s.status = 'active'`;
|
|
1075
|
+
const params: any[] = [];
|
|
1076
|
+
|
|
1077
|
+
if (scope === "self") {
|
|
1078
|
+
sql += ` AND s.owner = ?`;
|
|
1079
|
+
params.push(currentOwner);
|
|
1080
|
+
} else if (scope === "public") {
|
|
1081
|
+
sql += ` AND s.visibility = 'public'`;
|
|
1082
|
+
} else {
|
|
1083
|
+
sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
|
|
1084
|
+
params.push(currentOwner);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
|
|
1088
|
+
return rows.map((r) => ({
|
|
1089
|
+
skillId: r.skill_id,
|
|
1090
|
+
vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
skillFtsSearch(query: string, limit: number, scope: "self" | "public" | "mix", currentOwner: string): Array<{ skillId: string; score: number }> {
|
|
1095
|
+
const sanitized = sanitizeFtsQuery(query);
|
|
1096
|
+
if (!sanitized) return [];
|
|
1097
|
+
|
|
1098
|
+
try {
|
|
1099
|
+
let sql = `
|
|
1100
|
+
SELECT s.id as skill_id, rank
|
|
1101
|
+
FROM skills_fts f
|
|
1102
|
+
JOIN skills s ON s.rowid = f.rowid
|
|
1103
|
+
WHERE skills_fts MATCH ? AND s.status = 'active'`;
|
|
1104
|
+
const params: any[] = [sanitized];
|
|
1105
|
+
|
|
1106
|
+
if (scope === "self") {
|
|
1107
|
+
sql += ` AND s.owner = ?`;
|
|
1108
|
+
params.push(currentOwner);
|
|
1109
|
+
} else if (scope === "public") {
|
|
1110
|
+
sql += ` AND s.visibility = 'public'`;
|
|
1111
|
+
} else {
|
|
1112
|
+
sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
|
|
1113
|
+
params.push(currentOwner);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
1117
|
+
params.push(limit);
|
|
1118
|
+
|
|
1119
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ skill_id: string; rank: number }>;
|
|
1120
|
+
if (rows.length === 0) return [];
|
|
1121
|
+
const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
|
|
1122
|
+
return rows.map((r) => ({
|
|
1123
|
+
skillId: r.skill_id,
|
|
1124
|
+
score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
|
|
1125
|
+
}));
|
|
1126
|
+
} catch {
|
|
1127
|
+
this.log.warn(`Skill FTS query failed for: "${sanitized}", returning empty`);
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
listPublicSkills(): Skill[] {
|
|
1133
|
+
const rows = this.db.prepare("SELECT * FROM skills WHERE visibility = 'public' AND status = 'active' ORDER BY updated_at DESC").all() as SkillRow[];
|
|
1134
|
+
return rows.map(rowToSkill);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
917
1137
|
// ─── Skill Versions ───
|
|
918
1138
|
|
|
919
1139
|
insertSkillVersion(sv: SkillVersion): void {
|
|
@@ -1029,6 +1249,7 @@ interface ChunkRow {
|
|
|
1029
1249
|
summary: string;
|
|
1030
1250
|
task_id: string | null;
|
|
1031
1251
|
skill_id: string | null;
|
|
1252
|
+
owner: string;
|
|
1032
1253
|
dedup_status: string;
|
|
1033
1254
|
dedup_target: string | null;
|
|
1034
1255
|
dedup_reason: string | null;
|
|
@@ -1052,6 +1273,7 @@ function rowToChunk(row: ChunkRow): Chunk {
|
|
|
1052
1273
|
embedding: null,
|
|
1053
1274
|
taskId: row.task_id,
|
|
1054
1275
|
skillId: row.skill_id ?? null,
|
|
1276
|
+
owner: row.owner ?? "agent:main",
|
|
1055
1277
|
dedupStatus: (row.dedup_status ?? "active") as DedupStatus,
|
|
1056
1278
|
dedupTarget: row.dedup_target ?? null,
|
|
1057
1279
|
dedupReason: row.dedup_reason ?? null,
|
|
@@ -1069,6 +1291,7 @@ interface TaskRow {
|
|
|
1069
1291
|
title: string;
|
|
1070
1292
|
summary: string;
|
|
1071
1293
|
status: string;
|
|
1294
|
+
owner: string;
|
|
1072
1295
|
started_at: number;
|
|
1073
1296
|
ended_at: number | null;
|
|
1074
1297
|
updated_at: number;
|
|
@@ -1081,6 +1304,7 @@ function rowToTask(row: TaskRow): Task {
|
|
|
1081
1304
|
title: row.title,
|
|
1082
1305
|
summary: row.summary,
|
|
1083
1306
|
status: row.status as Task["status"],
|
|
1307
|
+
owner: row.owner ?? "agent:main",
|
|
1084
1308
|
startedAt: row.started_at,
|
|
1085
1309
|
endedAt: row.ended_at,
|
|
1086
1310
|
updatedAt: row.updated_at,
|
|
@@ -1097,6 +1321,8 @@ interface SkillRow {
|
|
|
1097
1321
|
source_type: string;
|
|
1098
1322
|
dir_path: string;
|
|
1099
1323
|
installed: number;
|
|
1324
|
+
owner: string;
|
|
1325
|
+
visibility: string;
|
|
1100
1326
|
quality_score: number | null;
|
|
1101
1327
|
created_at: number;
|
|
1102
1328
|
updated_at: number;
|
|
@@ -1113,6 +1339,8 @@ function rowToSkill(row: SkillRow): Skill {
|
|
|
1113
1339
|
sourceType: row.source_type as Skill["sourceType"],
|
|
1114
1340
|
dirPath: row.dir_path,
|
|
1115
1341
|
installed: row.installed,
|
|
1342
|
+
owner: row.owner ?? "agent:main",
|
|
1343
|
+
visibility: (row.visibility ?? "private") as Skill["visibility"],
|
|
1116
1344
|
qualityScore: row.quality_score ?? null,
|
|
1117
1345
|
createdAt: row.created_at,
|
|
1118
1346
|
updatedAt: row.updated_at,
|
package/src/storage/vector.ts
CHANGED
|
@@ -28,10 +28,11 @@ export function vectorSearch(
|
|
|
28
28
|
queryVec: number[],
|
|
29
29
|
topK: number,
|
|
30
30
|
maxChunks?: number,
|
|
31
|
+
ownerFilter?: string[],
|
|
31
32
|
): VectorHit[] {
|
|
32
33
|
const all = maxChunks != null && maxChunks > 0
|
|
33
|
-
? store.getRecentEmbeddings(maxChunks)
|
|
34
|
-
: store.getAllEmbeddings();
|
|
34
|
+
? store.getRecentEmbeddings(maxChunks, ownerFilter)
|
|
35
|
+
: store.getAllEmbeddings(ownerFilter);
|
|
35
36
|
const scored: VectorHit[] = all.map((row) => ({
|
|
36
37
|
chunkId: row.chunkId,
|
|
37
38
|
score: cosineSimilarity(queryVec, row.vector),
|
package/src/telemetry.ts
CHANGED
|
@@ -113,8 +113,7 @@ export class Telemetry {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
trackToolCalled(toolName: string, latencyMs: number, success: boolean): void {
|
|
116
|
-
this.capture(
|
|
117
|
-
tool_name: toolName,
|
|
116
|
+
this.capture(toolName, {
|
|
118
117
|
latency_ms: Math.round(latencyMs),
|
|
119
118
|
success,
|
|
120
119
|
});
|
|
@@ -144,7 +143,8 @@ export class Telemetry {
|
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
trackAutoRecall(hitCount: number, latencyMs: number): void {
|
|
147
|
-
this.capture("
|
|
146
|
+
this.capture("memory_search", {
|
|
147
|
+
auto: true,
|
|
148
148
|
hit_count: hitCount,
|
|
149
149
|
latency_ms: Math.round(latencyMs),
|
|
150
150
|
});
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface ConversationMessage {
|
|
|
9
9
|
turnId: string;
|
|
10
10
|
sessionKey: string;
|
|
11
11
|
toolName?: string;
|
|
12
|
+
owner?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
// ─── Chunk & Storage ───
|
|
@@ -27,6 +28,7 @@ export interface Chunk {
|
|
|
27
28
|
embedding: number[] | null;
|
|
28
29
|
taskId: string | null;
|
|
29
30
|
skillId: string | null;
|
|
31
|
+
owner: string;
|
|
30
32
|
dedupStatus: DedupStatus;
|
|
31
33
|
dedupTarget: string | null;
|
|
32
34
|
dedupReason: string | null;
|
|
@@ -47,6 +49,7 @@ export interface Task {
|
|
|
47
49
|
title: string;
|
|
48
50
|
summary: string;
|
|
49
51
|
status: TaskStatus;
|
|
52
|
+
owner: string;
|
|
50
53
|
startedAt: number;
|
|
51
54
|
endedAt: number | null;
|
|
52
55
|
updatedAt: number;
|
|
@@ -77,6 +80,7 @@ export interface SearchHit {
|
|
|
77
80
|
score: number;
|
|
78
81
|
taskId: string | null;
|
|
79
82
|
skillId: string | null;
|
|
83
|
+
owner?: string;
|
|
80
84
|
source: {
|
|
81
85
|
ts: number;
|
|
82
86
|
role: Role;
|
|
@@ -84,6 +88,16 @@ export interface SearchHit {
|
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
export interface SkillSearchHit {
|
|
92
|
+
skillId: string;
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
owner: string;
|
|
96
|
+
visibility: SkillVisibility;
|
|
97
|
+
score: number;
|
|
98
|
+
reason: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
export interface SearchResult {
|
|
88
102
|
hits: SearchHit[];
|
|
89
103
|
meta: {
|
|
@@ -176,6 +190,8 @@ export type SkillStatus = "active" | "archived" | "draft";
|
|
|
176
190
|
export type SkillUpgradeType = "create" | "refine" | "extend" | "fix";
|
|
177
191
|
export type TaskSkillRelation = "generated_from" | "evolved_from" | "applied_to";
|
|
178
192
|
|
|
193
|
+
export type SkillVisibility = "private" | "public";
|
|
194
|
+
|
|
179
195
|
export interface Skill {
|
|
180
196
|
id: string;
|
|
181
197
|
name: string;
|
|
@@ -186,6 +202,8 @@ export interface Skill {
|
|
|
186
202
|
sourceType: "task" | "manual";
|
|
187
203
|
dirPath: string;
|
|
188
204
|
installed: number;
|
|
205
|
+
owner: string;
|
|
206
|
+
visibility: SkillVisibility;
|
|
189
207
|
qualityScore: number | null;
|
|
190
208
|
createdAt: number;
|
|
191
209
|
updatedAt: number;
|