@memtensor/memos-local-openclaw-plugin 1.0.6-beta.9 → 1.0.7-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/index.ts +266 -273
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +3 -5
  4. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  5. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  6. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  7. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  8. package/scripts/postinstall.cjs +44 -44
  9. package/skill/memos-memory-guide/SKILL.md +25 -2
  10. package/src/context-engine/index.ts +321 -0
  11. package/src/ingest/providers/index.ts +14 -1
  12. package/src/shared/llm-call.ts +14 -1
  13. package/src/update-check.ts +2 -7
  14. package/src/viewer/html.ts +4 -4
  15. package/src/viewer/server.ts +25 -3
  16. package/telemetry.credentials.json +5 -0
  17. package/dist/capture/index.d.ts +0 -26
  18. package/dist/capture/index.d.ts.map +0 -1
  19. package/dist/capture/index.js +0 -283
  20. package/dist/capture/index.js.map +0 -1
  21. package/dist/client/connector.d.ts +0 -34
  22. package/dist/client/connector.d.ts.map +0 -1
  23. package/dist/client/connector.js +0 -381
  24. package/dist/client/connector.js.map +0 -1
  25. package/dist/client/hub.d.ts +0 -61
  26. package/dist/client/hub.d.ts.map +0 -1
  27. package/dist/client/hub.js +0 -174
  28. package/dist/client/hub.js.map +0 -1
  29. package/dist/client/skill-sync.d.ts +0 -36
  30. package/dist/client/skill-sync.d.ts.map +0 -1
  31. package/dist/client/skill-sync.js +0 -226
  32. package/dist/client/skill-sync.js.map +0 -1
  33. package/dist/config.d.ts +0 -5
  34. package/dist/config.d.ts.map +0 -1
  35. package/dist/config.js +0 -171
  36. package/dist/config.js.map +0 -1
  37. package/dist/embedding/index.d.ts +0 -14
  38. package/dist/embedding/index.d.ts.map +0 -1
  39. package/dist/embedding/index.js +0 -105
  40. package/dist/embedding/index.js.map +0 -1
  41. package/dist/embedding/local.d.ts +0 -3
  42. package/dist/embedding/local.d.ts.map +0 -1
  43. package/dist/embedding/local.js +0 -66
  44. package/dist/embedding/local.js.map +0 -1
  45. package/dist/embedding/providers/cohere.d.ts +0 -4
  46. package/dist/embedding/providers/cohere.d.ts.map +0 -1
  47. package/dist/embedding/providers/cohere.js +0 -57
  48. package/dist/embedding/providers/cohere.js.map +0 -1
  49. package/dist/embedding/providers/gemini.d.ts +0 -3
  50. package/dist/embedding/providers/gemini.d.ts.map +0 -1
  51. package/dist/embedding/providers/gemini.js +0 -31
  52. package/dist/embedding/providers/gemini.js.map +0 -1
  53. package/dist/embedding/providers/mistral.d.ts +0 -3
  54. package/dist/embedding/providers/mistral.d.ts.map +0 -1
  55. package/dist/embedding/providers/mistral.js +0 -25
  56. package/dist/embedding/providers/mistral.js.map +0 -1
  57. package/dist/embedding/providers/openai.d.ts +0 -3
  58. package/dist/embedding/providers/openai.d.ts.map +0 -1
  59. package/dist/embedding/providers/openai.js +0 -35
  60. package/dist/embedding/providers/openai.js.map +0 -1
  61. package/dist/embedding/providers/voyage.d.ts +0 -3
  62. package/dist/embedding/providers/voyage.d.ts.map +0 -1
  63. package/dist/embedding/providers/voyage.js +0 -25
  64. package/dist/embedding/providers/voyage.js.map +0 -1
  65. package/dist/hub/auth.d.ts +0 -19
  66. package/dist/hub/auth.d.ts.map +0 -1
  67. package/dist/hub/auth.js +0 -70
  68. package/dist/hub/auth.js.map +0 -1
  69. package/dist/hub/server.d.ts +0 -52
  70. package/dist/hub/server.d.ts.map +0 -1
  71. package/dist/hub/server.js +0 -1197
  72. package/dist/hub/server.js.map +0 -1
  73. package/dist/hub/user-manager.d.ts +0 -40
  74. package/dist/hub/user-manager.d.ts.map +0 -1
  75. package/dist/hub/user-manager.js +0 -153
  76. package/dist/hub/user-manager.js.map +0 -1
  77. package/dist/index.d.ts +0 -46
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js +0 -82
  80. package/dist/index.js.map +0 -1
  81. package/dist/ingest/chunker.d.ts +0 -15
  82. package/dist/ingest/chunker.d.ts.map +0 -1
  83. package/dist/ingest/chunker.js +0 -192
  84. package/dist/ingest/chunker.js.map +0 -1
  85. package/dist/ingest/dedup.d.ts +0 -19
  86. package/dist/ingest/dedup.d.ts.map +0 -1
  87. package/dist/ingest/dedup.js +0 -50
  88. package/dist/ingest/dedup.js.map +0 -1
  89. package/dist/ingest/providers/anthropic.d.ts +0 -21
  90. package/dist/ingest/providers/anthropic.d.ts.map +0 -1
  91. package/dist/ingest/providers/anthropic.js +0 -314
  92. package/dist/ingest/providers/anthropic.js.map +0 -1
  93. package/dist/ingest/providers/bedrock.d.ts +0 -21
  94. package/dist/ingest/providers/bedrock.d.ts.map +0 -1
  95. package/dist/ingest/providers/bedrock.js +0 -313
  96. package/dist/ingest/providers/bedrock.js.map +0 -1
  97. package/dist/ingest/providers/gemini.d.ts +0 -21
  98. package/dist/ingest/providers/gemini.d.ts.map +0 -1
  99. package/dist/ingest/providers/gemini.js +0 -298
  100. package/dist/ingest/providers/gemini.js.map +0 -1
  101. package/dist/ingest/providers/index.d.ts +0 -68
  102. package/dist/ingest/providers/index.d.ts.map +0 -1
  103. package/dist/ingest/providers/index.js +0 -611
  104. package/dist/ingest/providers/index.js.map +0 -1
  105. package/dist/ingest/providers/openai.d.ts +0 -30
  106. package/dist/ingest/providers/openai.d.ts.map +0 -1
  107. package/dist/ingest/providers/openai.js +0 -387
  108. package/dist/ingest/providers/openai.js.map +0 -1
  109. package/dist/ingest/task-processor.d.ts +0 -91
  110. package/dist/ingest/task-processor.d.ts.map +0 -1
  111. package/dist/ingest/task-processor.js +0 -478
  112. package/dist/ingest/task-processor.js.map +0 -1
  113. package/dist/ingest/worker.d.ts +0 -23
  114. package/dist/ingest/worker.d.ts.map +0 -1
  115. package/dist/ingest/worker.js +0 -255
  116. package/dist/ingest/worker.js.map +0 -1
  117. package/dist/openclaw-api.d.ts +0 -53
  118. package/dist/openclaw-api.d.ts.map +0 -1
  119. package/dist/openclaw-api.js +0 -189
  120. package/dist/openclaw-api.js.map +0 -1
  121. package/dist/recall/engine.d.ts +0 -28
  122. package/dist/recall/engine.d.ts.map +0 -1
  123. package/dist/recall/engine.js +0 -343
  124. package/dist/recall/engine.js.map +0 -1
  125. package/dist/recall/mmr.d.ts +0 -17
  126. package/dist/recall/mmr.d.ts.map +0 -1
  127. package/dist/recall/mmr.js +0 -53
  128. package/dist/recall/mmr.js.map +0 -1
  129. package/dist/recall/recency.d.ts +0 -20
  130. package/dist/recall/recency.d.ts.map +0 -1
  131. package/dist/recall/recency.js +0 -26
  132. package/dist/recall/recency.js.map +0 -1
  133. package/dist/recall/rrf.d.ts +0 -16
  134. package/dist/recall/rrf.d.ts.map +0 -1
  135. package/dist/recall/rrf.js +0 -15
  136. package/dist/recall/rrf.js.map +0 -1
  137. package/dist/shared/llm-call.d.ts +0 -30
  138. package/dist/shared/llm-call.d.ts.map +0 -1
  139. package/dist/shared/llm-call.js +0 -253
  140. package/dist/shared/llm-call.js.map +0 -1
  141. package/dist/sharing/types.contract.d.ts +0 -2
  142. package/dist/sharing/types.contract.d.ts.map +0 -1
  143. package/dist/sharing/types.contract.js +0 -3
  144. package/dist/sharing/types.contract.js.map +0 -1
  145. package/dist/sharing/types.d.ts +0 -80
  146. package/dist/sharing/types.d.ts.map +0 -1
  147. package/dist/sharing/types.js +0 -3
  148. package/dist/sharing/types.js.map +0 -1
  149. package/dist/skill/bundled-memory-guide.d.ts +0 -2
  150. package/dist/skill/bundled-memory-guide.d.ts.map +0 -1
  151. package/dist/skill/bundled-memory-guide.js +0 -45
  152. package/dist/skill/bundled-memory-guide.js.map +0 -1
  153. package/dist/skill/evaluator.d.ts +0 -28
  154. package/dist/skill/evaluator.d.ts.map +0 -1
  155. package/dist/skill/evaluator.js +0 -169
  156. package/dist/skill/evaluator.js.map +0 -1
  157. package/dist/skill/evolver.d.ts +0 -48
  158. package/dist/skill/evolver.d.ts.map +0 -1
  159. package/dist/skill/evolver.js +0 -406
  160. package/dist/skill/evolver.js.map +0 -1
  161. package/dist/skill/generator.d.ts +0 -26
  162. package/dist/skill/generator.d.ts.map +0 -1
  163. package/dist/skill/generator.js +0 -521
  164. package/dist/skill/generator.js.map +0 -1
  165. package/dist/skill/installer.d.ts +0 -42
  166. package/dist/skill/installer.d.ts.map +0 -1
  167. package/dist/skill/installer.js +0 -165
  168. package/dist/skill/installer.js.map +0 -1
  169. package/dist/skill/upgrader.d.ts +0 -19
  170. package/dist/skill/upgrader.d.ts.map +0 -1
  171. package/dist/skill/upgrader.js +0 -366
  172. package/dist/skill/upgrader.js.map +0 -1
  173. package/dist/skill/validator.d.ts +0 -30
  174. package/dist/skill/validator.d.ts.map +0 -1
  175. package/dist/skill/validator.js +0 -272
  176. package/dist/skill/validator.js.map +0 -1
  177. package/dist/storage/ensure-binding.d.ts +0 -12
  178. package/dist/storage/ensure-binding.d.ts.map +0 -1
  179. package/dist/storage/ensure-binding.js +0 -53
  180. package/dist/storage/ensure-binding.js.map +0 -1
  181. package/dist/storage/sqlite.d.ts +0 -649
  182. package/dist/storage/sqlite.d.ts.map +0 -1
  183. package/dist/storage/sqlite.js +0 -2657
  184. package/dist/storage/sqlite.js.map +0 -1
  185. package/dist/storage/vector.d.ts +0 -12
  186. package/dist/storage/vector.d.ts.map +0 -1
  187. package/dist/storage/vector.js +0 -34
  188. package/dist/storage/vector.js.map +0 -1
  189. package/dist/telemetry.d.ts +0 -47
  190. package/dist/telemetry.d.ts.map +0 -1
  191. package/dist/telemetry.js +0 -312
  192. package/dist/telemetry.js.map +0 -1
  193. package/dist/tools/index.d.ts +0 -5
  194. package/dist/tools/index.d.ts.map +0 -1
  195. package/dist/tools/index.js +0 -12
  196. package/dist/tools/index.js.map +0 -1
  197. package/dist/tools/memory-get.d.ts +0 -4
  198. package/dist/tools/memory-get.d.ts.map +0 -1
  199. package/dist/tools/memory-get.js +0 -64
  200. package/dist/tools/memory-get.js.map +0 -1
  201. package/dist/tools/memory-search.d.ts +0 -7
  202. package/dist/tools/memory-search.d.ts.map +0 -1
  203. package/dist/tools/memory-search.js +0 -84
  204. package/dist/tools/memory-search.js.map +0 -1
  205. package/dist/tools/memory-timeline.d.ts +0 -4
  206. package/dist/tools/memory-timeline.d.ts.map +0 -1
  207. package/dist/tools/memory-timeline.js +0 -73
  208. package/dist/tools/memory-timeline.js.map +0 -1
  209. package/dist/tools/network-memory-detail.d.ts +0 -4
  210. package/dist/tools/network-memory-detail.d.ts.map +0 -1
  211. package/dist/tools/network-memory-detail.js +0 -34
  212. package/dist/tools/network-memory-detail.js.map +0 -1
  213. package/dist/types.d.ts +0 -330
  214. package/dist/types.d.ts.map +0 -1
  215. package/dist/types.js +0 -38
  216. package/dist/types.js.map +0 -1
  217. package/dist/update-check.d.ts +0 -21
  218. package/dist/update-check.d.ts.map +0 -1
  219. package/dist/update-check.js +0 -110
  220. package/dist/update-check.js.map +0 -1
  221. package/dist/viewer/html.d.ts +0 -2
  222. package/dist/viewer/html.d.ts.map +0 -1
  223. package/dist/viewer/html.js +0 -9168
  224. package/dist/viewer/html.js.map +0 -1
  225. package/dist/viewer/server.d.ts +0 -205
  226. package/dist/viewer/server.d.ts.map +0 -1
  227. package/dist/viewer/server.js +0 -4876
  228. package/dist/viewer/server.js.map +0 -1
@@ -1,2657 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.SqliteStore = void 0;
40
- const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
- const crypto_1 = require("crypto");
42
- const fs = __importStar(require("fs"));
43
- const path = __importStar(require("path"));
44
- class SqliteStore {
45
- log;
46
- db;
47
- constructor(dbPath, log) {
48
- this.log = log;
49
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
50
- this.db = new better_sqlite3_1.default(dbPath);
51
- this.db.pragma("journal_mode = WAL");
52
- this.db.pragma("foreign_keys = ON");
53
- this.migrate();
54
- }
55
- // ─── Schema ───
56
- migrate() {
57
- this.db.exec(`
58
- CREATE TABLE IF NOT EXISTS chunks (
59
- id TEXT PRIMARY KEY,
60
- session_key TEXT NOT NULL,
61
- turn_id TEXT NOT NULL,
62
- seq INTEGER NOT NULL,
63
- role TEXT NOT NULL,
64
- content TEXT NOT NULL,
65
- kind TEXT NOT NULL DEFAULT 'paragraph',
66
- summary TEXT NOT NULL DEFAULT '',
67
- created_at INTEGER NOT NULL,
68
- updated_at INTEGER NOT NULL
69
- );
70
-
71
- CREATE INDEX IF NOT EXISTS idx_chunks_session
72
- ON chunks(session_key);
73
- CREATE INDEX IF NOT EXISTS idx_chunks_turn
74
- ON chunks(session_key, turn_id, seq);
75
- CREATE INDEX IF NOT EXISTS idx_chunks_created
76
- ON chunks(created_at);
77
- CREATE INDEX IF NOT EXISTS idx_chunks_session_created
78
- ON chunks(session_key, created_at, seq);
79
-
80
- CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
81
- summary,
82
- content,
83
- content='chunks',
84
- content_rowid='rowid',
85
- tokenize='trigram'
86
- );
87
-
88
- CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
89
- INSERT INTO chunks_fts(rowid, summary, content)
90
- VALUES (new.rowid, new.summary, new.content);
91
- END;
92
-
93
- CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
94
- INSERT INTO chunks_fts(chunks_fts, rowid, summary, content)
95
- VALUES ('delete', old.rowid, old.summary, old.content);
96
- END;
97
-
98
- CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
99
- INSERT INTO chunks_fts(chunks_fts, rowid, summary, content)
100
- VALUES ('delete', old.rowid, old.summary, old.content);
101
- INSERT INTO chunks_fts(rowid, summary, content)
102
- VALUES (new.rowid, new.summary, new.content);
103
- END;
104
-
105
- CREATE TABLE IF NOT EXISTS embeddings (
106
- chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
107
- vector BLOB NOT NULL,
108
- dimensions INTEGER NOT NULL,
109
- updated_at INTEGER NOT NULL
110
- );
111
-
112
- CREATE TABLE IF NOT EXISTS viewer_events (
113
- id INTEGER PRIMARY KEY AUTOINCREMENT,
114
- event_type TEXT NOT NULL,
115
- created_at INTEGER NOT NULL
116
- );
117
- CREATE INDEX IF NOT EXISTS idx_viewer_events_created ON viewer_events(created_at);
118
- CREATE INDEX IF NOT EXISTS idx_viewer_events_type ON viewer_events(event_type);
119
-
120
- CREATE TABLE IF NOT EXISTS tasks (
121
- id TEXT PRIMARY KEY,
122
- session_key TEXT NOT NULL,
123
- title TEXT NOT NULL DEFAULT '',
124
- summary TEXT NOT NULL DEFAULT '',
125
- status TEXT NOT NULL DEFAULT 'active',
126
- started_at INTEGER NOT NULL,
127
- ended_at INTEGER,
128
- updated_at INTEGER NOT NULL
129
- );
130
- CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_key);
131
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
132
- `);
133
- this.migrateTaskId();
134
- this.migrateContentHash();
135
- this.migrateSkillTables();
136
- this.migrateSkillId();
137
- this.migrateSkillQualityScore();
138
- this.migrateTaskSkillMeta();
139
- this.migrateToolCalls();
140
- this.migrateMergeFields();
141
- this.migrateApiLogs();
142
- this.migrateDedupStatus();
143
- this.migrateChunksIndexesForRecall();
144
- this.migrateOwnerFields();
145
- this.migrateSkillVisibility();
146
- this.migrateSkillEmbeddingsAndFts();
147
- this.migrateFtsToTrigram();
148
- this.migrateHubTables();
149
- this.migrateHubFtsToTrigram();
150
- this.migrateLocalSharedTasksOwner();
151
- this.migrateHubUserIdentityFields();
152
- this.migrateClientHubConnectionIdentityFields();
153
- this.migrateTeamSharingInstanceId();
154
- this.log.debug("Database schema initialized");
155
- }
156
- migrateChunksIndexesForRecall() {
157
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
158
- }
159
- migrateLocalSharedTasksOwner() {
160
- try {
161
- const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
162
- if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
163
- this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
164
- this.log.info("Migrated: added original_owner column to local_shared_tasks");
165
- }
166
- }
167
- catch { /* table may not exist yet */ }
168
- }
169
- migrateHubUserIdentityFields() {
170
- try {
171
- const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
172
- if (cols.length === 0)
173
- return;
174
- if (!cols.some(c => c.name === "identity_key")) {
175
- this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
176
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
177
- this.log.info("Migrated: added identity_key to hub_users");
178
- }
179
- if (!cols.some(c => c.name === "left_at")) {
180
- this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
181
- this.log.info("Migrated: added left_at to hub_users");
182
- }
183
- if (!cols.some(c => c.name === "removed_at")) {
184
- this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
185
- this.log.info("Migrated: added removed_at to hub_users");
186
- }
187
- if (!cols.some(c => c.name === "rejected_at")) {
188
- this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
189
- this.log.info("Migrated: added rejected_at to hub_users");
190
- }
191
- if (!cols.some(c => c.name === "rejoin_requested_at")) {
192
- this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
193
- this.log.info("Migrated: added rejoin_requested_at to hub_users");
194
- }
195
- }
196
- catch { /* table may not exist yet */ }
197
- }
198
- migrateClientHubConnectionIdentityFields() {
199
- try {
200
- const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all();
201
- if (cols.length === 0)
202
- return;
203
- if (!cols.some(c => c.name === "identity_key")) {
204
- this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
205
- this.log.info("Migrated: added identity_key to client_hub_connection");
206
- }
207
- if (!cols.some(c => c.name === "last_known_status")) {
208
- this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
209
- this.log.info("Migrated: added last_known_status to client_hub_connection");
210
- }
211
- }
212
- catch { /* table may not exist yet */ }
213
- }
214
- migrateTeamSharingInstanceId() {
215
- try {
216
- const tscCols = this.db.prepare("PRAGMA table_info(team_shared_chunks)").all();
217
- if (tscCols.length > 0 && !tscCols.some(c => c.name === "hub_instance_id")) {
218
- this.db.exec("ALTER TABLE team_shared_chunks ADD COLUMN hub_instance_id TEXT NOT NULL DEFAULT ''");
219
- this.log.info("Migrated: added hub_instance_id to team_shared_chunks");
220
- }
221
- }
222
- catch { /* table may not exist yet */ }
223
- try {
224
- const lstCols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
225
- if (lstCols.length > 0 && !lstCols.some(c => c.name === "hub_instance_id")) {
226
- this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN hub_instance_id TEXT NOT NULL DEFAULT ''");
227
- this.log.info("Migrated: added hub_instance_id to local_shared_tasks");
228
- }
229
- }
230
- catch { /* table may not exist yet */ }
231
- try {
232
- const connCols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all();
233
- if (connCols.length > 0 && !connCols.some(c => c.name === "hub_instance_id")) {
234
- this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN hub_instance_id TEXT NOT NULL DEFAULT ''");
235
- this.log.info("Migrated: added hub_instance_id to client_hub_connection");
236
- }
237
- }
238
- catch { /* table may not exist yet */ }
239
- this.db.exec(`
240
- CREATE TABLE IF NOT EXISTS team_shared_skills (
241
- skill_id TEXT PRIMARY KEY,
242
- hub_skill_id TEXT NOT NULL DEFAULT '',
243
- visibility TEXT NOT NULL DEFAULT 'public',
244
- group_id TEXT,
245
- hub_instance_id TEXT NOT NULL DEFAULT '',
246
- shared_at INTEGER NOT NULL
247
- )
248
- `);
249
- }
250
- migrateOwnerFields() {
251
- const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
252
- if (!chunkCols.some((c) => c.name === "owner")) {
253
- this.db.exec("ALTER TABLE chunks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
254
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_owner ON chunks(owner)");
255
- this.log.info("Migrated: added owner column to chunks");
256
- }
257
- const taskCols = this.db.prepare("PRAGMA table_info(tasks)").all();
258
- if (!taskCols.some((c) => c.name === "owner")) {
259
- this.db.exec("ALTER TABLE tasks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
260
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner)");
261
- this.log.info("Migrated: added owner column to tasks");
262
- }
263
- }
264
- migrateSkillVisibility() {
265
- const cols = this.db.prepare("PRAGMA table_info(skills)").all();
266
- if (!cols.some((c) => c.name === "owner")) {
267
- this.db.exec("ALTER TABLE skills ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
268
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner)");
269
- this.log.info("Migrated: added owner column to skills");
270
- }
271
- if (!cols.some((c) => c.name === "visibility")) {
272
- this.db.exec("ALTER TABLE skills ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private'");
273
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility)");
274
- this.log.info("Migrated: added visibility column to skills");
275
- }
276
- }
277
- migrateSkillEmbeddingsAndFts() {
278
- this.db.exec(`
279
- CREATE TABLE IF NOT EXISTS skill_embeddings (
280
- skill_id TEXT PRIMARY KEY REFERENCES skills(id) ON DELETE CASCADE,
281
- vector BLOB NOT NULL,
282
- dimensions INTEGER NOT NULL,
283
- updated_at INTEGER NOT NULL
284
- );
285
-
286
- CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
287
- name,
288
- description,
289
- content='skills',
290
- content_rowid='rowid',
291
- tokenize='trigram'
292
- );
293
- `);
294
- try {
295
- this.db.exec(`
296
- CREATE TRIGGER IF NOT EXISTS skills_ai AFTER INSERT ON skills BEGIN
297
- INSERT INTO skills_fts(rowid, name, description)
298
- VALUES (new.rowid, new.name, new.description);
299
- END;
300
- CREATE TRIGGER IF NOT EXISTS skills_ad AFTER DELETE ON skills BEGIN
301
- INSERT INTO skills_fts(skills_fts, rowid, name, description)
302
- VALUES ('delete', old.rowid, old.name, old.description);
303
- END;
304
- CREATE TRIGGER IF NOT EXISTS skills_au AFTER UPDATE ON skills BEGIN
305
- INSERT INTO skills_fts(skills_fts, rowid, name, description)
306
- VALUES ('delete', old.rowid, old.name, old.description);
307
- INSERT INTO skills_fts(rowid, name, description)
308
- VALUES (new.rowid, new.name, new.description);
309
- END;
310
- `);
311
- }
312
- catch {
313
- // triggers may already exist
314
- }
315
- // Backfill FTS for existing skills
316
- try {
317
- const count = this.db.prepare("SELECT COUNT(*) as c FROM skills_fts").get().c;
318
- const skillCount = this.db.prepare("SELECT COUNT(*) as c FROM skills").get().c;
319
- if (count === 0 && skillCount > 0) {
320
- this.db.exec("INSERT INTO skills_fts(rowid, name, description) SELECT rowid, name, description FROM skills");
321
- this.log.info(`Migrated: backfilled skills_fts for ${skillCount} skills`);
322
- }
323
- }
324
- catch { /* best-effort */ }
325
- }
326
- migrateFtsToTrigram() {
327
- // Check if chunks_fts still uses the old tokenizer (porter unicode61)
328
- try {
329
- const row = this.db.prepare("SELECT sql FROM sqlite_master WHERE name='chunks_fts'").get();
330
- if (row && row.sql && !row.sql.includes("trigram")) {
331
- this.log.info("Migrating chunks_fts from porter/unicode61 to trigram tokenizer...");
332
- this.db.exec("DROP TRIGGER IF EXISTS chunks_ai");
333
- this.db.exec("DROP TRIGGER IF EXISTS chunks_ad");
334
- this.db.exec("DROP TRIGGER IF EXISTS chunks_au");
335
- this.db.exec("DROP TABLE IF EXISTS chunks_fts");
336
- this.db.exec(`
337
- CREATE VIRTUAL TABLE chunks_fts USING fts5(
338
- summary, content, content='chunks', content_rowid='rowid',
339
- tokenize='trigram'
340
- )
341
- `);
342
- this.db.exec(`
343
- CREATE TRIGGER chunks_ai AFTER INSERT ON chunks BEGIN
344
- INSERT INTO chunks_fts(rowid, summary, content) VALUES (new.rowid, new.summary, new.content);
345
- END;
346
- CREATE TRIGGER chunks_ad AFTER DELETE ON chunks BEGIN
347
- INSERT INTO chunks_fts(chunks_fts, rowid, summary, content) VALUES ('delete', old.rowid, old.summary, old.content);
348
- END;
349
- CREATE TRIGGER chunks_au AFTER UPDATE ON chunks BEGIN
350
- INSERT INTO chunks_fts(chunks_fts, rowid, summary, content) VALUES ('delete', old.rowid, old.summary, old.content);
351
- INSERT INTO chunks_fts(rowid, summary, content) VALUES (new.rowid, new.summary, new.content);
352
- END
353
- `);
354
- this.db.exec("INSERT INTO chunks_fts(rowid, summary, content) SELECT rowid, summary, content FROM chunks");
355
- const count = this.db.prepare("SELECT COUNT(*) as c FROM chunks_fts").get().c;
356
- this.log.info(`Migrated chunks_fts to trigram: ${count} rows indexed`);
357
- }
358
- }
359
- catch (err) {
360
- this.log.warn(`Failed to migrate chunks_fts to trigram: ${err}`);
361
- }
362
- // Same for skills_fts
363
- try {
364
- const row = this.db.prepare("SELECT sql FROM sqlite_master WHERE name='skills_fts'").get();
365
- if (row && row.sql && !row.sql.includes("trigram")) {
366
- this.log.info("Migrating skills_fts to trigram tokenizer...");
367
- this.db.exec("DROP TRIGGER IF EXISTS skills_ai");
368
- this.db.exec("DROP TRIGGER IF EXISTS skills_ad");
369
- this.db.exec("DROP TRIGGER IF EXISTS skills_au");
370
- this.db.exec("DROP TABLE IF EXISTS skills_fts");
371
- this.db.exec(`
372
- CREATE VIRTUAL TABLE skills_fts USING fts5(
373
- name, description, content='skills', content_rowid='rowid',
374
- tokenize='trigram'
375
- )
376
- `);
377
- this.db.exec(`
378
- CREATE TRIGGER skills_ai AFTER INSERT ON skills BEGIN
379
- INSERT INTO skills_fts(rowid, name, description) VALUES (new.rowid, new.name, new.description);
380
- END;
381
- CREATE TRIGGER skills_ad AFTER DELETE ON skills BEGIN
382
- INSERT INTO skills_fts(skills_fts, rowid, name, description) VALUES ('delete', old.rowid, old.name, old.description);
383
- END;
384
- CREATE TRIGGER skills_au AFTER UPDATE ON skills BEGIN
385
- INSERT INTO skills_fts(skills_fts, rowid, name, description) VALUES ('delete', old.rowid, old.name, old.description);
386
- INSERT INTO skills_fts(rowid, name, description) VALUES (new.rowid, new.name, new.description);
387
- END
388
- `);
389
- this.db.exec("INSERT INTO skills_fts(rowid, name, description) SELECT rowid, name, description FROM skills");
390
- this.log.info("Migrated skills_fts to trigram");
391
- }
392
- }
393
- catch (err) {
394
- this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
395
- }
396
- }
397
- migrateHubFtsToTrigram() {
398
- const tables = [
399
- {
400
- fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
401
- triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
402
- },
403
- {
404
- fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
405
- triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
406
- },
407
- {
408
- fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
409
- triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
410
- },
411
- ];
412
- for (const t of tables) {
413
- try {
414
- const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get();
415
- if (!row || !row.sql)
416
- continue;
417
- if (row.sql.includes("trigram"))
418
- continue;
419
- this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
420
- for (const tr of t.triggers)
421
- this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
422
- this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
423
- this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
424
- this.db.exec(`
425
- CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
426
- INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
427
- END;
428
- CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
429
- INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
430
- END;
431
- CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
432
- INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
433
- INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
434
- END
435
- `);
436
- this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
437
- const cnt = this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get().c;
438
- this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
439
- }
440
- catch (err) {
441
- this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
442
- }
443
- }
444
- }
445
- migrateTaskId() {
446
- const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
447
- if (!cols.some((c) => c.name === "task_id")) {
448
- this.db.exec("ALTER TABLE chunks ADD COLUMN task_id TEXT REFERENCES tasks(id)");
449
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_task ON chunks(task_id)");
450
- this.log.info("Migrated: added task_id column to chunks");
451
- }
452
- }
453
- migrateContentHash() {
454
- const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
455
- if (!cols.some((c) => c.name === "content_hash")) {
456
- this.db.exec("ALTER TABLE chunks ADD COLUMN content_hash TEXT");
457
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup ON chunks(session_key, role, content_hash)");
458
- // Backfill existing rows
459
- const rows = this.db.prepare("SELECT id, content FROM chunks WHERE content_hash IS NULL").all();
460
- const updateStmt = this.db.prepare("UPDATE chunks SET content_hash = ? WHERE id = ?");
461
- for (const r of rows) {
462
- updateStmt.run(contentHash(r.content), r.id);
463
- }
464
- if (rows.length > 0) {
465
- this.log.info(`Migrated: backfilled content_hash for ${rows.length} chunks`);
466
- }
467
- }
468
- }
469
- migrateSkillTables() {
470
- this.db.exec(`
471
- CREATE TABLE IF NOT EXISTS skills (
472
- id TEXT PRIMARY KEY,
473
- name TEXT NOT NULL UNIQUE,
474
- description TEXT NOT NULL DEFAULT '',
475
- version INTEGER NOT NULL DEFAULT 1,
476
- status TEXT NOT NULL DEFAULT 'active',
477
- tags TEXT NOT NULL DEFAULT '[]',
478
- source_type TEXT NOT NULL DEFAULT 'task',
479
- dir_path TEXT NOT NULL DEFAULT '',
480
- installed INTEGER NOT NULL DEFAULT 0,
481
- created_at INTEGER NOT NULL,
482
- updated_at INTEGER NOT NULL
483
- );
484
- CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
485
- CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
486
-
487
- CREATE TABLE IF NOT EXISTS skill_versions (
488
- id TEXT PRIMARY KEY,
489
- skill_id TEXT NOT NULL REFERENCES skills(id),
490
- version INTEGER NOT NULL,
491
- content TEXT NOT NULL,
492
- changelog TEXT NOT NULL DEFAULT '',
493
- upgrade_type TEXT NOT NULL DEFAULT 'create',
494
- source_task_id TEXT,
495
- metrics TEXT NOT NULL DEFAULT '{}',
496
- created_at INTEGER NOT NULL,
497
- UNIQUE(skill_id, version)
498
- );
499
- CREATE INDEX IF NOT EXISTS idx_skill_versions_skill ON skill_versions(skill_id);
500
-
501
- CREATE TABLE IF NOT EXISTS task_skills (
502
- task_id TEXT NOT NULL REFERENCES tasks(id),
503
- skill_id TEXT NOT NULL REFERENCES skills(id),
504
- relation TEXT NOT NULL DEFAULT 'generated_from',
505
- version_at INTEGER NOT NULL DEFAULT 1,
506
- created_at INTEGER NOT NULL,
507
- PRIMARY KEY (task_id, skill_id)
508
- );
509
- `);
510
- }
511
- migrateSkillId() {
512
- const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
513
- if (!cols.some((c) => c.name === "skill_id")) {
514
- this.db.exec("ALTER TABLE chunks ADD COLUMN skill_id TEXT");
515
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_skill ON chunks(skill_id)");
516
- this.log.info("Migrated: added skill_id column to chunks");
517
- }
518
- }
519
- migrateSkillQualityScore() {
520
- const skillCols = this.db.prepare("PRAGMA table_info(skills)").all();
521
- if (!skillCols.some((c) => c.name === "quality_score")) {
522
- this.db.exec("ALTER TABLE skills ADD COLUMN quality_score REAL");
523
- this.log.info("Migrated: added quality_score column to skills");
524
- }
525
- const versionCols = this.db.prepare("PRAGMA table_info(skill_versions)").all();
526
- if (!versionCols.some((c) => c.name === "quality_score")) {
527
- this.db.exec("ALTER TABLE skill_versions ADD COLUMN quality_score REAL");
528
- this.log.info("Migrated: added quality_score column to skill_versions");
529
- }
530
- if (!versionCols.some((c) => c.name === "change_summary")) {
531
- this.db.exec("ALTER TABLE skill_versions ADD COLUMN change_summary TEXT NOT NULL DEFAULT ''");
532
- this.log.info("Migrated: added change_summary column to skill_versions");
533
- }
534
- }
535
- migrateTaskSkillMeta() {
536
- const cols = this.db.prepare("PRAGMA table_info(tasks)").all();
537
- if (!cols.some((c) => c.name === "skill_status")) {
538
- this.db.exec("ALTER TABLE tasks ADD COLUMN skill_status TEXT DEFAULT NULL");
539
- this.db.exec("ALTER TABLE tasks ADD COLUMN skill_reason TEXT DEFAULT NULL");
540
- this.log.info("Migrated: added skill_status/skill_reason columns to tasks");
541
- }
542
- }
543
- setTaskSkillMeta(taskId, meta) {
544
- this.db.prepare("UPDATE tasks SET skill_status = ?, skill_reason = ?, updated_at = ? WHERE id = ?")
545
- .run(meta.skillStatus, meta.skillReason, Date.now(), taskId);
546
- }
547
- getTasksBySkillStatus(statuses) {
548
- const placeholders = statuses.map(() => "?").join(",");
549
- const rows = this.db.prepare(`SELECT * FROM tasks WHERE skill_status IN (${placeholders}) AND status = 'completed' ORDER BY updated_at ASC`).all(...statuses);
550
- return rows.map(rowToTask);
551
- }
552
- migrateMergeFields() {
553
- const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
554
- if (!cols.some((c) => c.name === "merge_count")) {
555
- this.db.exec("ALTER TABLE chunks ADD COLUMN merge_count INTEGER NOT NULL DEFAULT 0");
556
- this.db.exec("ALTER TABLE chunks ADD COLUMN last_hit_at INTEGER");
557
- this.db.exec("ALTER TABLE chunks ADD COLUMN merge_history TEXT NOT NULL DEFAULT '[]'");
558
- this.log.info("Migrated: added merge_count/last_hit_at/merge_history columns to chunks");
559
- }
560
- }
561
- migrateApiLogs() {
562
- this.db.exec(`
563
- CREATE TABLE IF NOT EXISTS api_logs (
564
- id INTEGER PRIMARY KEY AUTOINCREMENT,
565
- tool_name TEXT NOT NULL,
566
- input_data TEXT NOT NULL DEFAULT '{}',
567
- output_data TEXT NOT NULL DEFAULT '',
568
- duration_ms INTEGER NOT NULL DEFAULT 0,
569
- success INTEGER NOT NULL DEFAULT 1,
570
- called_at INTEGER NOT NULL
571
- );
572
- CREATE INDEX IF NOT EXISTS idx_api_logs_at ON api_logs(called_at);
573
- CREATE INDEX IF NOT EXISTS idx_api_logs_name ON api_logs(tool_name);
574
- `);
575
- }
576
- migrateDedupStatus() {
577
- const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
578
- if (!cols.some((c) => c.name === "dedup_status")) {
579
- this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_status TEXT NOT NULL DEFAULT 'active'");
580
- this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_target TEXT DEFAULT NULL");
581
- this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_reason TEXT DEFAULT NULL");
582
- this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_status ON chunks(dedup_status)");
583
- this.log.info("Migrated: added dedup_status/dedup_target/dedup_reason columns to chunks");
584
- }
585
- }
586
- recordApiLog(toolName, input, output, durationMs, success) {
587
- const inputStr = typeof input === "string" ? input : JSON.stringify(input ?? {});
588
- this.db.prepare("INSERT INTO api_logs (tool_name, input_data, output_data, duration_ms, success, called_at) VALUES (?, ?, ?, ?, ?, ?)").run(toolName, inputStr, output, Math.round(durationMs), success ? 1 : 0, Date.now());
589
- }
590
- getApiLogs(limit = 50, offset = 0, toolFilter) {
591
- const whereClause = toolFilter ? " WHERE tool_name = ?" : "";
592
- const filterParams = toolFilter ? [toolFilter] : [];
593
- const countRow = this.db.prepare("SELECT COUNT(*) as c FROM api_logs" + whereClause).get(...filterParams);
594
- const rows = this.db.prepare("SELECT id, tool_name, input_data, output_data, duration_ms, success, called_at FROM api_logs" +
595
- whereClause + " ORDER BY called_at DESC LIMIT ? OFFSET ?").all(...filterParams, limit, offset);
596
- return {
597
- logs: rows.map((r) => ({
598
- id: r.id,
599
- toolName: r.tool_name,
600
- input: r.input_data,
601
- output: r.output_data,
602
- durationMs: r.duration_ms,
603
- success: r.success === 1,
604
- calledAt: r.called_at,
605
- })),
606
- total: countRow.c,
607
- };
608
- }
609
- getApiLogToolNames() {
610
- const rows = this.db.prepare("SELECT DISTINCT tool_name FROM api_logs ORDER BY tool_name").all();
611
- return rows.map((r) => r.tool_name);
612
- }
613
- recordMergeHit(chunkId, action, reason, oldSummary, newSummary) {
614
- const chunk = this.getChunk(chunkId);
615
- if (!chunk)
616
- return;
617
- const history = JSON.parse(chunk.mergeHistory || "[]");
618
- const entry = { at: Date.now(), action, reason };
619
- if (action === "UPDATE" && oldSummary && newSummary) {
620
- entry.from = oldSummary;
621
- entry.to = newSummary;
622
- }
623
- history.push(entry);
624
- this.db.prepare(`
625
- UPDATE chunks SET merge_count = merge_count + 1, last_hit_at = ?, merge_history = ?, updated_at = ?
626
- WHERE id = ?
627
- `).run(Date.now(), JSON.stringify(history), Date.now(), chunkId);
628
- }
629
- updateChunkSummaryAndContent(chunkId, newSummary, appendContent) {
630
- this.db.prepare(`
631
- UPDATE chunks SET summary = ?, content = content || ? || ?, updated_at = ? WHERE id = ?
632
- `).run(newSummary, "\n\n---\n\n", appendContent, Date.now(), chunkId);
633
- }
634
- migrateToolCalls() {
635
- this.db.exec(`
636
- CREATE TABLE IF NOT EXISTS tool_calls (
637
- id INTEGER PRIMARY KEY AUTOINCREMENT,
638
- tool_name TEXT NOT NULL,
639
- duration_ms INTEGER NOT NULL,
640
- success INTEGER NOT NULL DEFAULT 1,
641
- called_at INTEGER NOT NULL
642
- );
643
- CREATE INDEX IF NOT EXISTS idx_tool_calls_at ON tool_calls(called_at);
644
- CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
645
- `);
646
- }
647
- recordToolCall(toolName, durationMs, success) {
648
- this.db.prepare("INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)").run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
649
- }
650
- getToolMetrics(minutes, fromMs, toMs) {
651
- const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
652
- const until = toMs ?? Date.now();
653
- const rows = this.db.prepare(`SELECT tool_name,
654
- duration_ms,
655
- success,
656
- strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
657
- FROM tool_calls
658
- WHERE called_at >= ? AND called_at <= ?
659
- ORDER BY called_at`).all(since, until);
660
- const toolSet = new Set();
661
- const minuteMap = new Map();
662
- const aggMap = new Map();
663
- for (const r of rows) {
664
- toolSet.add(r.tool_name);
665
- if (!aggMap.has(r.tool_name))
666
- aggMap.set(r.tool_name, { durations: [], errors: 0 });
667
- const agg = aggMap.get(r.tool_name);
668
- agg.durations.push(r.duration_ms);
669
- if (!r.success)
670
- agg.errors++;
671
- if (!minuteMap.has(r.minute_key))
672
- minuteMap.set(r.minute_key, new Map());
673
- const toolMap = minuteMap.get(r.minute_key);
674
- if (!toolMap.has(r.tool_name))
675
- toolMap.set(r.tool_name, { total: 0, count: 0 });
676
- const entry = toolMap.get(r.tool_name);
677
- entry.total += r.duration_ms;
678
- entry.count++;
679
- }
680
- const tools = Array.from(toolSet).sort();
681
- const allMinutes = [];
682
- if (minutes > 0) {
683
- const startMinute = new Date(since);
684
- startMinute.setSeconds(0, 0);
685
- const now = new Date();
686
- for (let t = startMinute.getTime(); t <= now.getTime(); t += 60000) {
687
- const d = new Date(t);
688
- const pad = (n) => String(n).padStart(2, "0");
689
- allMinutes.push(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`);
690
- }
691
- }
692
- const series = allMinutes.map((m) => {
693
- const entry = { minute: m };
694
- const toolMap = minuteMap.get(m);
695
- for (const t of tools) {
696
- const data = toolMap?.get(t);
697
- entry[t] = data ? Math.round(data.total / data.count) : 0;
698
- }
699
- return entry;
700
- });
701
- const p95 = (arr) => {
702
- if (arr.length === 0)
703
- return 0;
704
- const sorted = [...arr].sort((a, b) => a - b);
705
- return sorted[Math.floor(sorted.length * 0.95)] ?? sorted[sorted.length - 1];
706
- };
707
- const aggregated = tools.map((t) => {
708
- const agg = aggMap.get(t);
709
- return {
710
- tool: t,
711
- totalCalls: agg.durations.length,
712
- avgMs: Math.round(agg.durations.reduce((s, v) => s + v, 0) / agg.durations.length),
713
- p95Ms: p95(agg.durations),
714
- errorCount: agg.errors,
715
- };
716
- });
717
- return { tools, series, aggregated };
718
- }
719
- /** Record a viewer API call for analytics (list, search, etc.). */
720
- recordViewerEvent(eventType) {
721
- this.db.prepare("INSERT INTO viewer_events (event_type, created_at) VALUES (?, ?)").run(eventType, Date.now());
722
- }
723
- /**
724
- * Return metrics for the last N days: writes per day (from chunks), viewer calls per day.
725
- */
726
- getMetrics(days) {
727
- const since = Date.now() - days * 86400 * 1000;
728
- const now = new Date();
729
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
730
- const writesRows = this.db
731
- .prepare(`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, COUNT(*) as c
732
- FROM chunks WHERE created_at >= ? GROUP BY d ORDER BY d`)
733
- .all(since);
734
- const writesPerDay = writesRows.map((r) => ({ date: r.d, count: r.c }));
735
- const eventsRows = this.db
736
- .prepare(`SELECT date(created_at/1000, 'unixepoch', 'localtime') as d, event_type, COUNT(*) as c
737
- FROM viewer_events WHERE created_at >= ? GROUP BY d, event_type ORDER BY d`)
738
- .all(since);
739
- const byDate = new Map();
740
- for (const r of eventsRows) {
741
- let row = byDate.get(r.d);
742
- if (!row) {
743
- row = { list: 0, search: 0 };
744
- byDate.set(r.d, row);
745
- }
746
- if (r.event_type === "list")
747
- row.list += r.c;
748
- else if (r.event_type === "search")
749
- row.search += r.c;
750
- }
751
- const viewerCallsPerDay = Array.from(byDate.entries())
752
- .sort((a, b) => a[0].localeCompare(b[0]))
753
- .map(([date, v]) => ({ date, list: v.list, search: v.search, total: v.list + v.search }));
754
- const totalChunks = this.db.prepare("SELECT COUNT(*) as c FROM chunks").get().c;
755
- const totalSessions = this.db.prepare("SELECT COUNT(DISTINCT session_key) as c FROM chunks").get().c;
756
- const totalEmbeddings = this.db.prepare("SELECT COUNT(*) as c FROM embeddings").get().c;
757
- const todayWrites = this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE created_at >= ?").get(todayStart).c;
758
- const todayViewerCalls = this.db.prepare("SELECT COUNT(*) as c FROM viewer_events WHERE created_at >= ?").get(todayStart).c;
759
- return {
760
- writesPerDay,
761
- viewerCallsPerDay,
762
- totals: {
763
- memories: totalChunks,
764
- sessions: totalSessions,
765
- embeddings: totalEmbeddings,
766
- todayWrites,
767
- todayViewerCalls,
768
- },
769
- };
770
- }
771
- migrateHubTables() {
772
- this.db.exec(`
773
- CREATE TABLE IF NOT EXISTS client_hub_connection (
774
- id INTEGER PRIMARY KEY CHECK (id = 1),
775
- hub_url TEXT NOT NULL,
776
- user_id TEXT NOT NULL,
777
- username TEXT NOT NULL,
778
- user_token TEXT NOT NULL,
779
- role TEXT NOT NULL,
780
- connected_at INTEGER NOT NULL
781
- );
782
-
783
- CREATE TABLE IF NOT EXISTS local_shared_tasks (
784
- task_id TEXT PRIMARY KEY,
785
- hub_task_id TEXT NOT NULL,
786
- visibility TEXT NOT NULL DEFAULT 'public',
787
- group_id TEXT,
788
- synced_chunks INTEGER NOT NULL DEFAULT 0,
789
- hub_instance_id TEXT NOT NULL DEFAULT '',
790
- shared_at INTEGER NOT NULL
791
- );
792
-
793
- CREATE TABLE IF NOT EXISTS local_shared_memories (
794
- chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
795
- original_owner TEXT NOT NULL,
796
- shared_at INTEGER NOT NULL
797
- );
798
-
799
- -- Client: team share UI metadata only (no hub_memories row — avoids local FTS/embed recall duplication)
800
- CREATE TABLE IF NOT EXISTS team_shared_chunks (
801
- chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
802
- hub_memory_id TEXT NOT NULL DEFAULT '',
803
- visibility TEXT NOT NULL DEFAULT 'public',
804
- group_id TEXT,
805
- hub_instance_id TEXT NOT NULL DEFAULT '',
806
- shared_at INTEGER NOT NULL
807
- );
808
-
809
- CREATE TABLE IF NOT EXISTS team_shared_skills (
810
- skill_id TEXT PRIMARY KEY,
811
- hub_skill_id TEXT NOT NULL DEFAULT '',
812
- visibility TEXT NOT NULL DEFAULT 'public',
813
- group_id TEXT,
814
- hub_instance_id TEXT NOT NULL DEFAULT '',
815
- shared_at INTEGER NOT NULL
816
- );
817
-
818
- CREATE TABLE IF NOT EXISTS hub_users (
819
- id TEXT PRIMARY KEY,
820
- username TEXT NOT NULL UNIQUE,
821
- device_name TEXT NOT NULL DEFAULT '',
822
- role TEXT NOT NULL,
823
- status TEXT NOT NULL,
824
- token_hash TEXT NOT NULL DEFAULT '',
825
- created_at INTEGER NOT NULL,
826
- approved_at INTEGER,
827
- last_ip TEXT NOT NULL DEFAULT '',
828
- last_active_at INTEGER
829
- );
830
- CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
831
- CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
832
-
833
- CREATE TABLE IF NOT EXISTS hub_groups (
834
- id TEXT PRIMARY KEY,
835
- name TEXT NOT NULL,
836
- description TEXT NOT NULL DEFAULT '',
837
- created_at INTEGER NOT NULL
838
- );
839
-
840
- CREATE TABLE IF NOT EXISTS hub_group_members (
841
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
842
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
843
- joined_at INTEGER NOT NULL,
844
- PRIMARY KEY (group_id, user_id)
845
- );
846
-
847
- CREATE TABLE IF NOT EXISTS hub_tasks (
848
- id TEXT PRIMARY KEY,
849
- source_task_id TEXT NOT NULL,
850
- source_user_id TEXT NOT NULL,
851
- title TEXT NOT NULL,
852
- summary TEXT NOT NULL DEFAULT '',
853
- group_id TEXT,
854
- visibility TEXT NOT NULL,
855
- created_at INTEGER NOT NULL,
856
- updated_at INTEGER NOT NULL,
857
- UNIQUE(source_user_id, source_task_id)
858
- );
859
- CREATE INDEX IF NOT EXISTS idx_hub_tasks_visibility ON hub_tasks(visibility);
860
- CREATE INDEX IF NOT EXISTS idx_hub_tasks_group ON hub_tasks(group_id);
861
-
862
- CREATE TABLE IF NOT EXISTS hub_chunks (
863
- id TEXT PRIMARY KEY,
864
- hub_task_id TEXT NOT NULL REFERENCES hub_tasks(id) ON DELETE CASCADE,
865
- source_chunk_id TEXT NOT NULL,
866
- source_user_id TEXT NOT NULL,
867
- role TEXT NOT NULL,
868
- content TEXT NOT NULL,
869
- summary TEXT NOT NULL DEFAULT '',
870
- kind TEXT NOT NULL DEFAULT 'paragraph',
871
- created_at INTEGER NOT NULL,
872
- UNIQUE(source_user_id, source_chunk_id)
873
- );
874
- CREATE INDEX IF NOT EXISTS idx_hub_chunks_task ON hub_chunks(hub_task_id);
875
-
876
- CREATE TABLE IF NOT EXISTS hub_embeddings (
877
- chunk_id TEXT PRIMARY KEY REFERENCES hub_chunks(id) ON DELETE CASCADE,
878
- vector BLOB NOT NULL,
879
- dimensions INTEGER NOT NULL,
880
- updated_at INTEGER NOT NULL
881
- );
882
-
883
- CREATE VIRTUAL TABLE IF NOT EXISTS hub_chunks_fts USING fts5(
884
- summary,
885
- content,
886
- content='hub_chunks',
887
- content_rowid='rowid',
888
- tokenize='trigram'
889
- );
890
-
891
- CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
892
- INSERT INTO hub_chunks_fts(rowid, summary, content)
893
- VALUES (new.rowid, new.summary, new.content);
894
- END;
895
-
896
- CREATE TRIGGER IF NOT EXISTS hub_chunks_ad AFTER DELETE ON hub_chunks BEGIN
897
- INSERT INTO hub_chunks_fts(hub_chunks_fts, rowid, summary, content)
898
- VALUES ('delete', old.rowid, old.summary, old.content);
899
- END;
900
-
901
- CREATE TRIGGER IF NOT EXISTS hub_chunks_au AFTER UPDATE ON hub_chunks BEGIN
902
- INSERT INTO hub_chunks_fts(hub_chunks_fts, rowid, summary, content)
903
- VALUES ('delete', old.rowid, old.summary, old.content);
904
- INSERT INTO hub_chunks_fts(rowid, summary, content)
905
- VALUES (new.rowid, new.summary, new.content);
906
- END;
907
-
908
- CREATE TABLE IF NOT EXISTS hub_skills (
909
- id TEXT PRIMARY KEY,
910
- source_skill_id TEXT NOT NULL,
911
- source_user_id TEXT NOT NULL,
912
- name TEXT NOT NULL,
913
- description TEXT NOT NULL DEFAULT '',
914
- version INTEGER NOT NULL,
915
- group_id TEXT,
916
- visibility TEXT NOT NULL,
917
- bundle TEXT NOT NULL,
918
- quality_score REAL,
919
- created_at INTEGER NOT NULL,
920
- updated_at INTEGER NOT NULL,
921
- UNIQUE(source_user_id, source_skill_id)
922
- );
923
- CREATE INDEX IF NOT EXISTS idx_hub_skills_visibility ON hub_skills(visibility);
924
- CREATE INDEX IF NOT EXISTS idx_hub_skills_group ON hub_skills(group_id);
925
-
926
- CREATE TABLE IF NOT EXISTS hub_skill_embeddings (
927
- skill_id TEXT PRIMARY KEY REFERENCES hub_skills(id) ON DELETE CASCADE,
928
- vector BLOB NOT NULL,
929
- dimensions INTEGER NOT NULL,
930
- updated_at INTEGER NOT NULL
931
- );
932
-
933
- CREATE VIRTUAL TABLE IF NOT EXISTS hub_skills_fts USING fts5(
934
- name,
935
- description,
936
- content='hub_skills',
937
- content_rowid='rowid',
938
- tokenize='trigram'
939
- );
940
-
941
- CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
942
- INSERT INTO hub_skills_fts(rowid, name, description)
943
- VALUES (new.rowid, new.name, new.description);
944
- END;
945
-
946
- CREATE TRIGGER IF NOT EXISTS hub_skills_ad AFTER DELETE ON hub_skills BEGIN
947
- INSERT INTO hub_skills_fts(hub_skills_fts, rowid, name, description)
948
- VALUES ('delete', old.rowid, old.name, old.description);
949
- END;
950
-
951
- CREATE TRIGGER IF NOT EXISTS hub_skills_au AFTER UPDATE ON hub_skills BEGIN
952
- INSERT INTO hub_skills_fts(hub_skills_fts, rowid, name, description)
953
- VALUES ('delete', old.rowid, old.name, old.description);
954
- INSERT INTO hub_skills_fts(rowid, name, description)
955
- VALUES (new.rowid, new.name, new.description);
956
- END;
957
-
958
- -- Independent shared memories (not tied to a task)
959
- CREATE TABLE IF NOT EXISTS hub_memories (
960
- id TEXT PRIMARY KEY,
961
- source_chunk_id TEXT NOT NULL,
962
- source_user_id TEXT NOT NULL,
963
- role TEXT NOT NULL,
964
- content TEXT NOT NULL,
965
- summary TEXT NOT NULL DEFAULT '',
966
- kind TEXT NOT NULL DEFAULT 'paragraph',
967
- group_id TEXT,
968
- visibility TEXT NOT NULL,
969
- created_at INTEGER NOT NULL,
970
- updated_at INTEGER NOT NULL,
971
- UNIQUE(source_user_id, source_chunk_id)
972
- );
973
- CREATE INDEX IF NOT EXISTS idx_hub_memories_visibility ON hub_memories(visibility);
974
- CREATE INDEX IF NOT EXISTS idx_hub_memories_group ON hub_memories(group_id);
975
-
976
- CREATE TABLE IF NOT EXISTS hub_memory_embeddings (
977
- memory_id TEXT PRIMARY KEY REFERENCES hub_memories(id) ON DELETE CASCADE,
978
- vector BLOB NOT NULL,
979
- dimensions INTEGER NOT NULL,
980
- updated_at INTEGER NOT NULL
981
- );
982
-
983
- CREATE VIRTUAL TABLE IF NOT EXISTS hub_memories_fts USING fts5(
984
- summary,
985
- content,
986
- content='hub_memories',
987
- content_rowid='rowid',
988
- tokenize='trigram'
989
- );
990
-
991
- CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
992
- INSERT INTO hub_memories_fts(rowid, summary, content)
993
- VALUES (new.rowid, new.summary, new.content);
994
- END;
995
-
996
- CREATE TRIGGER IF NOT EXISTS hub_memories_ad AFTER DELETE ON hub_memories BEGIN
997
- INSERT INTO hub_memories_fts(hub_memories_fts, rowid, summary, content)
998
- VALUES ('delete', old.rowid, old.summary, old.content);
999
- END;
1000
-
1001
- CREATE TRIGGER IF NOT EXISTS hub_memories_au AFTER UPDATE ON hub_memories BEGIN
1002
- INSERT INTO hub_memories_fts(hub_memories_fts, rowid, summary, content)
1003
- VALUES ('delete', old.rowid, old.summary, old.content);
1004
- INSERT INTO hub_memories_fts(rowid, summary, content)
1005
- VALUES (new.rowid, new.summary, new.content);
1006
- END;
1007
- `);
1008
- this.db.exec(`
1009
- CREATE TABLE IF NOT EXISTS hub_notifications (
1010
- id TEXT PRIMARY KEY,
1011
- user_id TEXT NOT NULL,
1012
- type TEXT NOT NULL,
1013
- resource TEXT NOT NULL,
1014
- title TEXT NOT NULL,
1015
- message TEXT NOT NULL DEFAULT '',
1016
- read INTEGER NOT NULL DEFAULT 0,
1017
- created_at INTEGER NOT NULL
1018
- );
1019
- CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
1020
- `);
1021
- try {
1022
- const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
1023
- if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
1024
- this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
1025
- this.log.info("Migrated: added last_ip column to hub_users");
1026
- }
1027
- if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
1028
- this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
1029
- this.log.info("Migrated: added last_active_at column to hub_users");
1030
- }
1031
- }
1032
- catch { /* table may not exist yet */ }
1033
- }
1034
- // ─── Write ───
1035
- insertChunk(chunk) {
1036
- const stmt = this.db.prepare(`
1037
- 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)
1038
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1039
- `);
1040
- stmt.run(chunk.id, chunk.sessionKey, chunk.turnId, chunk.seq, chunk.role, chunk.content, chunk.kind, chunk.summary, chunk.taskId, contentHash(chunk.content), chunk.owner ?? "agent:main", chunk.dedupStatus ?? "active", chunk.dedupTarget ?? null, chunk.dedupReason ?? null, chunk.createdAt, chunk.updatedAt);
1041
- }
1042
- markDedupStatus(chunkId, status, targetChunkId, reason) {
1043
- this.db.prepare("UPDATE chunks SET dedup_status = ?, dedup_target = ?, dedup_reason = ?, updated_at = ? WHERE id = ?").run(status, targetChunkId, reason, Date.now(), chunkId);
1044
- }
1045
- updateSummary(chunkId, summary) {
1046
- this.db.prepare("UPDATE chunks SET summary = ?, updated_at = ? WHERE id = ?").run(summary, Date.now(), chunkId);
1047
- }
1048
- upsertEmbedding(chunkId, vector) {
1049
- const buf = Buffer.from(new Float32Array(vector).buffer);
1050
- this.db.prepare(`
1051
- INSERT OR REPLACE INTO embeddings (chunk_id, vector, dimensions, updated_at)
1052
- VALUES (?, ?, ?, ?)
1053
- `).run(chunkId, buf, vector.length, Date.now());
1054
- }
1055
- deleteEmbedding(chunkId) {
1056
- this.db.prepare("DELETE FROM embeddings WHERE chunk_id = ?").run(chunkId);
1057
- }
1058
- // ─── Read ───
1059
- getChunk(chunkId) {
1060
- const row = this.db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1061
- return row ? rowToChunk(row) : null;
1062
- }
1063
- getChunkForOwners(chunkId, ownerFilter) {
1064
- if (!ownerFilter || ownerFilter.length === 0)
1065
- return this.getChunk(chunkId);
1066
- const placeholders = ownerFilter.map(() => "?").join(",");
1067
- const row = this.db.prepare(`SELECT * FROM chunks WHERE id = ? AND owner IN (${placeholders}) LIMIT 1`).get(chunkId, ...ownerFilter);
1068
- return row ? rowToChunk(row) : null;
1069
- }
1070
- getChunksByRef(ref, ownerFilter) {
1071
- return this.getChunkForOwners(ref.chunkId, ownerFilter);
1072
- }
1073
- getNeighborChunks(sessionKey, turnId, seq, window, ownerFilter) {
1074
- let sql = `
1075
- SELECT * FROM chunks
1076
- WHERE session_key = ?`;
1077
- const params = [sessionKey];
1078
- if (ownerFilter && ownerFilter.length > 0) {
1079
- const placeholders = ownerFilter.map(() => "?").join(",");
1080
- sql += ` AND owner IN (${placeholders})`;
1081
- params.push(...ownerFilter);
1082
- }
1083
- sql += `
1084
- ORDER BY created_at, seq
1085
- `;
1086
- const allRows = this.db.prepare(sql).all(...params);
1087
- const targetIdx = allRows.findIndex((r) => r.turn_id === turnId && r.seq === seq);
1088
- if (targetIdx === -1)
1089
- return [];
1090
- const radius = window * 3;
1091
- const start = Math.max(0, targetIdx - radius);
1092
- const end = Math.min(allRows.length, targetIdx + radius + 1);
1093
- return allRows.slice(start, end).map(rowToChunk);
1094
- }
1095
- // ─── FTS Search ───
1096
- ftsSearch(query, limit, ownerFilter) {
1097
- const sanitized = sanitizeFtsQuery(query);
1098
- if (!sanitized)
1099
- return [];
1100
- try {
1101
- let sql = `
1102
- SELECT c.id as chunk_id, rank
1103
- FROM chunks_fts f
1104
- JOIN chunks c ON c.rowid = f.rowid
1105
- WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'`;
1106
- const params = [sanitized];
1107
- if (ownerFilter && ownerFilter.length > 0) {
1108
- const placeholders = ownerFilter.map(() => "?").join(",");
1109
- sql += ` AND c.owner IN (${placeholders})`;
1110
- params.push(...ownerFilter);
1111
- }
1112
- sql += ` ORDER BY rank LIMIT ?`;
1113
- params.push(limit);
1114
- const rows = this.db.prepare(sql).all(...params);
1115
- if (rows.length === 0)
1116
- return [];
1117
- const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
1118
- return rows.map((r) => ({
1119
- chunkId: r.chunk_id,
1120
- score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
1121
- }));
1122
- }
1123
- catch {
1124
- this.log.warn(`FTS query failed for: "${sanitized}", returning empty`);
1125
- return [];
1126
- }
1127
- }
1128
- // ─── Pattern Search (LIKE-based, for CJK text where FTS tokenization is weak) ───
1129
- patternSearch(patterns, opts = {}) {
1130
- if (patterns.length === 0)
1131
- return [];
1132
- const limit = opts.limit ?? 10;
1133
- const conditions = patterns.map(() => "c.content LIKE ?");
1134
- const whereClause = conditions.join(" OR ");
1135
- const roleClause = opts.role ? " AND c.role = ?" : "";
1136
- const params = patterns.map(p => `%${p}%`);
1137
- if (opts.role)
1138
- params.push(opts.role);
1139
- params.push(limit);
1140
- try {
1141
- const rows = this.db.prepare(`
1142
- SELECT c.id as chunk_id, c.content, c.role, c.created_at
1143
- FROM chunks c
1144
- WHERE (${whereClause})${roleClause} AND c.dedup_status = 'active'
1145
- ORDER BY c.created_at DESC
1146
- LIMIT ?
1147
- `).all(...params);
1148
- return rows.map(r => ({
1149
- chunkId: r.chunk_id,
1150
- content: r.content,
1151
- role: r.role,
1152
- createdAt: r.created_at,
1153
- }));
1154
- }
1155
- catch {
1156
- return [];
1157
- }
1158
- }
1159
- hubMemoryPatternSearch(patterns, opts = {}) {
1160
- if (patterns.length === 0)
1161
- return [];
1162
- const limit = opts.limit ?? 10;
1163
- const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1164
- const params = [];
1165
- for (const p of patterns) {
1166
- params.push(`%${p}%`, `%${p}%`);
1167
- }
1168
- params.push(limit);
1169
- try {
1170
- const rows = this.db.prepare(`
1171
- SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1172
- FROM hub_memories hm
1173
- WHERE ${conditions.join(" OR ")}
1174
- ORDER BY hm.created_at DESC
1175
- LIMIT ?
1176
- `).all(...params);
1177
- return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1178
- }
1179
- catch {
1180
- return [];
1181
- }
1182
- }
1183
- listHubMemories(opts = {}) {
1184
- const limit = opts.limit ?? 200;
1185
- try {
1186
- return this.db.prepare("SELECT id, summary, content FROM hub_memories ORDER BY created_at DESC LIMIT ?").all(limit);
1187
- }
1188
- catch {
1189
- return [];
1190
- }
1191
- }
1192
- // ─── Vector Search ───
1193
- getAllEmbeddings(ownerFilter) {
1194
- let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
1195
- JOIN chunks c ON c.id = e.chunk_id
1196
- WHERE c.dedup_status = 'active'`;
1197
- const params = [];
1198
- if (ownerFilter && ownerFilter.length > 0) {
1199
- const placeholders = ownerFilter.map(() => "?").join(",");
1200
- sql += ` AND c.owner IN (${placeholders})`;
1201
- params.push(...ownerFilter);
1202
- }
1203
- const rows = this.db.prepare(sql).all(...params);
1204
- return rows.map((r) => ({
1205
- chunkId: r.chunk_id,
1206
- vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
1207
- }));
1208
- }
1209
- getRecentEmbeddings(limit, ownerFilter) {
1210
- if (limit <= 0)
1211
- return this.getAllEmbeddings(ownerFilter);
1212
- let sql = `SELECT e.chunk_id, e.vector, e.dimensions
1213
- FROM chunks c
1214
- JOIN embeddings e ON e.chunk_id = c.id
1215
- WHERE c.dedup_status = 'active'`;
1216
- const params = [];
1217
- if (ownerFilter && ownerFilter.length > 0) {
1218
- const placeholders = ownerFilter.map(() => "?").join(",");
1219
- sql += ` AND c.owner IN (${placeholders})`;
1220
- params.push(...ownerFilter);
1221
- }
1222
- sql += ` ORDER BY c.created_at DESC LIMIT ?`;
1223
- params.push(limit);
1224
- const rows = this.db.prepare(sql).all(...params);
1225
- return rows.map((r) => ({
1226
- chunkId: r.chunk_id,
1227
- vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
1228
- }));
1229
- }
1230
- getEmbedding(chunkId) {
1231
- const row = this.db.prepare("SELECT vector, dimensions FROM embeddings WHERE chunk_id = ?").get(chunkId);
1232
- if (!row)
1233
- return null;
1234
- return Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions));
1235
- }
1236
- // ─── Update ───
1237
- updateChunk(chunkId, fields) {
1238
- const sets = [];
1239
- const params = [];
1240
- if (fields.summary !== undefined) {
1241
- sets.push("summary = ?");
1242
- params.push(fields.summary);
1243
- }
1244
- if (fields.content !== undefined) {
1245
- sets.push("content = ?");
1246
- params.push(fields.content);
1247
- }
1248
- if (fields.role !== undefined) {
1249
- sets.push("role = ?");
1250
- params.push(fields.role);
1251
- }
1252
- if (fields.kind !== undefined) {
1253
- sets.push("kind = ?");
1254
- params.push(fields.kind);
1255
- }
1256
- if (fields.owner !== undefined) {
1257
- sets.push("owner = ?");
1258
- params.push(fields.owner);
1259
- }
1260
- if (sets.length === 0)
1261
- return false;
1262
- sets.push("updated_at = ?");
1263
- params.push(Date.now());
1264
- params.push(chunkId);
1265
- const result = this.db.prepare(`UPDATE chunks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1266
- return result.changes > 0;
1267
- }
1268
- /**
1269
- * Find user-role chunks that contain system-injected content that should
1270
- * have been stripped before storage. Returns chunk IDs and a preview.
1271
- */
1272
- findPollutedUserChunks() {
1273
- const results = [];
1274
- const patterns = [
1275
- { sql: "content LIKE '%<memory_context>%'", reason: "memory_context injection" },
1276
- { sql: "content LIKE '%=== MemOS LONG-TERM MEMORY%'", reason: "MemOS legacy injection" },
1277
- { sql: "content LIKE '%[MemOS Auto-Recall]%'", reason: "MemOS Auto-Recall injection" },
1278
- { sql: "content LIKE '%## Memory system%No memories were automatically recalled%'", reason: "Memory system no-recall hint" },
1279
- { sql: "content LIKE '%## Retrieved memories from past conversations%CRITICAL INSTRUCTION%'", reason: "prependContext recall injection" },
1280
- { sql: "content LIKE '%VERIFIED facts the user previously shared%'", reason: "VERIFIED facts injection" },
1281
- { sql: "content LIKE '%<memos_system_instruction>%'", reason: "memos_system_instruction injection" },
1282
- { sql: "content LIKE '%📝 Related memories:%'", reason: "Related memories injection" },
1283
- ];
1284
- for (const { sql, reason } of patterns) {
1285
- const rows = this.db.prepare(`SELECT id, substr(content, 1, 120) AS preview FROM chunks WHERE role = 'user' AND ${sql}`).all();
1286
- for (const row of rows) {
1287
- results.push({ id: row.id, preview: row.preview, reason });
1288
- }
1289
- }
1290
- return results;
1291
- }
1292
- /**
1293
- * Find user chunks where user+assistant content was mixed together
1294
- * (separated by \n\n---\n), and truncate to keep only the user's part.
1295
- */
1296
- fixMixedUserChunks() {
1297
- const rows = this.db.prepare(`SELECT id, content FROM chunks WHERE role = 'user'
1298
- AND content LIKE '%' || char(10) || char(10) || '---' || char(10) || '%'
1299
- AND length(content) > 300`).all();
1300
- let fixed = 0;
1301
- for (const { id, content } of rows) {
1302
- const dashIdx = content.indexOf("\n\n---\n");
1303
- if (dashIdx > 5) {
1304
- const userPart = content.slice(0, dashIdx).trim();
1305
- if (userPart.length >= 5 && userPart.length < content.length) {
1306
- this.db.prepare("UPDATE chunks SET content = ?, updated_at = ? WHERE id = ?")
1307
- .run(userPart, Date.now(), id);
1308
- fixed++;
1309
- }
1310
- }
1311
- }
1312
- return fixed;
1313
- }
1314
- // ─── Delete ───
1315
- deleteChunk(chunkId) {
1316
- const result = this.db.prepare("DELETE FROM chunks WHERE id = ?").run(chunkId);
1317
- return result.changes > 0;
1318
- }
1319
- deleteSession(sessionKey) {
1320
- const result = this.db.prepare("DELETE FROM chunks WHERE session_key = ?").run(sessionKey);
1321
- return result.changes;
1322
- }
1323
- deleteAll() {
1324
- this.db.exec("PRAGMA foreign_keys = OFF");
1325
- const tables = [
1326
- "task_skills",
1327
- "skill_embeddings",
1328
- "skill_versions",
1329
- "skills",
1330
- "local_shared_memories",
1331
- "team_shared_chunks",
1332
- "team_shared_skills",
1333
- "local_shared_tasks",
1334
- "embeddings",
1335
- "chunks",
1336
- "tasks",
1337
- "viewer_events",
1338
- "api_logs",
1339
- "tool_calls",
1340
- ];
1341
- for (const table of tables) {
1342
- try {
1343
- this.db.prepare(`DELETE FROM ${table}`).run();
1344
- }
1345
- catch (err) {
1346
- this.log.warn(`deleteAll: failed to clear ${table}: ${err}`);
1347
- }
1348
- }
1349
- this.db.exec("PRAGMA foreign_keys = ON");
1350
- const remaining = this.countChunks();
1351
- return remaining === 0 ? 1 : 0;
1352
- }
1353
- deleteTask(taskId) {
1354
- this.db.prepare("DELETE FROM task_skills WHERE task_id = ?").run(taskId);
1355
- this.db.prepare("UPDATE chunks SET task_id = NULL WHERE task_id = ?").run(taskId);
1356
- const result = this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
1357
- return result.changes > 0;
1358
- }
1359
- deleteSkill(skillId) {
1360
- this.db.prepare("DELETE FROM task_skills WHERE skill_id = ?").run(skillId);
1361
- this.db.prepare("DELETE FROM skill_versions WHERE skill_id = ?").run(skillId);
1362
- this.db.prepare("DELETE FROM skill_embeddings WHERE skill_id = ?").run(skillId);
1363
- this.db.prepare("UPDATE chunks SET skill_id = NULL WHERE skill_id = ?").run(skillId);
1364
- const result = this.db.prepare("DELETE FROM skills WHERE id = ?").run(skillId);
1365
- return result.changes > 0;
1366
- }
1367
- // ─── Task CRUD ───
1368
- insertTask(task) {
1369
- this.db.prepare(`
1370
- INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, owner, started_at, ended_at, updated_at)
1371
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1372
- `).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.owner ?? "agent:main", task.startedAt, task.endedAt, task.updatedAt);
1373
- }
1374
- getTask(taskId) {
1375
- const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId);
1376
- return row ? rowToTask(row) : null;
1377
- }
1378
- getActiveTask(sessionKey, owner) {
1379
- if (owner) {
1380
- const row = this.db.prepare("SELECT * FROM tasks WHERE session_key = ? AND status = 'active' AND owner = ? ORDER BY started_at DESC LIMIT 1").get(sessionKey, owner);
1381
- return row ? rowToTask(row) : null;
1382
- }
1383
- const row = this.db.prepare("SELECT * FROM tasks WHERE session_key = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1").get(sessionKey);
1384
- return row ? rowToTask(row) : null;
1385
- }
1386
- hasTaskForSession(sessionKey) {
1387
- const row = this.db.prepare("SELECT 1 FROM tasks WHERE session_key = ? LIMIT 1").get(sessionKey);
1388
- return !!row;
1389
- }
1390
- hasSkillForSessionTask(sessionKey) {
1391
- const row = this.db.prepare("SELECT 1 FROM task_skills ts JOIN tasks t ON ts.task_id = t.id WHERE t.session_key = ? LIMIT 1").get(sessionKey);
1392
- return !!row;
1393
- }
1394
- getCompletedTasksForSession(sessionKey) {
1395
- const rows = this.db.prepare("SELECT * FROM tasks WHERE session_key = ? AND status = 'completed'").all(sessionKey);
1396
- return rows.map(rowToTask);
1397
- }
1398
- getAllActiveTasks(owner) {
1399
- if (owner) {
1400
- const rows = this.db.prepare("SELECT * FROM tasks WHERE status = 'active' AND owner = ? ORDER BY started_at DESC").all(owner);
1401
- return rows.map(rowToTask);
1402
- }
1403
- const rows = this.db.prepare("SELECT * FROM tasks WHERE status = 'active' ORDER BY started_at DESC").all();
1404
- return rows.map(rowToTask);
1405
- }
1406
- updateTask(taskId, fields) {
1407
- const sets = [];
1408
- const params = [];
1409
- if (fields.title !== undefined) {
1410
- sets.push("title = ?");
1411
- params.push(fields.title);
1412
- }
1413
- if (fields.summary !== undefined) {
1414
- sets.push("summary = ?");
1415
- params.push(fields.summary);
1416
- }
1417
- if (fields.status !== undefined) {
1418
- sets.push("status = ?");
1419
- params.push(fields.status);
1420
- }
1421
- if (fields.endedAt !== undefined) {
1422
- sets.push("ended_at = ?");
1423
- params.push(fields.endedAt);
1424
- }
1425
- if (sets.length === 0)
1426
- return false;
1427
- sets.push("updated_at = ?");
1428
- params.push(Date.now());
1429
- params.push(taskId);
1430
- const result = this.db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1431
- return result.changes > 0;
1432
- }
1433
- getChunksByTask(taskId) {
1434
- const rows = this.db.prepare("SELECT * FROM chunks WHERE task_id = ? ORDER BY created_at, seq").all(taskId);
1435
- return rows.map(rowToChunk);
1436
- }
1437
- listTasks(opts = {}) {
1438
- const conditions = [];
1439
- const params = [];
1440
- if (opts.status) {
1441
- conditions.push("status = ?");
1442
- params.push(opts.status);
1443
- }
1444
- if (opts.owner) {
1445
- conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
1446
- params.push(opts.owner, opts.owner);
1447
- }
1448
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1449
- const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params);
1450
- const total = countRow.c;
1451
- const limit = opts.limit ?? 50;
1452
- const offset = opts.offset ?? 0;
1453
- const rows = this.db.prepare(`SELECT * FROM tasks ${whereClause} ORDER BY started_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
1454
- return { tasks: rows.map(rowToTask), total };
1455
- }
1456
- countChunksByTask(taskId) {
1457
- const row = this.db.prepare("SELECT COUNT(*) as c FROM chunks WHERE task_id = ?").get(taskId);
1458
- return row.c;
1459
- }
1460
- setChunkTaskId(chunkId, taskId) {
1461
- this.db.prepare("UPDATE chunks SET task_id = ?, updated_at = ? WHERE id = ?").run(taskId, Date.now(), chunkId);
1462
- }
1463
- getUnassignedChunks(sessionKey, owner) {
1464
- if (owner) {
1465
- const rows = this.db.prepare("SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL AND owner = ? ORDER BY created_at, seq").all(sessionKey, owner);
1466
- return rows.map(rowToChunk);
1467
- }
1468
- const rows = this.db.prepare("SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL ORDER BY created_at, seq").all(sessionKey);
1469
- return rows.map(rowToChunk);
1470
- }
1471
- /**
1472
- * Check if a chunk with the same (session_key, role, content_hash) already exists.
1473
- * Uses indexed content_hash for O(1) lookup to prevent duplicate ingestion
1474
- * when agent_end sends the full conversation history every turn.
1475
- */
1476
- chunkExistsByContent(sessionKey, role, content) {
1477
- const hash = contentHash(content);
1478
- const row = this.db.prepare("SELECT 1 FROM chunks WHERE session_key = ? AND role = ? AND content_hash = ? LIMIT 1").get(sessionKey, role, hash);
1479
- return !!row;
1480
- }
1481
- /**
1482
- * Find an active chunk with the same content_hash within the same owner (agent dimension).
1483
- * Returns the existing chunk ID if found, null otherwise.
1484
- */
1485
- findActiveChunkByHash(content, owner) {
1486
- const hash = contentHash(content);
1487
- // Check ANY existing chunk with the same hash (regardless of dedup_status)
1488
- // to prevent re-creating duplicates when all prior copies have been marked duplicate/merged.
1489
- if (owner) {
1490
- const row = this.db.prepare("SELECT id FROM chunks WHERE content_hash = ? AND owner = ? ORDER BY CASE dedup_status WHEN 'active' THEN 0 ELSE 1 END LIMIT 1").get(hash, owner);
1491
- return row?.id ?? null;
1492
- }
1493
- const row = this.db.prepare("SELECT id FROM chunks WHERE content_hash = ? ORDER BY CASE dedup_status WHEN 'active' THEN 0 ELSE 1 END LIMIT 1").get(hash);
1494
- return row?.id ?? null;
1495
- }
1496
- // ─── Util ───
1497
- getRecentChunkIds(limit) {
1498
- const rows = this.db.prepare("SELECT id FROM chunks ORDER BY created_at DESC LIMIT ?").all(limit);
1499
- return rows.map((r) => r.id);
1500
- }
1501
- countChunks() {
1502
- const row = this.db.prepare("SELECT COUNT(*) AS cnt FROM chunks").get();
1503
- return row.cnt;
1504
- }
1505
- // ─── Skill CRUD ───
1506
- insertSkill(skill) {
1507
- this.db.prepare(`
1508
- INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, owner, visibility, quality_score, created_at, updated_at)
1509
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1510
- `).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);
1511
- }
1512
- getSkill(skillId) {
1513
- const row = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(skillId);
1514
- return row ? rowToSkill(row) : null;
1515
- }
1516
- getSkillByName(name) {
1517
- const row = this.db.prepare("SELECT * FROM skills WHERE name = ?").get(name);
1518
- return row ? rowToSkill(row) : null;
1519
- }
1520
- updateSkill(skillId, fields) {
1521
- const sets = [];
1522
- const params = [];
1523
- if (fields.description !== undefined) {
1524
- sets.push("description = ?");
1525
- params.push(fields.description);
1526
- }
1527
- if (fields.version !== undefined) {
1528
- sets.push("version = ?");
1529
- params.push(fields.version);
1530
- }
1531
- if (fields.status !== undefined) {
1532
- sets.push("status = ?");
1533
- params.push(fields.status);
1534
- }
1535
- if (fields.installed !== undefined) {
1536
- sets.push("installed = ?");
1537
- params.push(fields.installed);
1538
- }
1539
- if (fields.qualityScore !== undefined) {
1540
- sets.push("quality_score = ?");
1541
- params.push(fields.qualityScore);
1542
- }
1543
- if (sets.length === 0)
1544
- return;
1545
- sets.push("updated_at = ?");
1546
- params.push(fields.updatedAt ?? Date.now());
1547
- params.push(skillId);
1548
- this.db.prepare(`UPDATE skills SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1549
- }
1550
- listSkills(opts = {}) {
1551
- const cond = opts.status ? "WHERE status = ?" : "";
1552
- const params = opts.status ? [opts.status] : [];
1553
- const rows = this.db.prepare(`SELECT * FROM skills ${cond} ORDER BY updated_at DESC`).all(...params);
1554
- return rows.map(rowToSkill);
1555
- }
1556
- // ─── Skill Visibility & Embeddings ───
1557
- setSkillVisibility(skillId, visibility) {
1558
- this.db.prepare("UPDATE skills SET visibility = ?, updated_at = ? WHERE id = ?")
1559
- .run(visibility, Date.now(), skillId);
1560
- }
1561
- upsertSkillEmbedding(skillId, vector) {
1562
- const buf = Buffer.from(new Float32Array(vector).buffer);
1563
- this.db.prepare(`
1564
- INSERT OR REPLACE INTO skill_embeddings (skill_id, vector, dimensions, updated_at)
1565
- VALUES (?, ?, ?, ?)
1566
- `).run(skillId, buf, vector.length, Date.now());
1567
- }
1568
- getSkillEmbedding(skillId) {
1569
- const row = this.db.prepare("SELECT vector, dimensions FROM skill_embeddings WHERE skill_id = ?").get(skillId);
1570
- if (!row)
1571
- return null;
1572
- return Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions));
1573
- }
1574
- getSkillEmbeddings(scope, currentOwner) {
1575
- let sql = `SELECT se.skill_id, se.vector, se.dimensions
1576
- FROM skill_embeddings se
1577
- JOIN skills s ON s.id = se.skill_id
1578
- WHERE s.status = 'active'`;
1579
- const params = [];
1580
- if (scope === "self") {
1581
- sql += ` AND s.owner = ?`;
1582
- params.push(currentOwner);
1583
- }
1584
- else if (scope === "public") {
1585
- sql += ` AND s.visibility = 'public'`;
1586
- }
1587
- else {
1588
- sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
1589
- params.push(currentOwner);
1590
- }
1591
- const rows = this.db.prepare(sql).all(...params);
1592
- return rows.map((r) => ({
1593
- skillId: r.skill_id,
1594
- vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
1595
- }));
1596
- }
1597
- skillFtsSearch(query, limit, scope, currentOwner) {
1598
- const sanitized = sanitizeFtsQuery(query);
1599
- if (!sanitized)
1600
- return [];
1601
- try {
1602
- let sql = `
1603
- SELECT s.id as skill_id, rank
1604
- FROM skills_fts f
1605
- JOIN skills s ON s.rowid = f.rowid
1606
- WHERE skills_fts MATCH ? AND s.status = 'active'`;
1607
- const params = [sanitized];
1608
- if (scope === "self") {
1609
- sql += ` AND s.owner = ?`;
1610
- params.push(currentOwner);
1611
- }
1612
- else if (scope === "public") {
1613
- sql += ` AND s.visibility = 'public'`;
1614
- }
1615
- else {
1616
- sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
1617
- params.push(currentOwner);
1618
- }
1619
- sql += ` ORDER BY rank LIMIT ?`;
1620
- params.push(limit);
1621
- const rows = this.db.prepare(sql).all(...params);
1622
- if (rows.length === 0)
1623
- return [];
1624
- const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
1625
- return rows.map((r) => ({
1626
- skillId: r.skill_id,
1627
- score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
1628
- }));
1629
- }
1630
- catch {
1631
- this.log.warn(`Skill FTS query failed for: "${sanitized}", returning empty`);
1632
- return [];
1633
- }
1634
- }
1635
- listPublicSkills() {
1636
- const rows = this.db.prepare("SELECT * FROM skills WHERE visibility = 'public' AND status = 'active' ORDER BY updated_at DESC").all();
1637
- return rows.map(rowToSkill);
1638
- }
1639
- // ─── Skill Versions ───
1640
- insertSkillVersion(sv) {
1641
- this.db.prepare(`
1642
- INSERT OR REPLACE INTO skill_versions (id, skill_id, version, content, changelog, change_summary, upgrade_type, source_task_id, metrics, quality_score, created_at)
1643
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1644
- `).run(sv.id, sv.skillId, sv.version, sv.content, sv.changelog, sv.changeSummary, sv.upgradeType, sv.sourceTaskId, sv.metrics, sv.qualityScore, sv.createdAt);
1645
- }
1646
- getLatestSkillVersion(skillId) {
1647
- const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC LIMIT 1").get(skillId);
1648
- return row ? rowToSkillVersion(row) : null;
1649
- }
1650
- getSkillVersions(skillId) {
1651
- const rows = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC").all(skillId);
1652
- return rows.map(rowToSkillVersion);
1653
- }
1654
- getSkillVersion(skillId, version) {
1655
- const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? AND version = ?").get(skillId, version);
1656
- return row ? rowToSkillVersion(row) : null;
1657
- }
1658
- // ─── Task-Skill Links ───
1659
- linkTaskSkill(taskId, skillId, relation, versionAt) {
1660
- const skillExists = this.db.prepare("SELECT 1 FROM skills WHERE id = ?").get(skillId);
1661
- if (!skillExists)
1662
- return;
1663
- const taskExists = this.db.prepare("SELECT 1 FROM tasks WHERE id = ?").get(taskId);
1664
- if (!taskExists)
1665
- return;
1666
- this.db.prepare(`
1667
- INSERT OR REPLACE INTO task_skills (task_id, skill_id, relation, version_at, created_at)
1668
- VALUES (?, ?, ?, ?, ?)
1669
- `).run(taskId, skillId, relation, versionAt, Date.now());
1670
- }
1671
- getSkillsByTask(taskId) {
1672
- const rows = this.db.prepare(`
1673
- SELECT s.*, ts.relation, ts.version_at
1674
- FROM task_skills ts JOIN skills s ON s.id = ts.skill_id
1675
- WHERE ts.task_id = ?
1676
- `).all(taskId);
1677
- return rows.map(r => ({
1678
- skill: rowToSkill(r),
1679
- relation: r.relation,
1680
- versionAt: r.version_at,
1681
- }));
1682
- }
1683
- getTasksBySkill(skillId) {
1684
- const rows = this.db.prepare(`
1685
- SELECT t.*, ts.relation
1686
- FROM task_skills ts JOIN tasks t ON t.id = ts.task_id
1687
- WHERE ts.skill_id = ?
1688
- ORDER BY t.started_at DESC
1689
- `).all(skillId);
1690
- return rows.map(r => ({
1691
- task: rowToTask(r),
1692
- relation: r.relation,
1693
- }));
1694
- }
1695
- countSkills(status) {
1696
- const cond = status ? "WHERE status = ?" : "";
1697
- const params = status ? [status] : [];
1698
- const row = this.db.prepare(`SELECT COUNT(*) as c FROM skills ${cond}`).get(...params);
1699
- return row.c;
1700
- }
1701
- // ─── Chunk-Skill ───
1702
- setChunkSkillId(chunkId, skillId) {
1703
- this.db.prepare("UPDATE chunks SET skill_id = ?, updated_at = ? WHERE id = ?").run(skillId, Date.now(), chunkId);
1704
- }
1705
- getDistinctSessionKeys() {
1706
- return this.db.prepare("SELECT DISTINCT session_key FROM chunks ORDER BY session_key").all()
1707
- .map(r => r.session_key);
1708
- }
1709
- // ─── Hub / Client connection ───
1710
- setClientHubConnection(conn) {
1711
- this.db.prepare(`
1712
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status, hub_instance_id)
1713
- VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1714
- ON CONFLICT(id) DO UPDATE SET
1715
- hub_url = excluded.hub_url,
1716
- user_id = excluded.user_id,
1717
- username = excluded.username,
1718
- user_token = excluded.user_token,
1719
- role = excluded.role,
1720
- connected_at = excluded.connected_at,
1721
- identity_key = excluded.identity_key,
1722
- last_known_status = excluded.last_known_status,
1723
- hub_instance_id = excluded.hub_instance_id
1724
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "", conn.hubInstanceId ?? "");
1725
- }
1726
- getClientHubConnection() {
1727
- const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get();
1728
- return row ? rowToClientHubConnection(row) : null;
1729
- }
1730
- clearClientHubConnection() {
1731
- this.db.prepare('DELETE FROM client_hub_connection WHERE id = 1').run();
1732
- }
1733
- // ─── Local Shared Tasks (client-side tracking) ───
1734
- markTaskShared(taskId, hubTaskId, syncedChunks, visibility, groupId, hubInstanceId) {
1735
- this.db.prepare(`
1736
- INSERT INTO local_shared_tasks (task_id, hub_task_id, visibility, group_id, synced_chunks, hub_instance_id, shared_at)
1737
- VALUES (?, ?, ?, ?, ?, ?, ?)
1738
- ON CONFLICT(task_id) DO UPDATE SET
1739
- hub_task_id = excluded.hub_task_id,
1740
- visibility = excluded.visibility,
1741
- group_id = excluded.group_id,
1742
- synced_chunks = excluded.synced_chunks,
1743
- hub_instance_id = excluded.hub_instance_id,
1744
- shared_at = excluded.shared_at
1745
- `).run(taskId, hubTaskId, visibility, groupId ?? null, syncedChunks, hubInstanceId ?? "", Date.now());
1746
- }
1747
- unmarkTaskShared(taskId) {
1748
- this.db.prepare('DELETE FROM local_shared_tasks WHERE task_id = ?').run(taskId);
1749
- }
1750
- getLocalSharedTask(taskId) {
1751
- const row = this.db.prepare('SELECT * FROM local_shared_tasks WHERE task_id = ?').get(taskId);
1752
- if (!row)
1753
- return null;
1754
- return { taskId: row.task_id, hubTaskId: row.hub_task_id, visibility: row.visibility, groupId: row.group_id, syncedChunks: row.synced_chunks, sharedAt: row.shared_at, hubInstanceId: row.hub_instance_id || "" };
1755
- }
1756
- listLocalSharedTasks() {
1757
- const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks, hub_instance_id FROM local_shared_tasks').all();
1758
- return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks, hubInstanceId: r.hub_instance_id || "" }));
1759
- }
1760
- // ─── Local Shared Memories (client-side tracking) ───
1761
- markMemorySharedLocally(chunkId) {
1762
- const chunk = this.getChunk(chunkId);
1763
- if (!chunk)
1764
- return { ok: false, reason: "not_found" };
1765
- if (chunk.owner === "public") {
1766
- const existing = this.getLocalSharedMemory(chunkId);
1767
- return {
1768
- ok: true,
1769
- owner: "public",
1770
- originalOwner: existing?.originalOwner ?? undefined,
1771
- sharedAt: existing?.sharedAt ?? undefined,
1772
- };
1773
- }
1774
- const sharedAt = Date.now();
1775
- this.db.transaction(() => {
1776
- this.db.prepare(`
1777
- INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1778
- VALUES (?, ?, ?)
1779
- ON CONFLICT(chunk_id) DO UPDATE SET
1780
- original_owner = excluded.original_owner,
1781
- shared_at = excluded.shared_at
1782
- `).run(chunkId, chunk.owner, sharedAt);
1783
- this.updateChunk(chunkId, { owner: "public" });
1784
- })();
1785
- return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1786
- }
1787
- unmarkMemorySharedLocally(chunkId, fallbackOwner) {
1788
- const chunk = this.getChunk(chunkId);
1789
- if (!chunk)
1790
- return { ok: false, reason: "not_found" };
1791
- if (chunk.owner !== "public") {
1792
- return { ok: true, owner: chunk.owner };
1793
- }
1794
- const existing = this.getLocalSharedMemory(chunkId);
1795
- const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1796
- if (!restoreOwner || restoreOwner === "public") {
1797
- return { ok: false, reason: "original_owner_missing" };
1798
- }
1799
- this.db.transaction(() => {
1800
- this.updateChunk(chunkId, { owner: restoreOwner });
1801
- this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1802
- })();
1803
- return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1804
- }
1805
- getLocalSharedMemory(chunkId) {
1806
- const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId);
1807
- if (!row)
1808
- return null;
1809
- return {
1810
- chunkId: row.chunk_id,
1811
- originalOwner: row.original_owner,
1812
- sharedAt: row.shared_at,
1813
- };
1814
- }
1815
- // ─── Hub Users / Groups ───
1816
- upsertHubUser(user) {
1817
- this.db.prepare(`
1818
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
1819
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1820
- ON CONFLICT(id) DO UPDATE SET
1821
- username = excluded.username,
1822
- device_name = excluded.device_name,
1823
- role = excluded.role,
1824
- status = excluded.status,
1825
- token_hash = excluded.token_hash,
1826
- created_at = excluded.created_at,
1827
- approved_at = excluded.approved_at,
1828
- identity_key = excluded.identity_key,
1829
- left_at = excluded.left_at,
1830
- removed_at = excluded.removed_at,
1831
- rejected_at = excluded.rejected_at,
1832
- rejoin_requested_at = excluded.rejoin_requested_at
1833
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
1834
- }
1835
- getHubUser(userId) {
1836
- const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
1837
- if (!row)
1838
- return null;
1839
- const user = rowToHubUser(row);
1840
- user.groups = this.getGroupsForHubUser(userId);
1841
- return user;
1842
- }
1843
- listHubUsers(status) {
1844
- const rows = status
1845
- ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
1846
- : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
1847
- return rows.map(r => {
1848
- const user = rowToHubUser(r);
1849
- user.groups = this.getGroupsForHubUser(r.id);
1850
- return user;
1851
- });
1852
- }
1853
- deleteHubMemoriesByUser(userId) {
1854
- this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1855
- }
1856
- deleteHubTasksByUser(userId) {
1857
- this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1858
- }
1859
- deleteHubSkillsByUser(userId) {
1860
- this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1861
- }
1862
- deleteHubUser(userId, cleanResources = false) {
1863
- if (cleanResources) {
1864
- this.deleteHubTasksByUser(userId);
1865
- this.deleteHubSkillsByUser(userId);
1866
- this.deleteHubMemoriesByUser(userId);
1867
- const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1868
- return result.changes > 0;
1869
- }
1870
- const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
1871
- return result.changes > 0;
1872
- }
1873
- findHubUserByIdentityKey(identityKey) {
1874
- if (!identityKey)
1875
- return null;
1876
- const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey);
1877
- return row ? rowToHubUser(row) : null;
1878
- }
1879
- markHubUserLeft(userId) {
1880
- const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
1881
- return result.changes > 0;
1882
- }
1883
- updateHubUserActivity(userId, ip, timestamp) {
1884
- this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1885
- }
1886
- // ─── Hub Groups ───
1887
- upsertHubGroup(group) {
1888
- this.db.prepare(`
1889
- INSERT INTO hub_groups (id, name, description, created_at)
1890
- VALUES (?, ?, ?, ?)
1891
- ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
1892
- `).run(group.id, group.name, group.description ?? "", group.createdAt);
1893
- }
1894
- addHubGroupMember(groupId, userId, joinedAt) {
1895
- this.db.prepare(`
1896
- INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
1897
- VALUES (?, ?, ?)
1898
- `).run(groupId, userId, joinedAt);
1899
- }
1900
- removeHubGroupMember(groupId, userId) {
1901
- this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1902
- }
1903
- getGroupsForHubUser(userId) {
1904
- return this.db.prepare(`
1905
- SELECT g.id, g.name, g.description FROM hub_groups g
1906
- JOIN hub_group_members m ON m.group_id = g.id
1907
- WHERE m.user_id = ?
1908
- `).all(userId);
1909
- }
1910
- getHubUserContributions() {
1911
- const result = {};
1912
- const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
1913
- const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all();
1914
- const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all();
1915
- for (const r of memRows) {
1916
- if (!result[r.source_user_id])
1917
- result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1918
- result[r.source_user_id].memoryCount = r.cnt;
1919
- }
1920
- for (const r of taskRows) {
1921
- if (!result[r.source_user_id])
1922
- result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1923
- result[r.source_user_id].taskCount = r.cnt;
1924
- }
1925
- for (const r of skillRows) {
1926
- if (!result[r.source_user_id])
1927
- result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1928
- result[r.source_user_id].skillCount = r.cnt;
1929
- }
1930
- return result;
1931
- }
1932
- // ─── Hub Shared Data ───
1933
- upsertHubTask(task) {
1934
- this.db.prepare(`
1935
- INSERT INTO hub_tasks (id, source_task_id, source_user_id, title, summary, group_id, visibility, created_at, updated_at)
1936
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1937
- ON CONFLICT(source_user_id, source_task_id) DO UPDATE SET
1938
- title = excluded.title,
1939
- summary = excluded.summary,
1940
- group_id = excluded.group_id,
1941
- visibility = excluded.visibility,
1942
- created_at = excluded.created_at,
1943
- updated_at = excluded.updated_at
1944
- `).run(task.id, task.sourceTaskId, task.sourceUserId, task.title, task.summary, task.groupId, task.visibility, task.createdAt, task.updatedAt);
1945
- }
1946
- getHubTaskBySource(sourceUserId, sourceTaskId) {
1947
- const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
1948
- return row ? rowToHubTask(row) : null;
1949
- }
1950
- getHubTaskById(taskId) {
1951
- const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId);
1952
- return row ? rowToHubTask(row) : null;
1953
- }
1954
- upsertHubChunk(chunk) {
1955
- if (!chunk.sourceTaskId)
1956
- throw new Error("sourceTaskId is required for hub chunk upserts");
1957
- const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
1958
- this.db.prepare(`
1959
- INSERT INTO hub_chunks (id, hub_task_id, source_chunk_id, source_user_id, role, content, summary, kind, created_at)
1960
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1961
- ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
1962
- hub_task_id = excluded.hub_task_id,
1963
- role = excluded.role,
1964
- content = excluded.content,
1965
- summary = excluded.summary,
1966
- kind = excluded.kind,
1967
- created_at = excluded.created_at
1968
- `).run(chunk.id, taskId, chunk.sourceChunkId, chunk.sourceUserId, chunk.role, chunk.content, chunk.summary, chunk.kind, chunk.createdAt);
1969
- }
1970
- getHubChunkBySource(sourceUserId, sourceChunkId) {
1971
- const row = this.db.prepare('SELECT * FROM hub_chunks WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId);
1972
- return row ? rowToHubChunk(row) : null;
1973
- }
1974
- deleteHubTaskBySource(sourceUserId, sourceTaskId) {
1975
- this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').run(sourceUserId, sourceTaskId);
1976
- }
1977
- upsertHubSkill(skill) {
1978
- this.db.prepare(`
1979
- INSERT INTO hub_skills (id, source_skill_id, source_user_id, name, description, version, group_id, visibility, bundle, quality_score, created_at, updated_at)
1980
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1981
- ON CONFLICT(source_user_id, source_skill_id) DO UPDATE SET
1982
- name = excluded.name,
1983
- description = excluded.description,
1984
- version = excluded.version,
1985
- group_id = excluded.group_id,
1986
- visibility = excluded.visibility,
1987
- bundle = excluded.bundle,
1988
- quality_score = excluded.quality_score,
1989
- created_at = excluded.created_at,
1990
- updated_at = excluded.updated_at
1991
- `).run(skill.id, skill.sourceSkillId, skill.sourceUserId, skill.name, skill.description, skill.version, skill.groupId, skill.visibility, skill.bundle, skill.qualityScore, skill.createdAt, skill.updatedAt);
1992
- }
1993
- getHubSkillBySource(sourceUserId, sourceSkillId) {
1994
- const row = this.db.prepare('SELECT * FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').get(sourceUserId, sourceSkillId);
1995
- return row ? rowToHubSkill(row) : null;
1996
- }
1997
- getHubSkillById(skillId) {
1998
- const row = this.db.prepare('SELECT * FROM hub_skills WHERE id = ?').get(skillId);
1999
- return row ? rowToHubSkill(row) : null;
2000
- }
2001
- upsertHubSkillEmbedding(skillId, vector, sourceUserId, sourceSkillId) {
2002
- if (!sourceUserId || !sourceSkillId)
2003
- throw new Error("sourceUserId and sourceSkillId are required for hub skill embedding upserts");
2004
- const canonicalSkillId = this.resolveCanonicalHubSkillId(skillId, sourceUserId, sourceSkillId);
2005
- const buf = Buffer.allocUnsafe(vector.length * 4);
2006
- for (let i = 0; i < vector.length; i++)
2007
- buf.writeFloatLE(vector[i], i * 4);
2008
- this.db.prepare(`
2009
- INSERT INTO hub_skill_embeddings (skill_id, vector, dimensions, updated_at)
2010
- VALUES (?, ?, ?, ?)
2011
- ON CONFLICT(skill_id) DO UPDATE SET
2012
- vector = excluded.vector,
2013
- dimensions = excluded.dimensions,
2014
- updated_at = excluded.updated_at
2015
- `).run(canonicalSkillId, buf, vector.length, Date.now());
2016
- }
2017
- getHubSkillEmbedding(skillId) {
2018
- const row = this.db.prepare('SELECT vector, dimensions FROM hub_skill_embeddings WHERE skill_id = ?').get(skillId);
2019
- if (!row)
2020
- return null;
2021
- const out = [];
2022
- for (let i = 0; i < row.dimensions; i++)
2023
- out.push(row.vector.readFloatLE(i * 4));
2024
- return out;
2025
- }
2026
- getVisibleHubSkillEmbeddings() {
2027
- const rows = this.db.prepare(`
2028
- SELECT hse.skill_id, hse.vector, hse.dimensions
2029
- FROM hub_skill_embeddings hse
2030
- JOIN hub_skills hs ON hs.id = hse.skill_id
2031
- `).all();
2032
- return rows.map(r => ({
2033
- skillId: r.skill_id,
2034
- vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2035
- }));
2036
- }
2037
- upsertHubMemoryEmbedding(memoryId, vector) {
2038
- const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2039
- this.db.prepare(`
2040
- INSERT INTO hub_memory_embeddings (memory_id, vector, dimensions, updated_at)
2041
- VALUES (?, ?, ?, ?)
2042
- ON CONFLICT(memory_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
2043
- `).run(memoryId, buf, vector.length, Date.now());
2044
- }
2045
- getHubMemoryEmbedding(memoryId) {
2046
- const row = this.db.prepare('SELECT vector, dimensions FROM hub_memory_embeddings WHERE memory_id = ?').get(memoryId);
2047
- if (!row)
2048
- return null;
2049
- return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
2050
- }
2051
- getVisibleHubMemoryEmbeddings(userId) {
2052
- const rows = this.db.prepare(`
2053
- SELECT hme.memory_id, hme.vector, hme.dimensions
2054
- FROM hub_memory_embeddings hme
2055
- JOIN hub_memories hm ON hm.id = hme.memory_id
2056
- WHERE hm.visibility = 'public'
2057
- OR hm.source_user_id = ?
2058
- OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = hm.group_id AND gm.user_id = ?)
2059
- `).all(userId, userId);
2060
- return rows.map(r => ({
2061
- memoryId: r.memory_id,
2062
- vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2063
- }));
2064
- }
2065
- searchHubChunks(query, options) {
2066
- const limit = options?.maxResults ?? 10;
2067
- const userId = options?.userId ?? "";
2068
- const rows = this.db.prepare(`
2069
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2070
- COALESCE(hg.name, '') as group_name, hu.username as owner_name,
2071
- bm25(hub_chunks_fts) as rank
2072
- FROM hub_chunks_fts f
2073
- JOIN hub_chunks hc ON hc.rowid = f.rowid
2074
- JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2075
- LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2076
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
2077
- WHERE hub_chunks_fts MATCH ?
2078
- AND (ht.visibility = 'public'
2079
- OR ht.source_user_id = ?
2080
- OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
2081
- ORDER BY rank
2082
- LIMIT ?
2083
- `).all(sanitizeFtsQuery(query), userId, userId, limit);
2084
- return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2085
- }
2086
- upsertHubEmbedding(chunkId, vector) {
2087
- const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2088
- this.db.prepare(`
2089
- INSERT INTO hub_embeddings (chunk_id, vector, dimensions, updated_at)
2090
- VALUES (?, ?, ?, ?)
2091
- ON CONFLICT(chunk_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
2092
- `).run(chunkId, buf, vector.length, Date.now());
2093
- }
2094
- getHubEmbedding(chunkId) {
2095
- const row = this.db.prepare('SELECT vector, dimensions FROM hub_embeddings WHERE chunk_id = ?').get(chunkId);
2096
- if (!row)
2097
- return null;
2098
- return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
2099
- }
2100
- getVisibleHubEmbeddings(userId) {
2101
- const rows = this.db.prepare(`
2102
- SELECT he.chunk_id, he.vector, he.dimensions
2103
- FROM hub_embeddings he
2104
- JOIN hub_chunks hc ON hc.id = he.chunk_id
2105
- JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2106
- WHERE ht.visibility = 'public'
2107
- OR ht.source_user_id = ?
2108
- OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
2109
- `).all(userId, userId);
2110
- return rows.map(r => ({
2111
- chunkId: r.chunk_id,
2112
- vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2113
- }));
2114
- }
2115
- getVisibleHubSearchHitByChunkId(chunkId, userId) {
2116
- const row = this.db.prepare(`
2117
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2118
- COALESCE(hg.name, '') as group_name, hu.username as owner_name,
2119
- 0 as rank
2120
- FROM hub_chunks hc
2121
- JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2122
- LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2123
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
2124
- WHERE hc.id = ?
2125
- AND (ht.visibility = 'public'
2126
- OR ht.source_user_id = ?
2127
- OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
2128
- LIMIT 1
2129
- `).get(chunkId, userId, userId);
2130
- return row ?? null;
2131
- }
2132
- getHubChunkById(chunkId) {
2133
- const row = this.db.prepare('SELECT * FROM hub_chunks WHERE id = ?').get(chunkId);
2134
- return row ? rowToHubChunk(row) : null;
2135
- }
2136
- searchHubSkills(query, options) {
2137
- const limit = options?.maxResults ?? 10;
2138
- const userId = options?.userId ?? "";
2139
- const sanitized = sanitizeFtsQuery(query);
2140
- let rows;
2141
- if (sanitized) {
2142
- rows = this.db.prepare(`
2143
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
2144
- bm25(hub_skills_fts) as rank
2145
- FROM hub_skills_fts f
2146
- JOIN hub_skills hs ON hs.rowid = f.rowid
2147
- LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
2148
- WHERE hub_skills_fts MATCH ?
2149
- ORDER BY rank
2150
- LIMIT ?
2151
- `).all(sanitized, limit);
2152
- }
2153
- else {
2154
- rows = this.db.prepare(`
2155
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
2156
- 0 as rank
2157
- FROM hub_skills hs
2158
- LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
2159
- ORDER BY hs.updated_at DESC
2160
- LIMIT ?
2161
- `).all(limit);
2162
- }
2163
- return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2164
- }
2165
- deleteHubSkillBySource(sourceUserId, sourceSkillId) {
2166
- this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
2167
- }
2168
- listVisibleHubTasks(userId, limit = 40) {
2169
- const rows = this.db.prepare(`
2170
- SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
2171
- (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2172
- FROM hub_tasks t
2173
- LEFT JOIN hub_users u ON u.id = t.source_user_id
2174
- ORDER BY t.updated_at DESC
2175
- LIMIT ?
2176
- `).all(limit);
2177
- return rows.map(r => ({
2178
- id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2179
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2180
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2181
- createdAt: r.created_at, updatedAt: r.updated_at,
2182
- }));
2183
- }
2184
- listAllHubTasks() {
2185
- const rows = this.db.prepare(`
2186
- SELECT t.*, u.username AS owner_name, u.status AS owner_status,
2187
- (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2188
- FROM hub_tasks t
2189
- LEFT JOIN hub_users u ON u.id = t.source_user_id
2190
- ORDER BY t.updated_at DESC
2191
- `).all();
2192
- return rows.map(r => ({
2193
- id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2194
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
2195
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2196
- createdAt: r.created_at, updatedAt: r.updated_at,
2197
- }));
2198
- }
2199
- listHubChunksByTaskId(hubTaskId) {
2200
- const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId);
2201
- return rows.map(rowToHubChunk);
2202
- }
2203
- deleteHubTaskById(taskId) {
2204
- const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2205
- return info.changes > 0;
2206
- }
2207
- listVisibleHubSkills(userId, limit = 40) {
2208
- const rows = this.db.prepare(`
2209
- SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2210
- FROM hub_skills s
2211
- LEFT JOIN hub_users u ON u.id = s.source_user_id
2212
- ORDER BY s.updated_at DESC
2213
- LIMIT ?
2214
- `).all(limit);
2215
- return rows.map(r => ({
2216
- id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2217
- name: r.name, description: r.description, version: r.version,
2218
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2219
- ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2220
- createdAt: r.created_at, updatedAt: r.updated_at,
2221
- }));
2222
- }
2223
- listAllHubSkills() {
2224
- const rows = this.db.prepare(`
2225
- SELECT s.*, u.username AS owner_name, u.status AS owner_status
2226
- FROM hub_skills s
2227
- LEFT JOIN hub_users u ON u.id = s.source_user_id
2228
- ORDER BY s.updated_at DESC
2229
- `).all();
2230
- return rows.map(r => ({
2231
- id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2232
- name: r.name, description: r.description, version: r.version,
2233
- groupId: r.group_id, groupName: null, visibility: r.visibility,
2234
- ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2235
- createdAt: r.created_at, updatedAt: r.updated_at,
2236
- }));
2237
- }
2238
- deleteHubSkillById(skillId) {
2239
- const info = this.db.prepare('DELETE FROM hub_skills WHERE id = ?').run(skillId);
2240
- return info.changes > 0;
2241
- }
2242
- // ─── Hub Shared Memories (independent) ───
2243
- upsertHubMemory(memory) {
2244
- this.db.prepare(`
2245
- INSERT INTO hub_memories (id, source_chunk_id, source_user_id, role, content, summary, kind, group_id, visibility, created_at, updated_at)
2246
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2247
- ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
2248
- role = excluded.role,
2249
- content = excluded.content,
2250
- summary = excluded.summary,
2251
- kind = excluded.kind,
2252
- group_id = excluded.group_id,
2253
- visibility = excluded.visibility,
2254
- created_at = excluded.created_at,
2255
- updated_at = excluded.updated_at
2256
- `).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
2257
- }
2258
- getHubMemoryBySource(sourceUserId, sourceChunkId) {
2259
- const row = this.db.prepare('SELECT * FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId);
2260
- return row ? rowToHubMemory(row) : null;
2261
- }
2262
- getHubMemoryById(memoryId) {
2263
- const row = this.db.prepare('SELECT * FROM hub_memories WHERE id = ?').get(memoryId);
2264
- return row ? rowToHubMemory(row) : null;
2265
- }
2266
- deleteHubMemoryBySource(sourceUserId, sourceChunkId) {
2267
- this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').run(sourceUserId, sourceChunkId);
2268
- }
2269
- deleteHubMemoryById(memoryId) {
2270
- const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
2271
- return info.changes > 0;
2272
- }
2273
- // ─── Team share metadata (Client role — UI only, not used for local recall / FTS) ───
2274
- upsertTeamSharedChunk(chunkId, row) {
2275
- const now = Date.now();
2276
- const vis = row.visibility === "group" ? "group" : "public";
2277
- const gid = vis === "group" ? (row.groupId ?? null) : null;
2278
- this.db.prepare(`
2279
- INSERT INTO team_shared_chunks (chunk_id, hub_memory_id, visibility, group_id, hub_instance_id, shared_at)
2280
- VALUES (?, ?, ?, ?, ?, ?)
2281
- ON CONFLICT(chunk_id) DO UPDATE SET
2282
- hub_memory_id = excluded.hub_memory_id,
2283
- visibility = excluded.visibility,
2284
- group_id = excluded.group_id,
2285
- hub_instance_id = excluded.hub_instance_id,
2286
- shared_at = excluded.shared_at
2287
- `).run(chunkId, row.hubMemoryId ?? "", vis, gid, row.hubInstanceId ?? "", now);
2288
- }
2289
- getTeamSharedChunk(chunkId) {
2290
- const r = this.db.prepare("SELECT chunk_id, hub_memory_id, visibility, group_id, hub_instance_id, shared_at FROM team_shared_chunks WHERE chunk_id = ?").get(chunkId);
2291
- if (!r)
2292
- return null;
2293
- return {
2294
- chunkId: r.chunk_id,
2295
- hubMemoryId: r.hub_memory_id,
2296
- visibility: r.visibility,
2297
- groupId: r.group_id,
2298
- hubInstanceId: r.hub_instance_id || "",
2299
- sharedAt: r.shared_at,
2300
- };
2301
- }
2302
- deleteTeamSharedChunk(chunkId) {
2303
- const info = this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id = ?").run(chunkId);
2304
- return info.changes > 0;
2305
- }
2306
- // ─── Team Shared Skills (Client role — UI metadata only) ───
2307
- upsertTeamSharedSkill(skillId, row) {
2308
- const now = Date.now();
2309
- const vis = row.visibility === "group" ? "group" : "public";
2310
- const gid = vis === "group" ? (row.groupId ?? null) : null;
2311
- this.db.prepare(`
2312
- INSERT INTO team_shared_skills (skill_id, hub_skill_id, visibility, group_id, hub_instance_id, shared_at)
2313
- VALUES (?, ?, ?, ?, ?, ?)
2314
- ON CONFLICT(skill_id) DO UPDATE SET
2315
- hub_skill_id = excluded.hub_skill_id,
2316
- visibility = excluded.visibility,
2317
- group_id = excluded.group_id,
2318
- hub_instance_id = excluded.hub_instance_id,
2319
- shared_at = excluded.shared_at
2320
- `).run(skillId, row.hubSkillId ?? "", vis, gid, row.hubInstanceId ?? "", now);
2321
- }
2322
- getTeamSharedSkill(skillId) {
2323
- const r = this.db.prepare("SELECT * FROM team_shared_skills WHERE skill_id = ?").get(skillId);
2324
- if (!r)
2325
- return null;
2326
- return { skillId: r.skill_id, hubSkillId: r.hub_skill_id, visibility: r.visibility, groupId: r.group_id, hubInstanceId: r.hub_instance_id || "", sharedAt: r.shared_at };
2327
- }
2328
- deleteTeamSharedSkill(skillId) {
2329
- return this.db.prepare("DELETE FROM team_shared_skills WHERE skill_id = ?").run(skillId).changes > 0;
2330
- }
2331
- // ─── Team sharing cleanup (role switch / leave) ───
2332
- clearTeamSharedChunks() {
2333
- this.db.prepare("DELETE FROM team_shared_chunks").run();
2334
- }
2335
- clearTeamSharedSkills() {
2336
- this.db.prepare("DELETE FROM team_shared_skills").run();
2337
- }
2338
- downgradeTeamSharedTasksToLocal() {
2339
- this.db.prepare("UPDATE local_shared_tasks SET hub_task_id = '', hub_instance_id = '', visibility = 'public', group_id = NULL, synced_chunks = 0").run();
2340
- }
2341
- downgradeTeamSharedTaskToLocal(taskId) {
2342
- this.db.prepare("UPDATE local_shared_tasks SET hub_task_id = '', hub_instance_id = '', visibility = 'public', group_id = NULL, synced_chunks = 0 WHERE task_id = ?").run(taskId);
2343
- }
2344
- clearAllTeamSharingState() {
2345
- this.clearTeamSharedChunks();
2346
- this.clearTeamSharedSkills();
2347
- this.downgradeTeamSharedTasksToLocal();
2348
- }
2349
- // ─── Hub Notifications ───
2350
- insertHubNotification(n) {
2351
- this.db.prepare('INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)').run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2352
- }
2353
- hasRecentHubNotification(userId, type, resource, windowMs = 300_000) {
2354
- const since = Date.now() - windowMs;
2355
- const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?').get(userId, type, resource, since);
2356
- return row.cnt > 0;
2357
- }
2358
- listHubNotifications(userId, opts) {
2359
- const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2360
- const limit = opts?.limit ?? 50;
2361
- const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit);
2362
- return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
2363
- }
2364
- countUnreadHubNotifications(userId) {
2365
- const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId);
2366
- return row.cnt;
2367
- }
2368
- markHubNotificationsRead(userId, ids) {
2369
- if (ids && ids.length > 0) {
2370
- const placeholders = ids.map(() => '?').join(',');
2371
- this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2372
- }
2373
- else {
2374
- this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2375
- }
2376
- }
2377
- clearHubNotifications(userId) {
2378
- this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2379
- }
2380
- // upsertHubMemoryEmbedding / getHubMemoryEmbedding removed:
2381
- // hub memory vectors are now computed on-the-fly at search time.
2382
- searchHubMemories(query, options) {
2383
- const limit = options?.maxResults ?? 10;
2384
- const userId = options?.userId ?? "";
2385
- const sanitized = sanitizeFtsQuery(query);
2386
- if (!sanitized)
2387
- return [];
2388
- const rows = this.db.prepare(`
2389
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2390
- bm25(hub_memories_fts) as rank
2391
- FROM hub_memories_fts f
2392
- JOIN hub_memories hm ON hm.rowid = f.rowid
2393
- LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2394
- WHERE hub_memories_fts MATCH ?
2395
- ORDER BY rank
2396
- LIMIT ?
2397
- `).all(sanitized, limit);
2398
- return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2399
- }
2400
- // getVisibleHubMemoryEmbeddings removed: vectors computed on-the-fly at search time.
2401
- getVisibleHubSearchHitByMemoryId(memoryId, userId) {
2402
- const row = this.db.prepare(`
2403
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2404
- 0 as rank
2405
- FROM hub_memories hm
2406
- LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2407
- WHERE hm.id = ?
2408
- LIMIT 1
2409
- `).get(memoryId);
2410
- return row ?? null;
2411
- }
2412
- listVisibleHubMemories(userId, limit = 40) {
2413
- const rows = this.db.prepare(`
2414
- SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2415
- FROM hub_memories m
2416
- LEFT JOIN hub_users u ON u.id = m.source_user_id
2417
- ORDER BY m.updated_at DESC
2418
- LIMIT ?
2419
- `).all(limit);
2420
- return rows.map(r => ({
2421
- id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2422
- role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2423
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2424
- ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2425
- }));
2426
- }
2427
- listAllHubMemories() {
2428
- const rows = this.db.prepare(`
2429
- SELECT m.*, u.username AS owner_name, u.status AS owner_status
2430
- FROM hub_memories m
2431
- LEFT JOIN hub_users u ON u.id = m.source_user_id
2432
- ORDER BY m.updated_at DESC
2433
- `).all();
2434
- return rows.map(r => ({
2435
- id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2436
- role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2437
- groupId: r.group_id, groupName: null, visibility: r.visibility,
2438
- ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2439
- }));
2440
- }
2441
- resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
2442
- if (sourceTaskId) {
2443
- const bySource = this.db.prepare('SELECT id FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
2444
- if (!bySource)
2445
- throw new Error(`source task not found for user=${sourceUserId} sourceTaskId=${sourceTaskId}`);
2446
- if (bySource.id != taskId)
2447
- throw new Error(`mismatch between source task and hubTaskId: expected ${bySource.id}, got ${taskId}`);
2448
- return bySource.id;
2449
- }
2450
- throw new Error(`source task not found for user=${sourceUserId} taskId=${taskId}`);
2451
- }
2452
- resolveCanonicalHubSkillId(skillId, sourceUserId, sourceSkillId) {
2453
- if (sourceUserId && sourceSkillId) {
2454
- const bySource = this.db.prepare('SELECT id FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').get(sourceUserId, sourceSkillId);
2455
- if (!bySource)
2456
- throw new Error(`source skill not found for user=${sourceUserId} sourceSkillId=${sourceSkillId}`);
2457
- if (bySource.id != skillId)
2458
- throw new Error(`mismatch between source skill and skillId: expected ${bySource.id}, got ${skillId}`);
2459
- return bySource.id;
2460
- }
2461
- throw new Error(`source skill not found for skillId=${skillId}`);
2462
- }
2463
- getSessionOwnerMap(sessionKeys) {
2464
- const result = new Map();
2465
- if (sessionKeys.length === 0)
2466
- return result;
2467
- const placeholders = sessionKeys.map(() => "?").join(",");
2468
- const rows = this.db.prepare(`SELECT session_key, owner FROM chunks WHERE session_key IN (${placeholders}) AND owner IS NOT NULL GROUP BY session_key`).all(...sessionKeys);
2469
- for (const r of rows)
2470
- result.set(r.session_key, r.owner);
2471
- return result;
2472
- }
2473
- close() {
2474
- this.db.close();
2475
- }
2476
- }
2477
- exports.SqliteStore = SqliteStore;
2478
- // ─── FTS helpers ───
2479
- /**
2480
- * Sanitize user input for FTS5 MATCH queries.
2481
- * Strip FTS operators and special characters, then join tokens
2482
- * with implicit AND (space-separated) for safe querying.
2483
- */
2484
- function sanitizeFtsQuery(raw) {
2485
- const tokens = raw
2486
- .replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`-]/g, " ")
2487
- .split(/\s+/)
2488
- .map((t) => t.trim().replace(/^-+|-+$/g, ""))
2489
- .filter((t) => t.length > 1)
2490
- .filter((t) => !FTS_RESERVED.has(t.toUpperCase()));
2491
- return tokens.join(" ");
2492
- }
2493
- const FTS_RESERVED = new Set(["AND", "OR", "NOT", "NEAR"]);
2494
- function rowToChunk(row) {
2495
- return {
2496
- id: row.id,
2497
- sessionKey: row.session_key,
2498
- turnId: row.turn_id,
2499
- seq: row.seq,
2500
- role: row.role,
2501
- content: row.content,
2502
- kind: row.kind,
2503
- summary: row.summary,
2504
- embedding: null,
2505
- taskId: row.task_id,
2506
- skillId: row.skill_id ?? null,
2507
- owner: row.owner ?? "agent:main",
2508
- dedupStatus: (row.dedup_status ?? "active"),
2509
- dedupTarget: row.dedup_target ?? null,
2510
- dedupReason: row.dedup_reason ?? null,
2511
- mergeCount: row.merge_count ?? 0,
2512
- lastHitAt: row.last_hit_at ?? null,
2513
- mergeHistory: row.merge_history ?? "[]",
2514
- createdAt: row.created_at,
2515
- updatedAt: row.updated_at,
2516
- };
2517
- }
2518
- function rowToTask(row) {
2519
- return {
2520
- id: row.id,
2521
- sessionKey: row.session_key,
2522
- title: row.title,
2523
- summary: row.summary,
2524
- status: row.status,
2525
- owner: row.owner ?? "agent:main",
2526
- startedAt: row.started_at,
2527
- endedAt: row.ended_at,
2528
- updatedAt: row.updated_at,
2529
- };
2530
- }
2531
- function rowToSkill(row) {
2532
- return {
2533
- id: row.id,
2534
- name: row.name,
2535
- description: row.description,
2536
- version: row.version,
2537
- status: row.status,
2538
- tags: row.tags,
2539
- sourceType: row.source_type,
2540
- dirPath: row.dir_path,
2541
- installed: row.installed,
2542
- owner: row.owner ?? "agent:main",
2543
- visibility: (row.visibility ?? "private"),
2544
- qualityScore: row.quality_score ?? null,
2545
- createdAt: row.created_at,
2546
- updatedAt: row.updated_at,
2547
- };
2548
- }
2549
- function rowToSkillVersion(row) {
2550
- return {
2551
- id: row.id,
2552
- skillId: row.skill_id,
2553
- version: row.version,
2554
- content: row.content,
2555
- changelog: row.changelog,
2556
- changeSummary: row.change_summary ?? "",
2557
- upgradeType: row.upgrade_type,
2558
- sourceTaskId: row.source_task_id,
2559
- metrics: row.metrics,
2560
- qualityScore: row.quality_score ?? null,
2561
- createdAt: row.created_at,
2562
- };
2563
- }
2564
- function rowToClientHubConnection(row) {
2565
- return {
2566
- hubUrl: row.hub_url,
2567
- userId: row.user_id,
2568
- username: row.username,
2569
- userToken: row.user_token,
2570
- role: row.role,
2571
- connectedAt: row.connected_at,
2572
- identityKey: row.identity_key || "",
2573
- lastKnownStatus: row.last_known_status || "",
2574
- hubInstanceId: row.hub_instance_id || "",
2575
- };
2576
- }
2577
- function rowToHubUser(row) {
2578
- return {
2579
- id: row.id,
2580
- username: row.username,
2581
- deviceName: row.device_name || undefined,
2582
- role: row.role,
2583
- status: row.status,
2584
- groups: [],
2585
- tokenHash: row.token_hash,
2586
- createdAt: row.created_at,
2587
- approvedAt: row.approved_at,
2588
- lastIp: row.last_ip || "",
2589
- lastActiveAt: row.last_active_at ?? null,
2590
- identityKey: row.identity_key || "",
2591
- leftAt: row.left_at ?? null,
2592
- removedAt: row.removed_at ?? null,
2593
- rejectedAt: row.rejected_at ?? null,
2594
- rejoinRequestedAt: row.rejoin_requested_at ?? null,
2595
- };
2596
- }
2597
- function rowToHubTask(row) {
2598
- return {
2599
- id: row.id,
2600
- sourceTaskId: row.source_task_id,
2601
- sourceUserId: row.source_user_id,
2602
- title: row.title,
2603
- summary: row.summary,
2604
- groupId: row.group_id,
2605
- visibility: row.visibility,
2606
- createdAt: row.created_at,
2607
- updatedAt: row.updated_at,
2608
- };
2609
- }
2610
- function rowToHubChunk(row) {
2611
- return {
2612
- id: row.id,
2613
- hubTaskId: row.hub_task_id,
2614
- sourceChunkId: row.source_chunk_id,
2615
- sourceUserId: row.source_user_id,
2616
- role: row.role,
2617
- content: row.content,
2618
- summary: row.summary,
2619
- kind: row.kind,
2620
- createdAt: row.created_at,
2621
- };
2622
- }
2623
- function rowToHubSkill(row) {
2624
- return {
2625
- id: row.id,
2626
- sourceSkillId: row.source_skill_id,
2627
- sourceUserId: row.source_user_id,
2628
- name: row.name,
2629
- description: row.description,
2630
- version: row.version,
2631
- groupId: row.group_id,
2632
- visibility: row.visibility,
2633
- bundle: row.bundle,
2634
- qualityScore: row.quality_score,
2635
- createdAt: row.created_at,
2636
- updatedAt: row.updated_at,
2637
- };
2638
- }
2639
- function rowToHubMemory(row) {
2640
- return {
2641
- id: row.id,
2642
- sourceChunkId: row.source_chunk_id,
2643
- sourceUserId: row.source_user_id,
2644
- role: row.role,
2645
- content: row.content,
2646
- summary: row.summary,
2647
- kind: row.kind,
2648
- groupId: row.group_id,
2649
- visibility: row.visibility,
2650
- createdAt: row.created_at,
2651
- updatedAt: row.updated_at,
2652
- };
2653
- }
2654
- function contentHash(content) {
2655
- return (0, crypto_1.createHash)("sha256").update(content).digest("hex").slice(0, 16);
2656
- }
2657
- //# sourceMappingURL=sqlite.js.map