@desplega.ai/agent-swarm 1.92.0 → 1.92.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 (80) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +327 -20
  6. package/src/be/memory/constants.ts +2 -1
  7. package/src/be/memory/providers/openai-embedding.ts +2 -5
  8. package/src/be/memory/providers/sqlite-store.ts +293 -76
  9. package/src/be/memory/types.ts +35 -0
  10. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  11. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  12. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  13. package/src/be/migrations/087_skill_files.sql +19 -0
  14. package/src/be/modelsdev-cache.json +264 -328
  15. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  16. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  17. package/src/be/seed-scripts/catalog/compound-insights.ts +94 -0
  18. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  19. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  20. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
  21. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  22. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  23. package/src/be/seed-scripts/index.ts +32 -4
  24. package/src/be/seed-skills/index.ts +0 -7
  25. package/src/be/skill-sync.ts +91 -7
  26. package/src/commands/runner.ts +6 -2
  27. package/src/heartbeat/templates.ts +20 -16
  28. package/src/http/index.ts +41 -7
  29. package/src/http/mcp-user.ts +23 -0
  30. package/src/http/mcp.ts +58 -0
  31. package/src/http/memory.ts +58 -0
  32. package/src/http/pages.ts +1 -1
  33. package/src/http/script-runs.ts +2 -0
  34. package/src/http/scripts.ts +39 -2
  35. package/src/http/skills.ts +225 -0
  36. package/src/providers/claude-adapter.ts +56 -24
  37. package/src/script-workflows/workflow-ctx.ts +7 -3
  38. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  39. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  40. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  41. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  42. package/src/server.ts +2 -0
  43. package/src/tests/claude-adapter-binary.test.ts +135 -81
  44. package/src/tests/create-page-tool.test.ts +19 -2
  45. package/src/tests/heartbeat-checklist.test.ts +36 -0
  46. package/src/tests/mcp-transport-gc.test.ts +58 -0
  47. package/src/tests/memory-health-endpoint.test.ts +78 -0
  48. package/src/tests/memory-store.test.ts +221 -1
  49. package/src/tests/pages-http.test.ts +20 -2
  50. package/src/tests/pages-storage.test.ts +26 -0
  51. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  52. package/src/tests/seed-scripts.test.ts +123 -3
  53. package/src/tests/skill-files-http.test.ts +171 -0
  54. package/src/tests/skill-files.test.ts +162 -0
  55. package/src/tests/skill-get-file-tool.test.ts +110 -0
  56. package/src/tests/skill-sync.test.ts +125 -6
  57. package/src/tools/create-page.ts +2 -2
  58. package/src/tools/skills/index.ts +1 -0
  59. package/src/tools/skills/skill-get-file.ts +80 -0
  60. package/src/tools/tool-config.ts +2 -1
  61. package/src/types.ts +20 -0
  62. package/src/utils/internal-ai/complete-structured.ts +2 -2
  63. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  64. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  65. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  66. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  67. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  68. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  69. package/templates/skills/agentmail-sending/content.md +6 -7
  70. package/templates/skills/desloppify/content.md +8 -9
  71. package/templates/skills/jira-interaction/content.md +25 -33
  72. package/templates/skills/kapso-whatsapp/content.md +29 -30
  73. package/templates/skills/linear-interaction/content.md +8 -9
  74. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  75. package/templates/skills/sprite-cli/content.md +4 -5
  76. package/templates/skills/turso-interaction/content.md +14 -17
  77. package/templates/skills/workflow-iterate/content.md +38 -391
  78. package/templates/skills/x-api-interactions/content.md +4 -6
  79. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  80. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -1,16 +1,20 @@
1
1
  import { getDb, isSqliteVecAvailable } from "@/be/db";
2
2
  import { cosineSimilarity, deserializeEmbedding, serializeEmbedding } from "@/be/embedding";
3
3
  import type { AgentMemory, AgentMemoryScope, AgentMemorySource } from "@/types";
4
- import { TTL_DEFAULTS } from "../constants";
4
+ import { EMBEDDING_DIMENSIONS, TTL_DEFAULTS } from "../constants";
5
5
  import type {
6
6
  MemoryCandidate,
7
+ MemoryHealth,
7
8
  MemoryInput,
8
9
  MemoryListOptions,
9
10
  MemorySearchOptions,
10
11
  MemoryStats,
11
12
  MemoryStore,
13
+ MemoryVecPopulateStats,
12
14
  } from "../types";
13
15
 
16
+ const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
17
+
14
18
  type AgentMemoryRow = {
15
19
  id: string;
16
20
  agentId: string | null;
@@ -76,56 +80,154 @@ function computeExpiresAt(source: AgentMemorySource): string | null {
76
80
 
77
81
  export class SqliteMemoryStore implements MemoryStore {
78
82
  private vecInitialized = false;
83
+ private lastPopulate: MemoryVecPopulateStats | null = null;
79
84
 
80
85
  constructor() {
81
86
  this.ensureVecTable();
82
87
  }
83
88
 
84
89
  private ensureVecTable(): void {
85
- if (this.vecInitialized || !isSqliteVecAvailable()) return;
90
+ if (this.vecInitialized) return;
91
+
92
+ if (!isSqliteVecAvailable()) {
93
+ console.warn("[memory-vec] sqlite-vec extension_loaded=false; retrieval_mode=fallback");
94
+ return;
95
+ }
86
96
 
87
97
  const db = getDb();
88
- // Create the virtual table if it doesn't exist
89
98
  try {
99
+ console.log(
100
+ `[memory-vec] sqlite-vec extension_loaded=true vector_dimensions=${EMBEDDING_DIMENSIONS}`,
101
+ );
102
+
103
+ const existingSchema = this.getVecTableSchema();
104
+ if (existingSchema && !existingSchema.includes("distance_metric=cosine")) {
105
+ console.warn(
106
+ "[memory-vec] Existing memory_vec table is missing cosine distance metric; rebuilding from agent_memory",
107
+ );
108
+ db.run("DROP TABLE memory_vec");
109
+ }
110
+
90
111
  db.run(`
91
112
  CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
92
113
  memory_id TEXT PRIMARY KEY,
93
- embedding float[512]
114
+ embedding float[${EMBEDDING_DIMENSIONS}] distance_metric=cosine
94
115
  )
95
116
  `);
96
117
 
97
- // Populate from existing embeddings that aren't yet in the vec table
98
- const existing = db
99
- .prepare<{ id: string; embedding: Buffer }, []>(
100
- "SELECT id, embedding FROM agent_memory WHERE embedding IS NOT NULL",
101
- )
102
- .all();
103
-
104
- if (existing.length > 0) {
105
- const vecCount = db
106
- .prepare<{ count: number }, []>("SELECT COUNT(*) as count FROM memory_vec")
107
- .get();
108
-
109
- if ((vecCount?.count ?? 0) < existing.length) {
110
- const insert = db.prepare(
111
- "INSERT OR IGNORE INTO memory_vec(memory_id, embedding) VALUES (?, ?)",
112
- );
113
- const tx = db.transaction(() => {
114
- for (const row of existing) {
115
- insert.run(row.id, row.embedding);
116
- }
117
- });
118
- tx();
119
- console.log(`[memory] Synced ${existing.length} embeddings to memory_vec`);
120
- }
118
+ const healthBefore = this.getHealthCounts();
119
+ if (healthBefore.missingFromVec > 0 || healthBefore.extraInVec > 0) {
120
+ this.populateVecTable(healthBefore.memoryVec);
121
+ } else {
122
+ console.log(
123
+ `[memory-vec] populate skipped attempted=0 inserted=0 memory_vec=${healthBefore.memoryVec} valid_embedding=${healthBefore.validEmbedding}`,
124
+ );
121
125
  }
122
126
 
123
127
  this.vecInitialized = true;
124
128
  } catch (err) {
125
- console.warn("[memory] Failed to initialize memory_vec:", (err as Error).message);
129
+ this.vecInitialized = false;
130
+ console.error("[memory-vec] Failed to initialize memory_vec:", (err as Error).message);
131
+ }
132
+ }
133
+
134
+ private getVecTableSchema(): string | null {
135
+ try {
136
+ return (
137
+ getDb()
138
+ .prepare<{ sql: string | null }, []>(
139
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_vec'",
140
+ )
141
+ .get()?.sql ?? null
142
+ );
143
+ } catch {
144
+ return null;
126
145
  }
127
146
  }
128
147
 
148
+ private getVecCount(): number {
149
+ if (!this.getVecTableSchema()) return 0;
150
+ return (
151
+ getDb().prepare<{ count: number }, []>("SELECT COUNT(*) as count FROM memory_vec").get()
152
+ ?.count ?? 0
153
+ );
154
+ }
155
+
156
+ private populateVecTable(beforeCount: number): void {
157
+ const db = getDb();
158
+ const deletedExtra = db
159
+ .prepare(
160
+ `DELETE FROM memory_vec
161
+ WHERE memory_id NOT IN (SELECT id FROM agent_memory)`,
162
+ )
163
+ .run();
164
+ if (deletedExtra.changes > 0) {
165
+ console.warn(`[memory-vec] removed_extra_rows count=${deletedExtra.changes}`);
166
+ }
167
+
168
+ const rows = db
169
+ .prepare<{ id: string; embedding: Buffer }, []>(
170
+ "SELECT id, embedding FROM agent_memory WHERE embedding IS NOT NULL",
171
+ )
172
+ .all();
173
+ const deleteVec = db.prepare("DELETE FROM memory_vec WHERE memory_id = ?");
174
+ const insertVec = db.prepare("INSERT INTO memory_vec(memory_id, embedding) VALUES (?, ?)");
175
+
176
+ let attempted = 0;
177
+ let inserted = 0;
178
+ let skippedInvalidDimensions = 0;
179
+ let failed = 0;
180
+
181
+ for (const row of rows) {
182
+ const embeddingBuffer = this.toVecBuffer(row.embedding);
183
+ if (!embeddingBuffer) {
184
+ skippedInvalidDimensions++;
185
+ continue;
186
+ }
187
+
188
+ attempted++;
189
+ try {
190
+ deleteVec.run(row.id);
191
+ insertVec.run(row.id, embeddingBuffer);
192
+ inserted++;
193
+ } catch (err) {
194
+ failed++;
195
+ console.error(
196
+ `[memory-vec] populate failed memory_id=${row.id}: ${(err as Error).message}`,
197
+ );
198
+ }
199
+ }
200
+
201
+ const afterCount = this.getVecCount();
202
+ this.lastPopulate = {
203
+ attempted,
204
+ inserted,
205
+ skippedInvalidDimensions,
206
+ failed,
207
+ beforeCount,
208
+ afterCount,
209
+ };
210
+
211
+ console.log(
212
+ `[memory-vec] populate attempted=${attempted} inserted=${inserted} skipped_invalid_dimensions=${skippedInvalidDimensions} failed=${failed} before_count=${beforeCount} after_count=${afterCount}`,
213
+ );
214
+
215
+ if (failed > 0 || afterCount < attempted) {
216
+ console.error(
217
+ `[memory-vec] populate incomplete attempted=${attempted} after_count=${afterCount} failed=${failed}`,
218
+ );
219
+ }
220
+ }
221
+
222
+ private toVecBuffer(embedding: Buffer | Float32Array): Buffer | null {
223
+ if (embedding instanceof Float32Array) {
224
+ if (embedding.length !== EMBEDDING_DIMENSIONS) return null;
225
+ return serializeEmbedding(embedding);
226
+ }
227
+ if (embedding.length !== VECTOR_BYTES) return null;
228
+ return embedding;
229
+ }
230
+
129
231
  store(input: MemoryInput): AgentMemory {
130
232
  const id = crypto.randomUUID();
131
233
  const now = new Date().toISOString();
@@ -223,7 +325,11 @@ export class SqliteMemoryStore implements MemoryStore {
223
325
  ): MemoryCandidate[] {
224
326
  const { scope = "all", limit = 10, source, isLead = false, includeExpired = false } = options;
225
327
 
226
- if (isSqliteVecAvailable() && this.vecInitialized) {
328
+ const health = this.getHealth();
329
+ if (health.retrievalMode === "vec" && embedding.length === EMBEDDING_DIMENSIONS) {
330
+ console.log(
331
+ `[memory-search] retrieval_path=vec scope=${scope} limit=${limit} vec_rows=${health.counts.memoryVec} searchable=${health.counts.searchable}`,
332
+ );
227
333
  return this.searchWithVec(embedding, agentId, {
228
334
  scope,
229
335
  limit,
@@ -232,6 +338,10 @@ export class SqliteMemoryStore implements MemoryStore {
232
338
  includeExpired,
233
339
  });
234
340
  }
341
+
342
+ console.log(
343
+ `[memory-search] retrieval_path=fallback scope=${scope} limit=${limit} reason=${embedding.length !== EMBEDDING_DIMENSIONS ? "query_dimension_mismatch" : health.reasons.join("|") || "vec_unavailable"}`,
344
+ );
235
345
  return this.searchBruteForce(embedding, agentId, {
236
346
  scope,
237
347
  limit,
@@ -255,58 +365,45 @@ export class SqliteMemoryStore implements MemoryStore {
255
365
  const db = getDb();
256
366
  const { scope, limit, source, isLead, includeExpired } = options;
257
367
 
258
- // KNN query — fetch more candidates than needed for post-filtering
259
- const knnLimit = limit * 5; // over-fetch to account for scope/expiry filters
260
368
  const embeddingBuffer = serializeEmbedding(queryEmbedding);
369
+ // sqlite-vec hard ceiling is 4096 for knn queries
370
+ const knnLimit = Math.min(Math.max(limit, this.getVecCount()), 4096);
261
371
 
262
- const vecRows = db
263
- .prepare<{ memory_id: string; distance: number }, [Buffer, number]>(
264
- "SELECT memory_id, distance FROM memory_vec WHERE embedding MATCH ? AND k = ?",
265
- )
266
- .all(embeddingBuffer, knnLimit);
372
+ const conditions: string[] = ["v.embedding MATCH ?"];
373
+ const params: (Buffer | string | number | null)[] = [embeddingBuffer];
267
374
 
268
- if (vecRows.length === 0) return [];
269
-
270
- // Build ID list and distance map
271
- const distanceMap = new Map<string, number>();
272
- const ids: string[] = [];
273
- for (const vr of vecRows) {
274
- distanceMap.set(vr.memory_id, vr.distance);
275
- ids.push(vr.memory_id);
276
- }
277
-
278
- // Hydrate from agent_memory with filters
279
- const placeholders = ids.map(() => "?").join(",");
280
- const conditions: string[] = [`id IN (${placeholders})`];
281
- const params: (string | null)[] = [...ids];
282
-
283
- this.addScopeConditions(conditions, params, agentId, scope, isLead);
375
+ this.addScopeConditions(conditions, params, agentId, scope, isLead, "m");
284
376
 
285
377
  if (source) {
286
- conditions.push("source = ?");
378
+ conditions.push("m.source = ?");
287
379
  params.push(source);
288
380
  }
289
381
 
290
382
  if (!includeExpired) {
291
- conditions.push("(expiresAt IS NULL OR expiresAt > datetime('now'))");
383
+ conditions.push("(m.expiresAt IS NULL OR m.expiresAt > datetime('now'))");
292
384
  }
293
385
 
386
+ conditions.push("v.k = ?");
387
+ params.push(knnLimit);
388
+
294
389
  const rows = db
295
- .prepare<AgentMemoryRow, (string | null)[]>(
296
- `SELECT * FROM agent_memory WHERE ${conditions.join(" AND ")}`,
390
+ .prepare<AgentMemoryRow & { distance: number }, (Buffer | string | number | null)[]>(
391
+ `SELECT m.*, v.distance
392
+ FROM memory_vec v
393
+ JOIN agent_memory m ON m.id = v.memory_id
394
+ WHERE ${conditions.join(" AND ")}
395
+ ORDER BY v.distance
396
+ LIMIT ?`,
297
397
  )
298
- .all(...params);
398
+ .all(...params, limit);
299
399
 
300
- // Map to candidates with similarity scores
301
400
  const candidates: MemoryCandidate[] = [];
302
401
  for (const row of rows) {
303
- const distance = distanceMap.get(row.id) ?? 1;
304
- const similarity = 1 - distance; // cosine distance to similarity
402
+ const similarity = 1 - row.distance;
305
403
  candidates.push(rowToCandidate(row, similarity));
306
404
  }
307
405
 
308
- candidates.sort((a, b) => b.similarity - a.similarity);
309
- return candidates.slice(0, limit);
406
+ return candidates;
310
407
  }
311
408
 
312
409
  private searchBruteForce(
@@ -358,26 +455,28 @@ export class SqliteMemoryStore implements MemoryStore {
358
455
 
359
456
  private addScopeConditions(
360
457
  conditions: string[],
361
- params: (string | null)[],
458
+ params: (Buffer | string | number | null)[],
362
459
  agentId: string,
363
460
  scope: string,
364
461
  isLead: boolean,
462
+ tableAlias = "",
365
463
  ): void {
464
+ const col = (name: string) => (tableAlias ? `${tableAlias}.${name}` : name);
366
465
  if (!isLead) {
367
466
  if (scope === "agent") {
368
- conditions.push("agentId = ? AND scope = 'agent'");
467
+ conditions.push(`${col("agentId")} = ? AND ${col("scope")} = 'agent'`);
369
468
  params.push(agentId);
370
469
  } else if (scope === "swarm") {
371
- conditions.push("scope = 'swarm'");
470
+ conditions.push(`${col("scope")} = 'swarm'`);
372
471
  } else {
373
- conditions.push("(agentId = ? OR scope = 'swarm')");
472
+ conditions.push(`(${col("agentId")} = ? OR ${col("scope")} = 'swarm')`);
374
473
  params.push(agentId);
375
474
  }
376
475
  } else {
377
476
  if (scope === "agent") {
378
- conditions.push("scope = 'agent'");
477
+ conditions.push(`${col("scope")} = 'agent'`);
379
478
  } else if (scope === "swarm") {
380
- conditions.push("scope = 'swarm'");
479
+ conditions.push(`${col("scope")} = 'swarm'`);
381
480
  }
382
481
  }
383
482
  }
@@ -440,7 +539,7 @@ export class SqliteMemoryStore implements MemoryStore {
440
539
 
441
540
  delete(id: string): boolean {
442
541
  const db = getDb();
443
- if (isSqliteVecAvailable() && this.vecInitialized) {
542
+ if (this.vecInitialized && this.getVecTableSchema()) {
444
543
  db.prepare("DELETE FROM memory_vec WHERE memory_id = ?").run(id);
445
544
  }
446
545
  const result = db.prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
@@ -450,7 +549,7 @@ export class SqliteMemoryStore implements MemoryStore {
450
549
  deleteBySourcePath(sourcePath: string, agentId: string): number {
451
550
  const db = getDb();
452
551
 
453
- if (isSqliteVecAvailable() && this.vecInitialized) {
552
+ if (this.vecInitialized && this.getVecTableSchema()) {
454
553
  // Get IDs first for vec table cleanup
455
554
  const ids = db
456
555
  .prepare<{ id: string }, [string, string]>(
@@ -472,6 +571,40 @@ export class SqliteMemoryStore implements MemoryStore {
472
571
  return result.changes;
473
572
  }
474
573
 
574
+ purgeExpired(): number {
575
+ const db = getDb();
576
+
577
+ const expiredIds = db
578
+ .prepare<{ id: string }, []>(
579
+ "SELECT id FROM agent_memory WHERE expiresAt IS NOT NULL AND expiresAt <= datetime('now')",
580
+ )
581
+ .all();
582
+
583
+ if (expiredIds.length === 0) return 0;
584
+
585
+ if (this.vecInitialized && this.getVecTableSchema()) {
586
+ const batchSize = 500;
587
+ for (let i = 0; i < expiredIds.length; i += batchSize) {
588
+ const batch = expiredIds.slice(i, i + batchSize);
589
+ const placeholders = batch.map(() => "?").join(",");
590
+ db.prepare(`DELETE FROM memory_vec WHERE memory_id IN (${placeholders})`).run(
591
+ ...batch.map((r) => r.id),
592
+ );
593
+ }
594
+ }
595
+
596
+ const result = db
597
+ .prepare(
598
+ "DELETE FROM agent_memory WHERE expiresAt IS NOT NULL AND expiresAt <= datetime('now')",
599
+ )
600
+ .run();
601
+
602
+ console.log(
603
+ `[memory] Purged ${result.changes} expired memory row(s) (vec cleanup: ${expiredIds.length} id(s))`,
604
+ );
605
+ return result.changes;
606
+ }
607
+
475
608
  updateEmbedding(id: string, embedding: Float32Array, model: string): void {
476
609
  const db = getDb();
477
610
  const buffer = serializeEmbedding(embedding);
@@ -481,11 +614,20 @@ export class SqliteMemoryStore implements MemoryStore {
481
614
  id,
482
615
  );
483
616
 
484
- if (isSqliteVecAvailable() && this.vecInitialized) {
485
- db.prepare("INSERT OR REPLACE INTO memory_vec(memory_id, embedding) VALUES (?, ?)").run(
486
- id,
487
- buffer,
488
- );
617
+ if (this.vecInitialized && this.getVecTableSchema()) {
618
+ const vecBuffer = this.toVecBuffer(embedding);
619
+ if (!vecBuffer) {
620
+ console.warn(
621
+ `[memory-vec] update skipped memory_id=${id} reason=invalid_dimensions dimensions=${embedding.length} expected=${EMBEDDING_DIMENSIONS}`,
622
+ );
623
+ return;
624
+ }
625
+ try {
626
+ db.prepare("DELETE FROM memory_vec WHERE memory_id = ?").run(id);
627
+ db.prepare("INSERT INTO memory_vec(memory_id, embedding) VALUES (?, ?)").run(id, vecBuffer);
628
+ } catch (err) {
629
+ console.error(`[memory-vec] update failed memory_id=${id}: ${(err as Error).message}`);
630
+ }
489
631
  }
490
632
  }
491
633
 
@@ -536,4 +678,79 @@ export class SqliteMemoryStore implements MemoryStore {
536
678
  expired: expired?.count ?? 0,
537
679
  };
538
680
  }
681
+
682
+ private getHealthCounts(): MemoryHealth["counts"] {
683
+ const db = getDb();
684
+ const tableExists = this.getVecTableSchema() !== null;
685
+ const tableUsable = tableExists && isSqliteVecAvailable();
686
+ const count = (sql: string) => db.prepare<{ count: number }, []>(sql).get()?.count ?? 0;
687
+
688
+ return {
689
+ total: count("SELECT COUNT(*) as count FROM agent_memory"),
690
+ withEmbedding: count(
691
+ "SELECT COUNT(*) as count FROM agent_memory WHERE embedding IS NOT NULL",
692
+ ),
693
+ validEmbedding: count(
694
+ `SELECT COUNT(*) as count FROM agent_memory WHERE embedding IS NOT NULL AND length(embedding) = ${VECTOR_BYTES}`,
695
+ ),
696
+ invalidEmbedding: count(
697
+ `SELECT COUNT(*) as count FROM agent_memory WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
698
+ ),
699
+ searchable: count(
700
+ `SELECT COUNT(*) as count FROM agent_memory
701
+ WHERE embedding IS NOT NULL
702
+ AND length(embedding) = ${VECTOR_BYTES}
703
+ AND (expiresAt IS NULL OR expiresAt > datetime('now'))`,
704
+ ),
705
+ memoryVec: tableUsable ? count("SELECT COUNT(*) as count FROM memory_vec") : 0,
706
+ missingFromVec: tableUsable
707
+ ? count(
708
+ `SELECT COUNT(*) as count
709
+ FROM agent_memory m
710
+ LEFT JOIN memory_vec v ON v.memory_id = m.id
711
+ WHERE m.embedding IS NOT NULL
712
+ AND length(m.embedding) = ${VECTOR_BYTES}
713
+ AND v.memory_id IS NULL`,
714
+ )
715
+ : count(
716
+ `SELECT COUNT(*) as count FROM agent_memory WHERE embedding IS NOT NULL AND length(embedding) = ${VECTOR_BYTES}`,
717
+ ),
718
+ extraInVec: tableUsable
719
+ ? count(
720
+ `SELECT COUNT(*) as count
721
+ FROM memory_vec v
722
+ LEFT JOIN agent_memory m ON m.id = v.memory_id
723
+ WHERE m.id IS NULL`,
724
+ )
725
+ : 0,
726
+ };
727
+ }
728
+
729
+ getHealth(): MemoryHealth {
730
+ const schema = this.getVecTableSchema();
731
+ const counts = this.getHealthCounts();
732
+ const reasons: string[] = [];
733
+
734
+ if (!isSqliteVecAvailable()) reasons.push("sqlite_vec_extension_unavailable");
735
+ if (!schema) reasons.push("memory_vec_table_missing");
736
+ if (!this.vecInitialized) reasons.push("memory_vec_not_initialized");
737
+ if (counts.memoryVec === 0) reasons.push("memory_vec_empty");
738
+ if (counts.missingFromVec > 0) reasons.push("memory_vec_missing_embeddings");
739
+ if (counts.extraInVec > 0) reasons.push("memory_vec_extra_rows");
740
+
741
+ return {
742
+ sqliteVec: {
743
+ extensionLoaded: isSqliteVecAvailable(),
744
+ tableExists: schema !== null,
745
+ initialized: this.vecInitialized,
746
+ vectorDimensions: EMBEDDING_DIMENSIONS,
747
+ distanceMetric: "cosine",
748
+ schema,
749
+ lastPopulate: this.lastPopulate,
750
+ },
751
+ counts,
752
+ retrievalMode: reasons.length === 0 ? "vec" : "fallback",
753
+ reasons,
754
+ };
755
+ }
539
756
  }
@@ -25,8 +25,10 @@ export interface MemoryStore {
25
25
  listForReembedding(options?: { agentId?: string }): { id: string; content: string }[];
26
26
  delete(id: string): boolean;
27
27
  deleteBySourcePath(sourcePath: string, agentId: string): number;
28
+ purgeExpired(): number;
28
29
  updateEmbedding(id: string, embedding: Float32Array, model: string): void;
29
30
  getStats(agentId: string): MemoryStats;
31
+ getHealth(): MemoryHealth;
30
32
  }
31
33
 
32
34
  // ============================================================================
@@ -81,6 +83,39 @@ export interface MemoryStats {
81
83
  expired: number;
82
84
  }
83
85
 
86
+ export interface MemoryHealth {
87
+ sqliteVec: {
88
+ extensionLoaded: boolean;
89
+ tableExists: boolean;
90
+ initialized: boolean;
91
+ vectorDimensions: number;
92
+ distanceMetric: "cosine";
93
+ schema: string | null;
94
+ lastPopulate: MemoryVecPopulateStats | null;
95
+ };
96
+ counts: {
97
+ total: number;
98
+ withEmbedding: number;
99
+ validEmbedding: number;
100
+ invalidEmbedding: number;
101
+ searchable: number;
102
+ memoryVec: number;
103
+ missingFromVec: number;
104
+ extraInVec: number;
105
+ };
106
+ retrievalMode: "vec" | "fallback";
107
+ reasons: string[];
108
+ }
109
+
110
+ export interface MemoryVecPopulateStats {
111
+ attempted: number;
112
+ inserted: number;
113
+ skippedInvalidDimensions: number;
114
+ failed: number;
115
+ beforeCount: number;
116
+ afterCount: number;
117
+ }
118
+
84
119
  export interface RerankOptions {
85
120
  limit: number;
86
121
  now?: Date;
@@ -0,0 +1,5 @@
1
+ -- Script Workflows: record real per-step wall-clock duration (ms) measured in the
2
+ -- subprocess, so the dashboard can render a truthful waterfall. Nullable — existing
3
+ -- journal rows stay NULL and are treated as "unmeasured".
4
+
5
+ ALTER TABLE script_run_journal ADD COLUMN durationMs INTEGER;
@@ -0,0 +1,9 @@
1
+ -- Discriminate durable script-workflow runs from inline/one-off `/api/scripts/run`
2
+ -- executions so both can be persisted in the same script_runs table.
3
+ -- Existing rows are all durable workflow runs, hence the 'workflow' default.
4
+
5
+ ALTER TABLE script_runs
6
+ ADD COLUMN kind TEXT NOT NULL DEFAULT 'workflow'
7
+ CHECK(kind IN ('workflow', 'inline'));
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_script_runs_kind ON script_runs(kind);
@@ -0,0 +1,64 @@
1
+ -- Flip the SQL-level default for new page rows from public to authed.
2
+ -- Existing row values are preserved; this only changes behavior when a caller
3
+ -- omits authMode at the database layer.
4
+
5
+ CREATE TABLE pages_new (
6
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
7
+ agentId TEXT NOT NULL,
8
+ slug TEXT NOT NULL,
9
+ title TEXT NOT NULL,
10
+ description TEXT,
11
+ contentType TEXT NOT NULL CHECK (contentType IN ('text/html','application/json')),
12
+ authMode TEXT NOT NULL DEFAULT 'authed' CHECK (authMode IN ('public','authed','password')),
13
+ passwordHash TEXT,
14
+ body TEXT NOT NULL,
15
+ needsCredentials TEXT,
16
+ createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ updatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ view_count INTEGER NOT NULL DEFAULT 0,
19
+ created_by TEXT REFERENCES users(id),
20
+ updated_by TEXT REFERENCES users(id),
21
+ UNIQUE (agentId, slug)
22
+ );
23
+
24
+ INSERT INTO pages_new (
25
+ id,
26
+ agentId,
27
+ slug,
28
+ title,
29
+ description,
30
+ contentType,
31
+ authMode,
32
+ passwordHash,
33
+ body,
34
+ needsCredentials,
35
+ createdAt,
36
+ updatedAt,
37
+ view_count,
38
+ created_by,
39
+ updated_by
40
+ )
41
+ SELECT
42
+ id,
43
+ agentId,
44
+ slug,
45
+ title,
46
+ description,
47
+ contentType,
48
+ authMode,
49
+ passwordHash,
50
+ body,
51
+ needsCredentials,
52
+ createdAt,
53
+ updatedAt,
54
+ view_count,
55
+ created_by,
56
+ updated_by
57
+ FROM pages;
58
+
59
+ DROP TABLE pages;
60
+ ALTER TABLE pages_new RENAME TO pages;
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_pages_agentId ON pages(agentId);
63
+ CREATE INDEX IF NOT EXISTS idx_pages_updatedAt ON pages(updatedAt DESC);
64
+ CREATE INDEX IF NOT EXISTS idx_pages_created_by ON pages(created_by) WHERE created_by IS NOT NULL;
@@ -0,0 +1,19 @@
1
+ -- Migration 087: skill_files table for complex (multi-file) skills.
2
+ -- Additive only: simple skills have zero rows and existing behavior is unchanged.
3
+
4
+ CREATE TABLE IF NOT EXISTS skill_files (
5
+ id TEXT PRIMARY KEY,
6
+ skillId TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
7
+ path TEXT NOT NULL,
8
+ content TEXT NOT NULL,
9
+ mimeType TEXT NOT NULL DEFAULT 'text/plain',
10
+ isBinary INTEGER NOT NULL DEFAULT 0,
11
+ size INTEGER,
12
+ createdAt TEXT NOT NULL,
13
+ lastUpdatedAt TEXT NOT NULL,
14
+ created_by TEXT REFERENCES users(id),
15
+ updated_by TEXT REFERENCES users(id),
16
+ UNIQUE(skillId, path)
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_skill_files_skill ON skill_files(skillId);