@desplega.ai/agent-swarm 1.92.0 → 1.92.2
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 +416 -20
- package/src/be/memory/boot-reembed.ts +85 -0
- package/src/be/memory/constants.ts +44 -2
- package/src/be/memory/providers/openai-embedding.ts +15 -5
- package/src/be/memory/providers/sqlite-store.ts +325 -76
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +43 -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 +5622 -2543
- 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 +465 -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 +50 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +62 -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/tasks/worker-follow-up.ts +12 -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-e2e.test.ts +6 -6
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-rater-e2e.test.ts +4 -5
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/memory.test.ts +13 -12
- 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 +328 -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/tests/task-cascade-fail.test.ts +304 -0
- 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/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
package/src/be/db.ts
CHANGED
|
@@ -64,6 +64,7 @@ import type {
|
|
|
64
64
|
ScheduledTaskSummary,
|
|
65
65
|
ScriptRun,
|
|
66
66
|
ScriptRunJournalEntry,
|
|
67
|
+
ScriptRunKind,
|
|
67
68
|
ScriptRunStatus,
|
|
68
69
|
Service,
|
|
69
70
|
ServiceStatus,
|
|
@@ -71,6 +72,7 @@ import type {
|
|
|
71
72
|
SessionCostSource,
|
|
72
73
|
SessionLog,
|
|
73
74
|
Skill,
|
|
75
|
+
SkillFile,
|
|
74
76
|
SkillScope,
|
|
75
77
|
SkillType,
|
|
76
78
|
SkillWithInstallInfo,
|
|
@@ -113,6 +115,26 @@ export function isSqliteVecAvailable(): boolean {
|
|
|
113
115
|
return sqliteVecAvailable;
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
function loadSqliteVec(database: Database): void {
|
|
119
|
+
sqliteVecAvailable = false;
|
|
120
|
+
try {
|
|
121
|
+
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
|
|
122
|
+
if (extensionPath) {
|
|
123
|
+
database.loadExtension(extensionPath);
|
|
124
|
+
} else {
|
|
125
|
+
const sqliteVec = require("sqlite-vec");
|
|
126
|
+
sqliteVec.load(database);
|
|
127
|
+
}
|
|
128
|
+
sqliteVecAvailable = true;
|
|
129
|
+
console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(
|
|
132
|
+
"[db] sqlite-vec not available, falling back to in-memory cosine:",
|
|
133
|
+
(err as Error).message,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
117
139
|
if (db) {
|
|
118
140
|
return db;
|
|
@@ -129,6 +151,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
129
151
|
db = Database.deserialize(templateBytes);
|
|
130
152
|
db.run("PRAGMA busy_timeout = 5000;");
|
|
131
153
|
db.run("PRAGMA foreign_keys = ON;");
|
|
154
|
+
loadSqliteVec(db);
|
|
132
155
|
configureDbResolver(resolvePromptTemplate);
|
|
133
156
|
// Ensure the encryption key is resolved even when restoring from the test
|
|
134
157
|
// template. The cache may have been cleared via __resetEncryptionKeyForTests
|
|
@@ -154,22 +177,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
154
177
|
// `require.resolve("sqlite-vec-<platform>/vec0.so")` can't find the native
|
|
155
178
|
// asset — so we prefer an explicit filesystem path when set, and only fall
|
|
156
179
|
// back to the npm resolver for normal dev runs.
|
|
157
|
-
|
|
158
|
-
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
|
|
159
|
-
if (extensionPath) {
|
|
160
|
-
database.loadExtension(extensionPath);
|
|
161
|
-
} else {
|
|
162
|
-
const sqliteVec = require("sqlite-vec");
|
|
163
|
-
sqliteVec.load(database);
|
|
164
|
-
}
|
|
165
|
-
sqliteVecAvailable = true;
|
|
166
|
-
console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.warn(
|
|
169
|
-
"[db] sqlite-vec not available, falling back to in-memory cosine:",
|
|
170
|
-
(err as Error).message,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
180
|
+
loadSqliteVec(database);
|
|
173
181
|
|
|
174
182
|
// Run database migrations (schema creation + incremental changes)
|
|
175
183
|
runMigrations(database);
|
|
@@ -347,6 +355,7 @@ export function closeDb(): void {
|
|
|
347
355
|
db.close();
|
|
348
356
|
db = null;
|
|
349
357
|
}
|
|
358
|
+
sqliteVecAvailable = false;
|
|
350
359
|
}
|
|
351
360
|
|
|
352
361
|
// ============================================================================
|
|
@@ -2108,6 +2117,14 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
2108
2117
|
});
|
|
2109
2118
|
});
|
|
2110
2119
|
} catch {}
|
|
2120
|
+
|
|
2121
|
+
// Cascade-fail any non-terminal tasks that depend on this one.
|
|
2122
|
+
// The cascade is recursive (transitive closure) and cycle-safe.
|
|
2123
|
+
try {
|
|
2124
|
+
cascadeFailDependents(id, "failed");
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
console.error("[failTask] cascade-fail dependents error:", err);
|
|
2127
|
+
}
|
|
2111
2128
|
}
|
|
2112
2129
|
return row ? rowToAgentTask(row) : null;
|
|
2113
2130
|
}
|
|
@@ -2146,6 +2163,12 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2146
2163
|
});
|
|
2147
2164
|
});
|
|
2148
2165
|
} catch {}
|
|
2166
|
+
|
|
2167
|
+
try {
|
|
2168
|
+
cascadeFailDependents(id, "cancelled");
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
console.error("[cancelTask] cascade-fail dependents error:", err);
|
|
2171
|
+
}
|
|
2149
2172
|
}
|
|
2150
2173
|
|
|
2151
2174
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -2209,6 +2232,12 @@ export function supersedeTask(
|
|
|
2209
2232
|
});
|
|
2210
2233
|
});
|
|
2211
2234
|
} catch {}
|
|
2235
|
+
|
|
2236
|
+
try {
|
|
2237
|
+
cascadeFailDependents(id, "superseded");
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
console.error("[supersedeTask] cascade-fail dependents error:", err);
|
|
2240
|
+
}
|
|
2212
2241
|
}
|
|
2213
2242
|
|
|
2214
2243
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -3381,6 +3410,75 @@ export function checkDependencies(taskId: string): {
|
|
|
3381
3410
|
return { ready: blockedBy.length === 0, blockedBy };
|
|
3382
3411
|
}
|
|
3383
3412
|
|
|
3413
|
+
/**
|
|
3414
|
+
* Reverse-lookup: find all tasks whose `dependsOn` JSON array contains `parentId`.
|
|
3415
|
+
* Uses SQLite `json_each` to scan the dependsOn column efficiently.
|
|
3416
|
+
* Returns only non-terminal tasks by default (the callers want to cascade-fail
|
|
3417
|
+
* live dependents, not re-process already-finished ones).
|
|
3418
|
+
*/
|
|
3419
|
+
export function getDependentTasks(
|
|
3420
|
+
parentId: string,
|
|
3421
|
+
opts?: { includeTerminal?: boolean },
|
|
3422
|
+
): AgentTask[] {
|
|
3423
|
+
const database = getDb();
|
|
3424
|
+
const rows = database
|
|
3425
|
+
.prepare<AgentTaskRow, [string]>(
|
|
3426
|
+
`SELECT t.*
|
|
3427
|
+
FROM agent_tasks t, json_each(t.dependsOn) AS dep
|
|
3428
|
+
WHERE dep.value = ?`,
|
|
3429
|
+
)
|
|
3430
|
+
.all(parentId);
|
|
3431
|
+
|
|
3432
|
+
const tasks = rows.map(rowToAgentTask);
|
|
3433
|
+
if (opts?.includeTerminal) return tasks;
|
|
3434
|
+
return tasks.filter((t) => !isTerminalTaskStatus(t.status));
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
export interface CascadeFailResult {
|
|
3438
|
+
taskId: string;
|
|
3439
|
+
taskSubject: string;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
/**
|
|
3443
|
+
* Recursively cascade-fail all transitive dependents of a parent task.
|
|
3444
|
+
* Walks the full dependency graph: if A fails, and B depends on A, and C
|
|
3445
|
+
* depends on B, then both B and C are failed.
|
|
3446
|
+
*
|
|
3447
|
+
* Guards against cycles with a visited set. Skips already-terminal tasks.
|
|
3448
|
+
* Returns the list of tasks that were actually cascade-failed (for follow-up
|
|
3449
|
+
* enrichment).
|
|
3450
|
+
*/
|
|
3451
|
+
export function cascadeFailDependents(
|
|
3452
|
+
parentId: string,
|
|
3453
|
+
parentStatus: string,
|
|
3454
|
+
visited?: Set<string>,
|
|
3455
|
+
): CascadeFailResult[] {
|
|
3456
|
+
const seen = visited ?? new Set<string>();
|
|
3457
|
+
if (seen.has(parentId)) return [];
|
|
3458
|
+
seen.add(parentId);
|
|
3459
|
+
|
|
3460
|
+
const dependents = getDependentTasks(parentId);
|
|
3461
|
+
const results: CascadeFailResult[] = [];
|
|
3462
|
+
|
|
3463
|
+
for (const dep of dependents) {
|
|
3464
|
+
if (seen.has(dep.id)) continue;
|
|
3465
|
+
|
|
3466
|
+
const reason = `Blocked dependency ${parentId.slice(0, 8)} was ${parentStatus}`;
|
|
3467
|
+
const failed = failTask(dep.id, reason);
|
|
3468
|
+
if (failed) {
|
|
3469
|
+
results.push({
|
|
3470
|
+
taskId: failed.id,
|
|
3471
|
+
taskSubject: failed.task.slice(0, 120),
|
|
3472
|
+
});
|
|
3473
|
+
// Recurse: this dependent may itself have dependents
|
|
3474
|
+
const transitive = cascadeFailDependents(dep.id, "failed (cascade)", seen);
|
|
3475
|
+
results.push(...transitive);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
return results;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3384
3482
|
// ============================================================================
|
|
3385
3483
|
// Agent Profile Operations
|
|
3386
3484
|
// ============================================================================
|
|
@@ -7091,7 +7189,7 @@ export function createPage(data: {
|
|
|
7091
7189
|
title: string;
|
|
7092
7190
|
description?: string;
|
|
7093
7191
|
contentType: PageContentType;
|
|
7094
|
-
authMode
|
|
7192
|
+
authMode?: PageAuthMode;
|
|
7095
7193
|
passwordHash?: string;
|
|
7096
7194
|
body: string;
|
|
7097
7195
|
needsCredentials?: string[];
|
|
@@ -7110,7 +7208,7 @@ export function createPage(data: {
|
|
|
7110
7208
|
data.title,
|
|
7111
7209
|
data.description ?? null,
|
|
7112
7210
|
data.contentType,
|
|
7113
|
-
data.authMode,
|
|
7211
|
+
data.authMode ?? "authed",
|
|
7114
7212
|
data.passwordHash ?? null,
|
|
7115
7213
|
data.body,
|
|
7116
7214
|
data.needsCredentials ? JSON.stringify(data.needsCredentials) : null,
|
|
@@ -8692,6 +8790,119 @@ function rowToSkillWithInstall(row: SkillWithInstallRow): SkillWithInstallInfo {
|
|
|
8692
8790
|
};
|
|
8693
8791
|
}
|
|
8694
8792
|
|
|
8793
|
+
type SkillFileRow = {
|
|
8794
|
+
id: string;
|
|
8795
|
+
skillId: string;
|
|
8796
|
+
path: string;
|
|
8797
|
+
content: string;
|
|
8798
|
+
mimeType: string;
|
|
8799
|
+
isBinary: number;
|
|
8800
|
+
size: number | null;
|
|
8801
|
+
createdAt: string;
|
|
8802
|
+
lastUpdatedAt: string;
|
|
8803
|
+
};
|
|
8804
|
+
|
|
8805
|
+
function rowToSkillFile(row: SkillFileRow): SkillFile {
|
|
8806
|
+
return {
|
|
8807
|
+
id: row.id,
|
|
8808
|
+
skillId: row.skillId,
|
|
8809
|
+
path: row.path,
|
|
8810
|
+
content: row.content,
|
|
8811
|
+
mimeType: row.mimeType,
|
|
8812
|
+
isBinary: row.isBinary === 1,
|
|
8813
|
+
size: row.size,
|
|
8814
|
+
createdAt: row.createdAt,
|
|
8815
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
8816
|
+
};
|
|
8817
|
+
}
|
|
8818
|
+
|
|
8819
|
+
export type SkillFileInput = {
|
|
8820
|
+
path: string;
|
|
8821
|
+
content: string;
|
|
8822
|
+
mimeType?: string;
|
|
8823
|
+
isBinary?: boolean;
|
|
8824
|
+
size?: number | null;
|
|
8825
|
+
};
|
|
8826
|
+
|
|
8827
|
+
export type SkillFileManifestEntry = Omit<SkillFile, "content">;
|
|
8828
|
+
type NormalizedSkillFileInput = {
|
|
8829
|
+
path: string;
|
|
8830
|
+
content: string;
|
|
8831
|
+
mimeType: string;
|
|
8832
|
+
isBinary: boolean;
|
|
8833
|
+
size: number;
|
|
8834
|
+
};
|
|
8835
|
+
|
|
8836
|
+
export const SKILL_FILE_LIMITS = {
|
|
8837
|
+
maxCount: Number(process.env.SKILL_FILES_MAX_COUNT ?? 100),
|
|
8838
|
+
maxTotalBytes: Number(process.env.SKILL_FILES_MAX_TOTAL_BYTES ?? 10 * 1024 * 1024),
|
|
8839
|
+
maxFileBytes: Number(process.env.SKILL_FILES_MAX_FILE_BYTES ?? 500 * 1024),
|
|
8840
|
+
};
|
|
8841
|
+
|
|
8842
|
+
const BINARY_SKILL_FILE_PLACEHOLDER = "[binary file - not synced]";
|
|
8843
|
+
|
|
8844
|
+
export function normalizeSkillFilePath(path: string): string {
|
|
8845
|
+
const raw = path.trim().replace(/\\/g, "/");
|
|
8846
|
+
if (!raw) throw new Error("File path is required");
|
|
8847
|
+
if (raw.startsWith("/")) throw new Error("File path must be relative");
|
|
8848
|
+
|
|
8849
|
+
const parts = raw.split("/").filter(Boolean);
|
|
8850
|
+
if (parts.length === 0) throw new Error("File path is required");
|
|
8851
|
+
if (parts.some((part) => part === "." || part === "..")) {
|
|
8852
|
+
throw new Error("File path cannot contain traversal segments");
|
|
8853
|
+
}
|
|
8854
|
+
|
|
8855
|
+
const normalized = parts.join("/");
|
|
8856
|
+
if (normalized === "SKILL.md") {
|
|
8857
|
+
throw new Error("SKILL.md is stored on the skill record, not in skill_files");
|
|
8858
|
+
}
|
|
8859
|
+
return normalized;
|
|
8860
|
+
}
|
|
8861
|
+
|
|
8862
|
+
function byteSize(content: string): number {
|
|
8863
|
+
return Buffer.byteLength(content, "utf8");
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8866
|
+
function normalizeSkillFileInput(input: SkillFileInput): NormalizedSkillFileInput {
|
|
8867
|
+
const path = normalizeSkillFilePath(input.path);
|
|
8868
|
+
const isBinary = input.isBinary === true;
|
|
8869
|
+
const content = isBinary ? input.content || BINARY_SKILL_FILE_PLACEHOLDER : input.content;
|
|
8870
|
+
const size = input.size ?? byteSize(content);
|
|
8871
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
8872
|
+
throw new Error("File size must be a non-negative number");
|
|
8873
|
+
}
|
|
8874
|
+
if (size > SKILL_FILE_LIMITS.maxFileBytes) {
|
|
8875
|
+
throw new Error(`File ${path} exceeds max size ${SKILL_FILE_LIMITS.maxFileBytes}`);
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8878
|
+
return {
|
|
8879
|
+
path,
|
|
8880
|
+
content,
|
|
8881
|
+
mimeType: input.mimeType ?? "text/plain",
|
|
8882
|
+
isBinary,
|
|
8883
|
+
size,
|
|
8884
|
+
};
|
|
8885
|
+
}
|
|
8886
|
+
|
|
8887
|
+
function assertSkillFileLimits(skillId: string, incoming: SkillFileInput[], replaceAll: boolean) {
|
|
8888
|
+
const existing = replaceAll ? [] : listSkillFileManifest(skillId);
|
|
8889
|
+
const byPath = new Map(existing.map((file) => [file.path, file.size ?? 0]));
|
|
8890
|
+
|
|
8891
|
+
for (const input of incoming) {
|
|
8892
|
+
const normalized = normalizeSkillFileInput(input);
|
|
8893
|
+
byPath.set(normalized.path, normalized.size);
|
|
8894
|
+
}
|
|
8895
|
+
|
|
8896
|
+
if (byPath.size > SKILL_FILE_LIMITS.maxCount) {
|
|
8897
|
+
throw new Error(`Skill file count exceeds max ${SKILL_FILE_LIMITS.maxCount}`);
|
|
8898
|
+
}
|
|
8899
|
+
|
|
8900
|
+
const total = [...byPath.values()].reduce((sum, size) => sum + size, 0);
|
|
8901
|
+
if (total > SKILL_FILE_LIMITS.maxTotalBytes) {
|
|
8902
|
+
throw new Error(`Skill files exceed max total size ${SKILL_FILE_LIMITS.maxTotalBytes}`);
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8695
8906
|
export interface SkillInsert {
|
|
8696
8907
|
name: string;
|
|
8697
8908
|
description: string;
|
|
@@ -8865,6 +9076,124 @@ export function updateSkill(
|
|
|
8865
9076
|
return row ? rowToSkill(row) : null;
|
|
8866
9077
|
}
|
|
8867
9078
|
|
|
9079
|
+
function bumpSkillVersion(skillId: string, now = new Date().toISOString()) {
|
|
9080
|
+
getDb()
|
|
9081
|
+
.prepare("UPDATE skills SET version = version + 1, lastUpdatedAt = ? WHERE id = ?")
|
|
9082
|
+
.run(now, skillId);
|
|
9083
|
+
}
|
|
9084
|
+
|
|
9085
|
+
export function listSkillFileManifest(skillId: string): SkillFileManifestEntry[] {
|
|
9086
|
+
return getDb()
|
|
9087
|
+
.prepare<SkillFileRow, [string]>(
|
|
9088
|
+
`SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
|
|
9089
|
+
FROM skill_files
|
|
9090
|
+
WHERE skillId = ?
|
|
9091
|
+
ORDER BY path ASC`,
|
|
9092
|
+
)
|
|
9093
|
+
.all(skillId)
|
|
9094
|
+
.map((row) => {
|
|
9095
|
+
const { content: _content, ...manifest } = rowToSkillFile(row);
|
|
9096
|
+
return manifest;
|
|
9097
|
+
});
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9100
|
+
export function getSkillFiles(skillId: string): SkillFile[] {
|
|
9101
|
+
return getDb()
|
|
9102
|
+
.prepare<SkillFileRow, [string]>(
|
|
9103
|
+
`SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
|
|
9104
|
+
FROM skill_files
|
|
9105
|
+
WHERE skillId = ?
|
|
9106
|
+
ORDER BY path ASC`,
|
|
9107
|
+
)
|
|
9108
|
+
.all(skillId)
|
|
9109
|
+
.map(rowToSkillFile);
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9112
|
+
export function getSkillFile(skillId: string, path: string): SkillFile | null {
|
|
9113
|
+
const normalizedPath = normalizeSkillFilePath(path);
|
|
9114
|
+
const row = getDb()
|
|
9115
|
+
.prepare<SkillFileRow, [string, string]>(
|
|
9116
|
+
`SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
|
|
9117
|
+
FROM skill_files
|
|
9118
|
+
WHERE skillId = ? AND path = ?`,
|
|
9119
|
+
)
|
|
9120
|
+
.get(skillId, normalizedPath);
|
|
9121
|
+
return row ? rowToSkillFile(row) : null;
|
|
9122
|
+
}
|
|
9123
|
+
|
|
9124
|
+
export function upsertSkillFile(skillId: string, input: SkillFileInput): SkillFile {
|
|
9125
|
+
const payload = normalizeSkillFileInput(input);
|
|
9126
|
+
assertSkillFileLimits(skillId, [payload], false);
|
|
9127
|
+
|
|
9128
|
+
const id = crypto.randomUUID();
|
|
9129
|
+
const now = new Date().toISOString();
|
|
9130
|
+
return upsertSkillFileUnchecked(skillId, payload, id, now, true);
|
|
9131
|
+
}
|
|
9132
|
+
|
|
9133
|
+
function upsertSkillFileUnchecked(
|
|
9134
|
+
skillId: string,
|
|
9135
|
+
payload: NormalizedSkillFileInput,
|
|
9136
|
+
id: string,
|
|
9137
|
+
now: string,
|
|
9138
|
+
bumpVersion: boolean,
|
|
9139
|
+
): SkillFile {
|
|
9140
|
+
const row = getDb()
|
|
9141
|
+
.prepare<SkillFileRow, (string | number | null)[]>(
|
|
9142
|
+
`INSERT INTO skill_files (
|
|
9143
|
+
id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
|
|
9144
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9145
|
+
ON CONFLICT(skillId, path) DO UPDATE SET
|
|
9146
|
+
content = excluded.content,
|
|
9147
|
+
mimeType = excluded.mimeType,
|
|
9148
|
+
isBinary = excluded.isBinary,
|
|
9149
|
+
size = excluded.size,
|
|
9150
|
+
lastUpdatedAt = excluded.lastUpdatedAt
|
|
9151
|
+
RETURNING *`,
|
|
9152
|
+
)
|
|
9153
|
+
.get(
|
|
9154
|
+
id,
|
|
9155
|
+
skillId,
|
|
9156
|
+
payload.path,
|
|
9157
|
+
payload.content,
|
|
9158
|
+
payload.mimeType,
|
|
9159
|
+
payload.isBinary ? 1 : 0,
|
|
9160
|
+
payload.size,
|
|
9161
|
+
now,
|
|
9162
|
+
now,
|
|
9163
|
+
);
|
|
9164
|
+
|
|
9165
|
+
if (!row) throw new Error("Failed to upsert skill file");
|
|
9166
|
+
if (bumpVersion) bumpSkillVersion(skillId, now);
|
|
9167
|
+
return rowToSkillFile(row);
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
export function upsertSkillFiles(skillId: string, files: SkillFileInput[]): SkillFile[] {
|
|
9171
|
+
if (files.length === 0) return [];
|
|
9172
|
+
const normalized = files.map(normalizeSkillFileInput);
|
|
9173
|
+
assertSkillFileLimits(skillId, normalized, false);
|
|
9174
|
+
|
|
9175
|
+
const now = new Date().toISOString();
|
|
9176
|
+
return getDb().transaction(() => {
|
|
9177
|
+
const rows = normalized.map((file) =>
|
|
9178
|
+
upsertSkillFileUnchecked(skillId, file, crypto.randomUUID(), now, false),
|
|
9179
|
+
);
|
|
9180
|
+
bumpSkillVersion(skillId, now);
|
|
9181
|
+
return rows;
|
|
9182
|
+
})();
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
export function deleteSkillFile(skillId: string, path: string): boolean {
|
|
9186
|
+
const normalizedPath = normalizeSkillFilePath(path);
|
|
9187
|
+
const result = getDb()
|
|
9188
|
+
.prepare("DELETE FROM skill_files WHERE skillId = ? AND path = ?")
|
|
9189
|
+
.run(skillId, normalizedPath);
|
|
9190
|
+
if (result.changes > 0) {
|
|
9191
|
+
bumpSkillVersion(skillId);
|
|
9192
|
+
return true;
|
|
9193
|
+
}
|
|
9194
|
+
return false;
|
|
9195
|
+
}
|
|
9196
|
+
|
|
8868
9197
|
export function deleteSkill(id: string): boolean {
|
|
8869
9198
|
const result = getDb().prepare("DELETE FROM skills WHERE id = ?").run(id);
|
|
8870
9199
|
return result.changes > 0;
|
|
@@ -11231,6 +11560,7 @@ type ScriptRunRow = {
|
|
|
11231
11560
|
scriptName: string | null;
|
|
11232
11561
|
source: string;
|
|
11233
11562
|
args: string;
|
|
11563
|
+
kind: string;
|
|
11234
11564
|
status: string;
|
|
11235
11565
|
pid: number | null;
|
|
11236
11566
|
startedAt: string;
|
|
@@ -11256,6 +11586,7 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
|
|
|
11256
11586
|
scriptName: row.scriptName ?? undefined,
|
|
11257
11587
|
source: row.source,
|
|
11258
11588
|
args: JSON.parse(row.args),
|
|
11589
|
+
kind: row.kind as ScriptRunKind,
|
|
11259
11590
|
status: row.status as ScriptRunStatus,
|
|
11260
11591
|
pid: row.pid ?? undefined,
|
|
11261
11592
|
startedAt: row.startedAt,
|
|
@@ -11322,6 +11653,67 @@ export function createScriptRun(data: {
|
|
|
11322
11653
|
return { run: rowToScriptRun(row), existing: false };
|
|
11323
11654
|
}
|
|
11324
11655
|
|
|
11656
|
+
// Persist a synchronous inline run (POST /api/scripts/run) as an already-terminal
|
|
11657
|
+
// row. Unlike createScriptRun these never get a journal and never use the
|
|
11658
|
+
// idempotencyKey column (inline idempotency lives in the kv table).
|
|
11659
|
+
export function recordInlineScriptRun(data: {
|
|
11660
|
+
id: string;
|
|
11661
|
+
agentId: string;
|
|
11662
|
+
source: string;
|
|
11663
|
+
args: unknown;
|
|
11664
|
+
scriptName?: string;
|
|
11665
|
+
status: "completed" | "failed";
|
|
11666
|
+
output?: unknown;
|
|
11667
|
+
error?: string;
|
|
11668
|
+
startedAt: string;
|
|
11669
|
+
finishedAt: string;
|
|
11670
|
+
requestedByUserId?: string;
|
|
11671
|
+
createdBy?: string;
|
|
11672
|
+
}): ScriptRun {
|
|
11673
|
+
const row = getDb()
|
|
11674
|
+
.prepare<
|
|
11675
|
+
ScriptRunRow,
|
|
11676
|
+
[
|
|
11677
|
+
string,
|
|
11678
|
+
string,
|
|
11679
|
+
string | null,
|
|
11680
|
+
string,
|
|
11681
|
+
string,
|
|
11682
|
+
string,
|
|
11683
|
+
string | null,
|
|
11684
|
+
string | null,
|
|
11685
|
+
string,
|
|
11686
|
+
string,
|
|
11687
|
+
string | null,
|
|
11688
|
+
string | null,
|
|
11689
|
+
string | null,
|
|
11690
|
+
]
|
|
11691
|
+
>(
|
|
11692
|
+
`INSERT INTO script_runs
|
|
11693
|
+
(id, agentId, scriptName, source, args, kind, status, output, error,
|
|
11694
|
+
startedAt, finishedAt, requestedByUserId, created_by, updated_by)
|
|
11695
|
+
VALUES (?, ?, ?, ?, ?, 'inline', ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11696
|
+
RETURNING *`,
|
|
11697
|
+
)
|
|
11698
|
+
.get(
|
|
11699
|
+
data.id,
|
|
11700
|
+
data.agentId,
|
|
11701
|
+
data.scriptName ?? null,
|
|
11702
|
+
data.source,
|
|
11703
|
+
JSON.stringify(data.args ?? null),
|
|
11704
|
+
data.status,
|
|
11705
|
+
data.output === undefined ? null : JSON.stringify(data.output),
|
|
11706
|
+
data.error ?? null,
|
|
11707
|
+
data.startedAt,
|
|
11708
|
+
data.finishedAt,
|
|
11709
|
+
data.requestedByUserId ?? null,
|
|
11710
|
+
data.createdBy ?? null,
|
|
11711
|
+
data.createdBy ?? null,
|
|
11712
|
+
);
|
|
11713
|
+
if (!row) throw new Error("Failed to record inline script run");
|
|
11714
|
+
return rowToScriptRun(row);
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11325
11717
|
export function getScriptRun(id: string): ScriptRun | null {
|
|
11326
11718
|
const row = getDb()
|
|
11327
11719
|
.prepare<ScriptRunRow, [string]>("SELECT * FROM script_runs WHERE id = ?")
|
|
@@ -11459,6 +11851,7 @@ type ScriptRunJournalRow = {
|
|
|
11459
11851
|
error: string | null;
|
|
11460
11852
|
startedAt: string;
|
|
11461
11853
|
completedAt: string | null;
|
|
11854
|
+
durationMs: number | null;
|
|
11462
11855
|
created_by: string | null;
|
|
11463
11856
|
updated_by: string | null;
|
|
11464
11857
|
};
|
|
@@ -11475,6 +11868,7 @@ function rowToScriptRunJournalEntry(row: ScriptRunJournalRow): ScriptRunJournalE
|
|
|
11475
11868
|
error: row.error ?? undefined,
|
|
11476
11869
|
startedAt: row.startedAt,
|
|
11477
11870
|
completedAt: row.completedAt ?? undefined,
|
|
11871
|
+
durationMs: row.durationMs ?? undefined,
|
|
11478
11872
|
};
|
|
11479
11873
|
}
|
|
11480
11874
|
|
|
@@ -11498,11 +11892,12 @@ export function upsertScriptRunJournalStep(data: {
|
|
|
11498
11892
|
status: "completed" | "failed";
|
|
11499
11893
|
result?: unknown;
|
|
11500
11894
|
error?: string;
|
|
11895
|
+
durationMs?: number;
|
|
11501
11896
|
}): void {
|
|
11502
11897
|
getDb().run(
|
|
11503
11898
|
`INSERT OR IGNORE INTO script_run_journal
|
|
11504
|
-
(id, runId, stepKey, stepType, config, status, result, error, completedAt)
|
|
11505
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
11899
|
+
(id, runId, stepKey, stepType, config, status, result, error, durationMs, completedAt)
|
|
11900
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
11506
11901
|
[
|
|
11507
11902
|
crypto.randomUUID(),
|
|
11508
11903
|
data.runId,
|
|
@@ -11512,6 +11907,7 @@ export function upsertScriptRunJournalStep(data: {
|
|
|
11512
11907
|
data.status,
|
|
11513
11908
|
data.result !== undefined ? JSON.stringify(data.result) : null,
|
|
11514
11909
|
data.error ?? null,
|
|
11910
|
+
data.durationMs ?? null,
|
|
11515
11911
|
],
|
|
11516
11912
|
);
|
|
11517
11913
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup backfill: detect agent_memory rows with wrong-dimension embeddings
|
|
3
|
+
* (not 512d) and re-embed them in the background. Runs once per boot,
|
|
4
|
+
* async/non-blocking, idempotent, no-op when the DB is clean.
|
|
5
|
+
*
|
|
6
|
+
* This is the app-level equivalent of a forward-only migration — SQL can't
|
|
7
|
+
* call OpenAI, so the backfill runs at startup instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDb } from "@/be/db";
|
|
11
|
+
import { EMBEDDING_DIMENSIONS } from "./constants";
|
|
12
|
+
import { getEmbeddingProvider, getMemoryStore } from "./index";
|
|
13
|
+
|
|
14
|
+
const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
|
|
15
|
+
const BATCH_SIZE = 20;
|
|
16
|
+
const BACKFILL_KV_KEY = "memory:reembed:backfill_complete";
|
|
17
|
+
|
|
18
|
+
export async function runBootReembed(): Promise<void> {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
|
|
21
|
+
const invalidCount =
|
|
22
|
+
db
|
|
23
|
+
.prepare<{ count: number }, []>(
|
|
24
|
+
`SELECT COUNT(*) as count FROM agent_memory
|
|
25
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
26
|
+
)
|
|
27
|
+
.get()?.count ?? 0;
|
|
28
|
+
|
|
29
|
+
if (invalidCount === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const provider = getEmbeddingProvider();
|
|
34
|
+
const testEmbed = await provider.embed("test");
|
|
35
|
+
if (!testEmbed) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[boot-reembed] skipped: ${invalidCount} wrong-dimension rows found but no OpenAI key configured`,
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`[boot-reembed] starting: ${invalidCount} rows with wrong embedding dimensions`);
|
|
43
|
+
|
|
44
|
+
const store = getMemoryStore();
|
|
45
|
+
const rows = db
|
|
46
|
+
.prepare<{ id: string; content: string }, []>(
|
|
47
|
+
`SELECT id, content FROM agent_memory
|
|
48
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
49
|
+
)
|
|
50
|
+
.all();
|
|
51
|
+
|
|
52
|
+
let reembedded = 0;
|
|
53
|
+
let failed = 0;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
56
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
57
|
+
try {
|
|
58
|
+
const embeddings = await provider.embedBatch(batch.map((m) => m.content));
|
|
59
|
+
for (let j = 0; j < embeddings.length; j++) {
|
|
60
|
+
if (embeddings[j]) {
|
|
61
|
+
store.updateEmbedding(batch[j]!.id, embeddings[j]!, provider.name);
|
|
62
|
+
reembedded++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
failed += batch.length;
|
|
67
|
+
console.error(
|
|
68
|
+
`[boot-reembed] batch ${Math.floor(i / BATCH_SIZE) + 1} failed:`,
|
|
69
|
+
(err as Error).message,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const afterInvalid =
|
|
75
|
+
db
|
|
76
|
+
.prepare<{ count: number }, []>(
|
|
77
|
+
`SELECT COUNT(*) as count FROM agent_memory
|
|
78
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
79
|
+
)
|
|
80
|
+
.get()?.count ?? 0;
|
|
81
|
+
|
|
82
|
+
console.log(
|
|
83
|
+
`[boot-reembed] complete: reembedded=${reembedded} failed=${failed} remaining_invalid=${afterInvalid}`,
|
|
84
|
+
);
|
|
85
|
+
}
|