@desplega.ai/agent-swarm 1.92.1 → 1.93.0
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/openapi.json +63 -3
- package/package.json +5 -5
- package/src/be/db.ts +180 -6
- package/src/be/memory/boot-reembed.ts +84 -0
- package/src/be/memory/constants.ts +42 -1
- package/src/be/memory/providers/openai-embedding.ts +13 -0
- package/src/be/memory/providers/sqlite-store.ts +75 -26
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +11 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- package/src/be/modelsdev-cache.json +6478 -3099
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +371 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +5 -5
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +124 -7
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +9 -0
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +27 -24
- package/src/http/tasks.ts +10 -6
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +7 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +11 -4
- package/src/providers/pi-mono-adapter.ts +12 -2
- package/src/providers/types.ts +2 -0
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +2 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/http-api-integration.test.ts +4 -6
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +4 -7
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +64 -12
- package/src/tests/model-control.test.ts +1 -1
- package/src/tests/reload-config.test.ts +33 -17
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +218 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/src/tests/use-dismissible-card.test.ts +0 -4
- package/src/tools/schedules/create-schedule.ts +2 -2
- package/src/tools/schedules/update-schedule.ts +1 -1
- package/src/tools/send-task.ts +2 -2
- package/src/tools/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +2 -2
- package/src/types.ts +11 -0
- package/src/utils/context-window.ts +3 -0
- package/src/utils/credentials.ts +22 -2
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.93.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -2740,6 +2740,59 @@
|
|
|
2740
2740
|
}
|
|
2741
2741
|
}
|
|
2742
2742
|
},
|
|
2743
|
+
"/api/keys/clear-rate-limit": {
|
|
2744
|
+
"post": {
|
|
2745
|
+
"summary": "Clear rate-limited status for a key after a successful use proves it is healthy",
|
|
2746
|
+
"tags": [
|
|
2747
|
+
"API Keys"
|
|
2748
|
+
],
|
|
2749
|
+
"security": [
|
|
2750
|
+
{
|
|
2751
|
+
"bearerAuth": []
|
|
2752
|
+
}
|
|
2753
|
+
],
|
|
2754
|
+
"requestBody": {
|
|
2755
|
+
"content": {
|
|
2756
|
+
"application/json": {
|
|
2757
|
+
"schema": {
|
|
2758
|
+
"type": "object",
|
|
2759
|
+
"properties": {
|
|
2760
|
+
"keyType": {
|
|
2761
|
+
"type": "string"
|
|
2762
|
+
},
|
|
2763
|
+
"keySuffix": {
|
|
2764
|
+
"type": "string",
|
|
2765
|
+
"minLength": 1,
|
|
2766
|
+
"maxLength": 10
|
|
2767
|
+
},
|
|
2768
|
+
"scope": {
|
|
2769
|
+
"type": "string"
|
|
2770
|
+
},
|
|
2771
|
+
"scopeId": {
|
|
2772
|
+
"type": "string"
|
|
2773
|
+
}
|
|
2774
|
+
},
|
|
2775
|
+
"required": [
|
|
2776
|
+
"keyType",
|
|
2777
|
+
"keySuffix"
|
|
2778
|
+
]
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
},
|
|
2783
|
+
"responses": {
|
|
2784
|
+
"200": {
|
|
2785
|
+
"description": "Rate limit cleared (or key was not rate-limited)"
|
|
2786
|
+
},
|
|
2787
|
+
"400": {
|
|
2788
|
+
"description": "Validation error"
|
|
2789
|
+
},
|
|
2790
|
+
"401": {
|
|
2791
|
+
"description": "Unauthorized"
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
},
|
|
2743
2796
|
"/api/events": {
|
|
2744
2797
|
"post": {
|
|
2745
2798
|
"summary": "Store a single event",
|
|
@@ -10711,9 +10764,9 @@
|
|
|
10711
10764
|
}
|
|
10712
10765
|
}
|
|
10713
10766
|
},
|
|
10714
|
-
"/api/tasks/{id}/
|
|
10767
|
+
"/api/tasks/{id}/session": {
|
|
10715
10768
|
"put": {
|
|
10716
|
-
"summary": "Update
|
|
10769
|
+
"summary": "Update provider session ID and harness metadata for a task",
|
|
10717
10770
|
"tags": [
|
|
10718
10771
|
"Tasks"
|
|
10719
10772
|
],
|
|
@@ -10800,6 +10853,13 @@
|
|
|
10800
10853
|
"providerMeta": {
|
|
10801
10854
|
"type": "object",
|
|
10802
10855
|
"properties": {}
|
|
10856
|
+
},
|
|
10857
|
+
"harnessVariant": {
|
|
10858
|
+
"type": "string"
|
|
10859
|
+
},
|
|
10860
|
+
"harnessVariantMeta": {
|
|
10861
|
+
"type": "object",
|
|
10862
|
+
"additionalProperties": {}
|
|
10803
10863
|
}
|
|
10804
10864
|
},
|
|
10805
10865
|
"required": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.93.0",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -112,11 +112,11 @@
|
|
|
112
112
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
113
113
|
"@inkjs/ui": "^2.0.0",
|
|
114
114
|
"@linear/sdk": "^77.0.0",
|
|
115
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
116
|
-
"@earendil-works/pi-ai": "^0.
|
|
117
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
115
|
+
"@earendil-works/pi-agent-core": "^0.79.1",
|
|
116
|
+
"@earendil-works/pi-ai": "^0.79.1",
|
|
117
|
+
"@earendil-works/pi-coding-agent": "^0.79.1",
|
|
118
118
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
119
|
-
"@openai/codex-sdk": "^0.
|
|
119
|
+
"@openai/codex-sdk": "^0.139.0",
|
|
120
120
|
"@opencode-ai/sdk": "^1.16.2",
|
|
121
121
|
"@openfort/openfort-node": "^0.9.1",
|
|
122
122
|
"@opentelemetry/api": "^1.9.1",
|
package/src/be/db.ts
CHANGED
|
@@ -65,6 +65,7 @@ import type {
|
|
|
65
65
|
ScriptRun,
|
|
66
66
|
ScriptRunJournalEntry,
|
|
67
67
|
ScriptRunKind,
|
|
68
|
+
ScriptRunListItem,
|
|
68
69
|
ScriptRunStatus,
|
|
69
70
|
Service,
|
|
70
71
|
ServiceStatus,
|
|
@@ -1041,6 +1042,8 @@ type AgentTaskRow = {
|
|
|
1041
1042
|
swarmVersion: string | null;
|
|
1042
1043
|
provider: string | null;
|
|
1043
1044
|
providerMeta: string | null;
|
|
1045
|
+
harnessVariant: string | null;
|
|
1046
|
+
harnessVariantMeta: string | null;
|
|
1044
1047
|
totalCostUsd?: number | null;
|
|
1045
1048
|
};
|
|
1046
1049
|
|
|
@@ -1102,7 +1105,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1102
1105
|
dir: row.dir ?? undefined,
|
|
1103
1106
|
parentTaskId: row.parentTaskId ?? undefined,
|
|
1104
1107
|
claudeSessionId: row.claudeSessionId ?? undefined,
|
|
1105
|
-
model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
|
|
1108
|
+
model: (row.model as "haiku" | "sonnet" | "opus" | "fable" | null) ?? undefined,
|
|
1106
1109
|
scheduleId: row.scheduleId ?? undefined,
|
|
1107
1110
|
workflowRunId: row.workflowRunId ?? undefined,
|
|
1108
1111
|
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
@@ -1127,6 +1130,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1127
1130
|
swarmVersion: row.swarmVersion ?? undefined,
|
|
1128
1131
|
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
1129
1132
|
providerMeta: parseProviderMeta(row.provider as ProviderName | null, row.providerMeta),
|
|
1133
|
+
harnessVariant: row.harnessVariant ?? undefined,
|
|
1134
|
+
harnessVariantMeta: row.harnessVariantMeta ? JSON.parse(row.harnessVariantMeta) : undefined,
|
|
1130
1135
|
totalCostUsd: row.totalCostUsd ?? undefined,
|
|
1131
1136
|
};
|
|
1132
1137
|
}
|
|
@@ -1398,6 +1403,8 @@ export function updateTaskClaudeSessionId(
|
|
|
1398
1403
|
provider?: ProviderName,
|
|
1399
1404
|
providerMeta?: Record<string, unknown>,
|
|
1400
1405
|
model?: string,
|
|
1406
|
+
harnessVariant?: string,
|
|
1407
|
+
harnessVariantMeta?: Record<string, unknown>,
|
|
1401
1408
|
): AgentTask | null {
|
|
1402
1409
|
const setClauses = ["claudeSessionId = ?", "lastUpdatedAt = ?"];
|
|
1403
1410
|
const params: (string | null)[] = [claudeSessionId, new Date().toISOString()];
|
|
@@ -1414,6 +1421,14 @@ export function updateTaskClaudeSessionId(
|
|
|
1414
1421
|
setClauses.push("model = ?");
|
|
1415
1422
|
params.push(model);
|
|
1416
1423
|
}
|
|
1424
|
+
if (harnessVariant !== undefined) {
|
|
1425
|
+
setClauses.push("harnessVariant = ?");
|
|
1426
|
+
params.push(harnessVariant);
|
|
1427
|
+
}
|
|
1428
|
+
if (harnessVariantMeta !== undefined) {
|
|
1429
|
+
setClauses.push("harnessVariantMeta = ?");
|
|
1430
|
+
params.push(JSON.stringify(harnessVariantMeta));
|
|
1431
|
+
}
|
|
1417
1432
|
|
|
1418
1433
|
params.push(taskId);
|
|
1419
1434
|
|
|
@@ -2117,6 +2132,14 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
2117
2132
|
});
|
|
2118
2133
|
});
|
|
2119
2134
|
} catch {}
|
|
2135
|
+
|
|
2136
|
+
// Cascade-fail any non-terminal tasks that depend on this one.
|
|
2137
|
+
// The cascade is recursive (transitive closure) and cycle-safe.
|
|
2138
|
+
try {
|
|
2139
|
+
cascadeFailDependents(id, "failed");
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
console.error("[failTask] cascade-fail dependents error:", err);
|
|
2142
|
+
}
|
|
2120
2143
|
}
|
|
2121
2144
|
return row ? rowToAgentTask(row) : null;
|
|
2122
2145
|
}
|
|
@@ -2155,6 +2178,12 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2155
2178
|
});
|
|
2156
2179
|
});
|
|
2157
2180
|
} catch {}
|
|
2181
|
+
|
|
2182
|
+
try {
|
|
2183
|
+
cascadeFailDependents(id, "cancelled");
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
console.error("[cancelTask] cascade-fail dependents error:", err);
|
|
2186
|
+
}
|
|
2158
2187
|
}
|
|
2159
2188
|
|
|
2160
2189
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -2218,6 +2247,12 @@ export function supersedeTask(
|
|
|
2218
2247
|
});
|
|
2219
2248
|
});
|
|
2220
2249
|
} catch {}
|
|
2250
|
+
|
|
2251
|
+
try {
|
|
2252
|
+
cascadeFailDependents(id, "superseded");
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
console.error("[supersedeTask] cascade-fail dependents error:", err);
|
|
2255
|
+
}
|
|
2221
2256
|
}
|
|
2222
2257
|
|
|
2223
2258
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -3390,6 +3425,75 @@ export function checkDependencies(taskId: string): {
|
|
|
3390
3425
|
return { ready: blockedBy.length === 0, blockedBy };
|
|
3391
3426
|
}
|
|
3392
3427
|
|
|
3428
|
+
/**
|
|
3429
|
+
* Reverse-lookup: find all tasks whose `dependsOn` JSON array contains `parentId`.
|
|
3430
|
+
* Uses SQLite `json_each` to scan the dependsOn column efficiently.
|
|
3431
|
+
* Returns only non-terminal tasks by default (the callers want to cascade-fail
|
|
3432
|
+
* live dependents, not re-process already-finished ones).
|
|
3433
|
+
*/
|
|
3434
|
+
export function getDependentTasks(
|
|
3435
|
+
parentId: string,
|
|
3436
|
+
opts?: { includeTerminal?: boolean },
|
|
3437
|
+
): AgentTask[] {
|
|
3438
|
+
const database = getDb();
|
|
3439
|
+
const rows = database
|
|
3440
|
+
.prepare<AgentTaskRow, [string]>(
|
|
3441
|
+
`SELECT t.*
|
|
3442
|
+
FROM agent_tasks t, json_each(t.dependsOn) AS dep
|
|
3443
|
+
WHERE dep.value = ?`,
|
|
3444
|
+
)
|
|
3445
|
+
.all(parentId);
|
|
3446
|
+
|
|
3447
|
+
const tasks = rows.map(rowToAgentTask);
|
|
3448
|
+
if (opts?.includeTerminal) return tasks;
|
|
3449
|
+
return tasks.filter((t) => !isTerminalTaskStatus(t.status));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
export interface CascadeFailResult {
|
|
3453
|
+
taskId: string;
|
|
3454
|
+
taskSubject: string;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
/**
|
|
3458
|
+
* Recursively cascade-fail all transitive dependents of a parent task.
|
|
3459
|
+
* Walks the full dependency graph: if A fails, and B depends on A, and C
|
|
3460
|
+
* depends on B, then both B and C are failed.
|
|
3461
|
+
*
|
|
3462
|
+
* Guards against cycles with a visited set. Skips already-terminal tasks.
|
|
3463
|
+
* Returns the list of tasks that were actually cascade-failed (for follow-up
|
|
3464
|
+
* enrichment).
|
|
3465
|
+
*/
|
|
3466
|
+
export function cascadeFailDependents(
|
|
3467
|
+
parentId: string,
|
|
3468
|
+
parentStatus: string,
|
|
3469
|
+
visited?: Set<string>,
|
|
3470
|
+
): CascadeFailResult[] {
|
|
3471
|
+
const seen = visited ?? new Set<string>();
|
|
3472
|
+
if (seen.has(parentId)) return [];
|
|
3473
|
+
seen.add(parentId);
|
|
3474
|
+
|
|
3475
|
+
const dependents = getDependentTasks(parentId);
|
|
3476
|
+
const results: CascadeFailResult[] = [];
|
|
3477
|
+
|
|
3478
|
+
for (const dep of dependents) {
|
|
3479
|
+
if (seen.has(dep.id)) continue;
|
|
3480
|
+
|
|
3481
|
+
const reason = `Blocked dependency ${parentId.slice(0, 8)} was ${parentStatus}`;
|
|
3482
|
+
const failed = failTask(dep.id, reason);
|
|
3483
|
+
if (failed) {
|
|
3484
|
+
results.push({
|
|
3485
|
+
taskId: failed.id,
|
|
3486
|
+
taskSubject: failed.task.slice(0, 120),
|
|
3487
|
+
});
|
|
3488
|
+
// Recurse: this dependent may itself have dependents
|
|
3489
|
+
const transitive = cascadeFailDependents(dep.id, "failed (cascade)", seen);
|
|
3490
|
+
results.push(...transitive);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
return results;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3393
3497
|
// ============================================================================
|
|
3394
3498
|
// Agent Profile Operations
|
|
3395
3499
|
// ============================================================================
|
|
@@ -5202,7 +5306,7 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
|
5202
5306
|
consecutiveErrors: row.consecutiveErrors ?? 0,
|
|
5203
5307
|
lastErrorAt: normalizeDate(row.lastErrorAt) ?? undefined,
|
|
5204
5308
|
lastErrorMessage: row.lastErrorMessage ?? undefined,
|
|
5205
|
-
model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
|
|
5309
|
+
model: (row.model as "haiku" | "sonnet" | "opus" | "fable" | null) ?? undefined,
|
|
5206
5310
|
scheduleType: row.scheduleType as "recurring" | "one_time",
|
|
5207
5311
|
createdAt: normalizeDateRequired(row.createdAt),
|
|
5208
5312
|
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
@@ -9969,6 +10073,28 @@ export function setApiKeyName(
|
|
|
9969
10073
|
return result.changes > 0;
|
|
9970
10074
|
}
|
|
9971
10075
|
|
|
10076
|
+
/**
|
|
10077
|
+
* Clear a stale rate-limit record after a successful use proves the key is healthy.
|
|
10078
|
+
*/
|
|
10079
|
+
export function clearKeyRateLimit(
|
|
10080
|
+
keyType: string,
|
|
10081
|
+
keySuffix: string,
|
|
10082
|
+
scope = "global",
|
|
10083
|
+
scopeId: string | null = null,
|
|
10084
|
+
): boolean {
|
|
10085
|
+
const now = new Date().toISOString();
|
|
10086
|
+
const effectiveScopeId = scopeId ?? "";
|
|
10087
|
+
const result = getDb()
|
|
10088
|
+
.prepare(
|
|
10089
|
+
`UPDATE api_key_status
|
|
10090
|
+
SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
|
|
10091
|
+
WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
|
|
10092
|
+
AND status = 'rate_limited'`,
|
|
10093
|
+
)
|
|
10094
|
+
.run(now, keyType, keySuffix, scope, effectiveScopeId);
|
|
10095
|
+
return result.changes > 0;
|
|
10096
|
+
}
|
|
10097
|
+
|
|
9972
10098
|
/**
|
|
9973
10099
|
* Get all key status records for a credential type.
|
|
9974
10100
|
*/
|
|
@@ -11485,6 +11611,22 @@ type ScriptRunRow = {
|
|
|
11485
11611
|
updated_by: string | null;
|
|
11486
11612
|
};
|
|
11487
11613
|
|
|
11614
|
+
type ScriptRunListRow = Pick<
|
|
11615
|
+
ScriptRunRow,
|
|
11616
|
+
| "id"
|
|
11617
|
+
| "agentId"
|
|
11618
|
+
| "scriptName"
|
|
11619
|
+
| "kind"
|
|
11620
|
+
| "status"
|
|
11621
|
+
| "pid"
|
|
11622
|
+
| "startedAt"
|
|
11623
|
+
| "finishedAt"
|
|
11624
|
+
| "error"
|
|
11625
|
+
| "last_heartbeat_at"
|
|
11626
|
+
| "idempotencyKey"
|
|
11627
|
+
| "requestedByUserId"
|
|
11628
|
+
>;
|
|
11629
|
+
|
|
11488
11630
|
function parseJsonColumn(value: string | null): unknown | undefined {
|
|
11489
11631
|
if (value === null) return undefined;
|
|
11490
11632
|
return JSON.parse(value);
|
|
@@ -11510,6 +11652,23 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
|
|
|
11510
11652
|
};
|
|
11511
11653
|
}
|
|
11512
11654
|
|
|
11655
|
+
function rowToScriptRunListItem(row: ScriptRunListRow): ScriptRunListItem {
|
|
11656
|
+
return {
|
|
11657
|
+
id: row.id,
|
|
11658
|
+
agentId: row.agentId,
|
|
11659
|
+
scriptName: row.scriptName ?? undefined,
|
|
11660
|
+
kind: row.kind as ScriptRunKind,
|
|
11661
|
+
status: row.status as ScriptRunStatus,
|
|
11662
|
+
pid: row.pid ?? undefined,
|
|
11663
|
+
startedAt: row.startedAt,
|
|
11664
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
11665
|
+
error: row.error ?? undefined,
|
|
11666
|
+
lastHeartbeatAt: row.last_heartbeat_at ?? undefined,
|
|
11667
|
+
idempotencyKey: row.idempotencyKey ?? undefined,
|
|
11668
|
+
requestedByUserId: row.requestedByUserId ?? undefined,
|
|
11669
|
+
};
|
|
11670
|
+
}
|
|
11671
|
+
|
|
11513
11672
|
export function createScriptRun(data: {
|
|
11514
11673
|
id: string;
|
|
11515
11674
|
agentId: string;
|
|
@@ -11644,7 +11803,7 @@ export function listScriptRuns(opts?: {
|
|
|
11644
11803
|
agentId?: string;
|
|
11645
11804
|
limit?: number;
|
|
11646
11805
|
offset?: number;
|
|
11647
|
-
}):
|
|
11806
|
+
}): ScriptRunListItem[] {
|
|
11648
11807
|
const conditions: string[] = [];
|
|
11649
11808
|
const params: Array<string | number> = [];
|
|
11650
11809
|
if (opts?.status) {
|
|
@@ -11661,11 +11820,26 @@ export function listScriptRuns(opts?: {
|
|
|
11661
11820
|
params.push(limit, offset);
|
|
11662
11821
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
11663
11822
|
const rows = getDb()
|
|
11664
|
-
.prepare<
|
|
11665
|
-
`SELECT
|
|
11823
|
+
.prepare<ScriptRunListRow, Array<string | number>>(
|
|
11824
|
+
`SELECT
|
|
11825
|
+
id,
|
|
11826
|
+
agentId,
|
|
11827
|
+
scriptName,
|
|
11828
|
+
kind,
|
|
11829
|
+
status,
|
|
11830
|
+
pid,
|
|
11831
|
+
startedAt,
|
|
11832
|
+
finishedAt,
|
|
11833
|
+
error,
|
|
11834
|
+
last_heartbeat_at,
|
|
11835
|
+
idempotencyKey,
|
|
11836
|
+
requestedByUserId
|
|
11837
|
+
FROM script_runs ${where}
|
|
11838
|
+
ORDER BY startedAt DESC
|
|
11839
|
+
LIMIT ? OFFSET ?`,
|
|
11666
11840
|
)
|
|
11667
11841
|
.all(...params);
|
|
11668
|
-
return rows.map(
|
|
11842
|
+
return rows.map(rowToScriptRunListItem);
|
|
11669
11843
|
}
|
|
11670
11844
|
|
|
11671
11845
|
export function countScriptRuns(opts?: { status?: ScriptRunStatus; agentId?: string }): number {
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
|
|
17
|
+
export async function runBootReembed(): Promise<void> {
|
|
18
|
+
const db = getDb();
|
|
19
|
+
|
|
20
|
+
const invalidCount =
|
|
21
|
+
db
|
|
22
|
+
.prepare<{ count: number }, []>(
|
|
23
|
+
`SELECT COUNT(*) as count FROM agent_memory
|
|
24
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
25
|
+
)
|
|
26
|
+
.get()?.count ?? 0;
|
|
27
|
+
|
|
28
|
+
if (invalidCount === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const provider = getEmbeddingProvider();
|
|
33
|
+
const testEmbed = await provider.embed("test");
|
|
34
|
+
if (!testEmbed) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[boot-reembed] skipped: ${invalidCount} wrong-dimension rows found but no OpenAI key configured`,
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`[boot-reembed] starting: ${invalidCount} rows with wrong embedding dimensions`);
|
|
42
|
+
|
|
43
|
+
const store = getMemoryStore();
|
|
44
|
+
const rows = db
|
|
45
|
+
.prepare<{ id: string; content: string }, []>(
|
|
46
|
+
`SELECT id, content FROM agent_memory
|
|
47
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
48
|
+
)
|
|
49
|
+
.all();
|
|
50
|
+
|
|
51
|
+
let reembedded = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
55
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
56
|
+
try {
|
|
57
|
+
const embeddings = await provider.embedBatch(batch.map((m) => m.content));
|
|
58
|
+
for (let j = 0; j < embeddings.length; j++) {
|
|
59
|
+
if (embeddings[j]) {
|
|
60
|
+
store.updateEmbedding(batch[j]!.id, embeddings[j]!, provider.name);
|
|
61
|
+
reembedded++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
failed += batch.length;
|
|
66
|
+
console.error(
|
|
67
|
+
`[boot-reembed] batch ${Math.floor(i / BATCH_SIZE) + 1} failed:`,
|
|
68
|
+
(err as Error).message,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const afterInvalid =
|
|
74
|
+
db
|
|
75
|
+
.prepare<{ count: number }, []>(
|
|
76
|
+
`SELECT COUNT(*) as count FROM agent_memory
|
|
77
|
+
WHERE embedding IS NOT NULL AND length(embedding) != ${VECTOR_BYTES}`,
|
|
78
|
+
)
|
|
79
|
+
.get()?.count ?? 0;
|
|
80
|
+
|
|
81
|
+
console.log(
|
|
82
|
+
`[boot-reembed] complete: reembedded=${reembedded} failed=${failed} remaining_invalid=${afterInvalid}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -15,8 +15,46 @@ export const TTL_DEFAULTS: Record<AgentMemorySource, number | null> = {
|
|
|
15
15
|
manual: null,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// Per-source recency decay half-life (in days).
|
|
19
|
+
// manual = Infinity (no decay — curated knowledge stays relevant forever).
|
|
20
|
+
// A global MEMORY_RECENCY_HALF_LIFE_DAYS override forces ALL sources to the same value.
|
|
21
|
+
const GLOBAL_HALF_LIFE_OVERRIDE = process.env.MEMORY_RECENCY_HALF_LIFE_DAYS;
|
|
22
|
+
const GLOBAL_HALF_LIFE =
|
|
23
|
+
GLOBAL_HALF_LIFE_OVERRIDE != null && GLOBAL_HALF_LIFE_OVERRIDE !== ""
|
|
24
|
+
? Number(GLOBAL_HALF_LIFE_OVERRIDE)
|
|
25
|
+
: null;
|
|
26
|
+
|
|
27
|
+
export const RECENCY_DECAY_HALF_LIFE: Record<AgentMemorySource, number> =
|
|
28
|
+
GLOBAL_HALF_LIFE != null && Number.isFinite(GLOBAL_HALF_LIFE)
|
|
29
|
+
? {
|
|
30
|
+
manual: GLOBAL_HALF_LIFE,
|
|
31
|
+
file_index: GLOBAL_HALF_LIFE,
|
|
32
|
+
task_completion: GLOBAL_HALF_LIFE,
|
|
33
|
+
session_summary: GLOBAL_HALF_LIFE,
|
|
34
|
+
}
|
|
35
|
+
: {
|
|
36
|
+
manual: Number.POSITIVE_INFINITY,
|
|
37
|
+
file_index: 180,
|
|
38
|
+
task_completion: 14,
|
|
39
|
+
session_summary: 7,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Legacy export — callers that don't have a source fall back to task_completion's value.
|
|
43
|
+
export const RECENCY_DECAY_HALF_LIFE_DAYS = RECENCY_DECAY_HALF_LIFE.task_completion;
|
|
44
|
+
|
|
45
|
+
// Source-quality multiplier for reranking.
|
|
46
|
+
// Curated manual memories rank higher; ephemeral session summaries rank lower.
|
|
47
|
+
export const SOURCE_QUALITY_MULTIPLIER: Record<AgentMemorySource, number> = {
|
|
48
|
+
manual: 1.5,
|
|
49
|
+
file_index: 1.0,
|
|
50
|
+
task_completion: 0.7,
|
|
51
|
+
session_summary: 0.5,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Minimum raw cosine similarity to keep a candidate. Below this, the result is noise.
|
|
55
|
+
export const MIN_SIMILARITY = numEnv("MEMORY_MIN_SIMILARITY", 0.1);
|
|
56
|
+
|
|
18
57
|
// Reranking parameters
|
|
19
|
-
export const RECENCY_DECAY_HALF_LIFE_DAYS = numEnv("MEMORY_RECENCY_HALF_LIFE_DAYS", 14);
|
|
20
58
|
export const ACCESS_BOOST_MAX_MULTIPLIER = numEnv("MEMORY_ACCESS_BOOST_MAX", 1.5);
|
|
21
59
|
export const ACCESS_BOOST_RECENCY_WINDOW_HOURS = numEnv("MEMORY_ACCESS_RECENCY_HOURS", 48);
|
|
22
60
|
export const CANDIDATE_SET_MULTIPLIER = numEnv("MEMORY_CANDIDATE_MULTIPLIER", 3);
|
|
@@ -25,3 +63,6 @@ export const CANDIDATE_SET_MULTIPLIER = numEnv("MEMORY_CANDIDATE_MULTIPLIER", 3)
|
|
|
25
63
|
export const EMBEDDING_DIMENSIONS = numEnv("EMBEDDING_DIMENSIONS", 512);
|
|
26
64
|
export const DEFAULT_EMBEDDING_DIMENSIONS = EMBEDDING_DIMENSIONS;
|
|
27
65
|
export const DEFAULT_EMBEDDING_MODEL = "openai/text-embedding-3-small";
|
|
66
|
+
|
|
67
|
+
// Manual memories must NEVER be deleted by automated processes (curator, GC, etc.)
|
|
68
|
+
export const PROTECTED_SOURCES: ReadonlySet<AgentMemorySource> = new Set(["manual"]);
|
|
@@ -55,6 +55,13 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
|
55
55
|
const values = response.data[0]?.embedding;
|
|
56
56
|
if (!values) return null;
|
|
57
57
|
|
|
58
|
+
if (values.length !== this.dimensions) {
|
|
59
|
+
console.error(
|
|
60
|
+
`[memory] Embedding dimension mismatch: expected=${this.dimensions} got=${values.length}. Provider may not support the 'dimensions' parameter.`,
|
|
61
|
+
);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
return new Float32Array(values);
|
|
59
66
|
} catch (err) {
|
|
60
67
|
console.error("[memory] Embedding failed:", (err as Error).message);
|
|
@@ -90,6 +97,12 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
|
90
97
|
for (const item of response.data) {
|
|
91
98
|
const originalIndex = nonEmptyIndices[item.index];
|
|
92
99
|
if (originalIndex !== undefined && item.embedding) {
|
|
100
|
+
if (item.embedding.length !== this.dimensions) {
|
|
101
|
+
console.error(
|
|
102
|
+
`[memory] Batch embedding dimension mismatch: expected=${this.dimensions} got=${item.embedding.length}. Provider may not support the 'dimensions' parameter.`,
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
93
106
|
results[originalIndex] = new Float32Array(item.embedding);
|
|
94
107
|
}
|
|
95
108
|
}
|