@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.
- package/README.md +1 -1
- package/openapi.json +276 -3
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +327 -20
- package/src/be/memory/constants.ts +2 -1
- package/src/be/memory/providers/openai-embedding.ts +2 -5
- package/src/be/memory/providers/sqlite-store.ts +293 -76
- package/src/be/memory/types.ts +35 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +264 -328
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +94 -0
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/index.ts +32 -4
- package/src/be/seed-skills/index.ts +0 -7
- package/src/be/skill-sync.ts +91 -7
- package/src/commands/runner.ts +6 -2
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/index.ts +41 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +58 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +2 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/providers/claude-adapter.ts +56 -24
- package/src/script-workflows/workflow-ctx.ts +7 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tests/claude-adapter-binary.test.ts +135 -81
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/scripts-mcp-e2e.test.ts +53 -0
- package/src/tests/seed-scripts.test.ts +123 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tools/create-page.ts +2 -2
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +20 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- 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
|
|
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[
|
|
114
|
+
embedding float[${EMBEDDING_DIMENSIONS}] distance_metric=cosine
|
|
94
115
|
)
|
|
95
116
|
`);
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
263
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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 (
|
|
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 (
|
|
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 (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
}
|
package/src/be/memory/types.ts
CHANGED
|
@@ -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);
|