@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.
Files changed (87) hide show
  1. package/openapi.json +63 -3
  2. package/package.json +5 -5
  3. package/src/be/db.ts +180 -6
  4. package/src/be/memory/boot-reembed.ts +84 -0
  5. package/src/be/memory/constants.ts +42 -1
  6. package/src/be/memory/providers/openai-embedding.ts +13 -0
  7. package/src/be/memory/providers/sqlite-store.ts +75 -26
  8. package/src/be/memory/raters/llm-client.ts +12 -5
  9. package/src/be/memory/reranker.ts +35 -17
  10. package/src/be/memory/types.ts +11 -0
  11. package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
  12. package/src/be/migrations/089_harness_variant.sql +2 -0
  13. package/src/be/modelsdev-cache.json +6478 -3099
  14. package/src/be/seed-pricing.ts +1 -0
  15. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
  16. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
  17. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
  18. package/src/be/seed-scripts/catalog/compound-insights.ts +371 -0
  19. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
  20. package/src/be/seed-scripts/index.ts +5 -5
  21. package/src/be/skill-sync.ts +28 -179
  22. package/src/commands/runner.ts +124 -7
  23. package/src/http/api-keys.ts +42 -0
  24. package/src/http/index.ts +9 -0
  25. package/src/http/mcp-bridge.ts +1 -1
  26. package/src/http/memory.ts +27 -24
  27. package/src/http/tasks.ts +10 -6
  28. package/src/providers/claude-adapter.ts +33 -1
  29. package/src/providers/claude-managed-adapter.ts +3 -0
  30. package/src/providers/claude-managed-models.ts +7 -0
  31. package/src/providers/codex-adapter.ts +8 -1
  32. package/src/providers/codex-models.ts +1 -0
  33. package/src/providers/codex-oauth/auth-json.ts +1 -0
  34. package/src/providers/harness-version.ts +7 -0
  35. package/src/providers/opencode-adapter.ts +11 -4
  36. package/src/providers/pi-mono-adapter.ts +12 -2
  37. package/src/providers/types.ts +2 -0
  38. package/src/scripts-runtime/egress-secrets.ts +83 -0
  39. package/src/scripts-runtime/eval-harness.ts +4 -0
  40. package/src/scripts-runtime/executors/types.ts +7 -0
  41. package/src/scripts-runtime/loader.ts +2 -0
  42. package/src/server-user.ts +2 -2
  43. package/src/slack/channel-join.ts +41 -0
  44. package/src/tasks/worker-follow-up.ts +12 -0
  45. package/src/tests/additive-buffer.test.ts +0 -1
  46. package/src/tests/api-key-tracking.test.ts +113 -0
  47. package/src/tests/approval-requests.test.ts +0 -6
  48. package/src/tests/claude-managed-setup.test.ts +0 -4
  49. package/src/tests/codex-pool.test.ts +2 -6
  50. package/src/tests/http-api-integration.test.ts +4 -6
  51. package/src/tests/memory-e2e.test.ts +6 -6
  52. package/src/tests/memory-edges.test.ts +0 -2
  53. package/src/tests/memory-rate-endpoint.test.ts +0 -2
  54. package/src/tests/memory-rater-e2e.test.ts +4 -7
  55. package/src/tests/memory-reranker.test.ts +135 -124
  56. package/src/tests/memory-store.test.ts +19 -1
  57. package/src/tests/memory.test.ts +64 -12
  58. package/src/tests/model-control.test.ts +1 -1
  59. package/src/tests/reload-config.test.ts +33 -17
  60. package/src/tests/runner-skills-refresh.test.ts +216 -46
  61. package/src/tests/script-runs-http.test.ts +7 -1
  62. package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
  63. package/src/tests/seed-scripts.test.ts +218 -1
  64. package/src/tests/session-attach.test.ts +6 -6
  65. package/src/tests/skill-fs-writer.test.ts +250 -0
  66. package/src/tests/slack-attachments-block.test.ts +0 -1
  67. package/src/tests/slack-blocks.test.ts +0 -1
  68. package/src/tests/slack-channel-join.test.ts +80 -0
  69. package/src/tests/slack-identity-resolution.test.ts +0 -1
  70. package/src/tests/structured-output.test.ts +0 -2
  71. package/src/tests/task-cascade-fail.test.ts +304 -0
  72. package/src/tests/use-dismissible-card.test.ts +0 -4
  73. package/src/tools/schedules/create-schedule.ts +2 -2
  74. package/src/tools/schedules/update-schedule.ts +1 -1
  75. package/src/tools/send-task.ts +2 -2
  76. package/src/tools/slack-post.ts +18 -15
  77. package/src/tools/slack-read.ts +9 -11
  78. package/src/tools/slack-reply.ts +18 -15
  79. package/src/tools/slack-start-thread.ts +17 -14
  80. package/src/tools/task-action.ts +2 -2
  81. package/src/types.ts +11 -0
  82. package/src/utils/context-window.ts +3 -0
  83. package/src/utils/credentials.ts +22 -2
  84. package/src/utils/skill-fs-writer.ts +220 -0
  85. package/src/utils/skills-refresh.ts +123 -40
  86. package/templates/workflows/llm-safe-release-context/config.json +13 -0
  87. 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.92.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}/claude-session": {
10767
+ "/api/tasks/{id}/session": {
10715
10768
  "put": {
10716
- "summary": "Update Claude session ID for a task",
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.92.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.78.1",
116
- "@earendil-works/pi-ai": "^0.78.1",
117
- "@earendil-works/pi-coding-agent": "^0.78.1",
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.137.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
- }): ScriptRun[] {
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<ScriptRunRow, Array<string | number>>(
11665
- `SELECT * FROM script_runs ${where} ORDER BY startedAt DESC LIMIT ? OFFSET ?`,
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(rowToScriptRun);
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
  }