@desplega.ai/agent-swarm 1.92.1 → 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/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.92.2",
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": [
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.92.2",
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>",
package/src/be/db.ts CHANGED
@@ -2117,6 +2117,14 @@ export function failTask(id: string, reason: string): AgentTask | null {
2117
2117
  });
2118
2118
  });
2119
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
+ }
2120
2128
  }
2121
2129
  return row ? rowToAgentTask(row) : null;
2122
2130
  }
@@ -2155,6 +2163,12 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
2155
2163
  });
2156
2164
  });
2157
2165
  } catch {}
2166
+
2167
+ try {
2168
+ cascadeFailDependents(id, "cancelled");
2169
+ } catch (err) {
2170
+ console.error("[cancelTask] cascade-fail dependents error:", err);
2171
+ }
2158
2172
  }
2159
2173
 
2160
2174
  return row ? rowToAgentTask(row) : null;
@@ -2218,6 +2232,12 @@ export function supersedeTask(
2218
2232
  });
2219
2233
  });
2220
2234
  } catch {}
2235
+
2236
+ try {
2237
+ cascadeFailDependents(id, "superseded");
2238
+ } catch (err) {
2239
+ console.error("[supersedeTask] cascade-fail dependents error:", err);
2240
+ }
2221
2241
  }
2222
2242
 
2223
2243
  return row ? rowToAgentTask(row) : null;
@@ -3390,6 +3410,75 @@ export function checkDependencies(taskId: string): {
3390
3410
  return { ready: blockedBy.length === 0, blockedBy };
3391
3411
  }
3392
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
+
3393
3482
  // ============================================================================
3394
3483
  // Agent Profile Operations
3395
3484
  // ============================================================================
@@ -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
+ }
@@ -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
  }
@@ -1,7 +1,12 @@
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 { EMBEDDING_DIMENSIONS, TTL_DEFAULTS } from "../constants";
4
+ import {
5
+ EMBEDDING_DIMENSIONS,
6
+ MIN_SIMILARITY,
7
+ PROTECTED_SOURCES,
8
+ TTL_DEFAULTS,
9
+ } from "../constants";
5
10
  import type {
6
11
  MemoryCandidate,
7
12
  MemoryHealth,
@@ -400,6 +405,7 @@ export class SqliteMemoryStore implements MemoryStore {
400
405
  const candidates: MemoryCandidate[] = [];
401
406
  for (const row of rows) {
402
407
  const similarity = 1 - row.distance;
408
+ if (similarity < MIN_SIMILARITY) continue;
403
409
  candidates.push(rowToCandidate(row, similarity));
404
410
  }
405
411
 
@@ -446,6 +452,7 @@ export class SqliteMemoryStore implements MemoryStore {
446
452
  const emb = deserializeEmbedding(row.embedding);
447
453
  if (emb.length !== queryEmbedding.length) continue;
448
454
  const similarity = cosineSimilarity(queryEmbedding, emb);
455
+ if (similarity < MIN_SIMILARITY) continue;
449
456
  candidates.push(rowToCandidate(row, similarity));
450
457
  }
451
458
 
@@ -523,6 +530,31 @@ export class SqliteMemoryStore implements MemoryStore {
523
530
  return rows.map(rowToAgentMemory);
524
531
  }
525
532
 
533
+ isSourceProtected(source: AgentMemorySource): boolean {
534
+ return PROTECTED_SOURCES.has(source);
535
+ }
536
+
537
+ listForCuration(
538
+ agentId?: string,
539
+ ): { id: string; source: string; name: string; createdAt: string }[] {
540
+ const db = getDb();
541
+ const protectedList = [...PROTECTED_SOURCES].map((s) => `'${s}'`).join(",");
542
+ if (agentId) {
543
+ return db
544
+ .prepare<{ id: string; source: string; name: string; createdAt: string }, [string]>(
545
+ `SELECT id, source, name, createdAt FROM agent_memory
546
+ WHERE agentId = ? AND source NOT IN (${protectedList})`,
547
+ )
548
+ .all(agentId);
549
+ }
550
+ return db
551
+ .prepare<{ id: string; source: string; name: string; createdAt: string }, []>(
552
+ `SELECT id, source, name, createdAt FROM agent_memory
553
+ WHERE source NOT IN (${protectedList})`,
554
+ )
555
+ .all();
556
+ }
557
+
526
558
  listForReembedding(options?: { agentId?: string }): { id: string; content: string }[] {
527
559
  const db = getDb();
528
560
  if (options?.agentId) {
@@ -1,7 +1,10 @@
1
+ import type { AgentMemorySource } from "@/types";
1
2
  import {
2
3
  ACCESS_BOOST_MAX_MULTIPLIER,
3
4
  ACCESS_BOOST_RECENCY_WINDOW_HOURS,
5
+ RECENCY_DECAY_HALF_LIFE,
4
6
  RECENCY_DECAY_HALF_LIFE_DAYS,
7
+ SOURCE_QUALITY_MULTIPLIER,
5
8
  } from "./constants";
6
9
  import type { MemoryCandidate, RerankOptions } from "./types";
7
10
 
@@ -9,13 +12,16 @@ const MS_PER_DAY = 1000 * 60 * 60 * 24;
9
12
  const MS_PER_HOUR = 1000 * 60 * 60;
10
13
 
11
14
  /**
12
- * Exponential decay based on age. A memory at exactly HALF_LIFE_DAYS old
13
- * gets multiplied by 0.5. Fresh memories get ~1.0.
15
+ * Exponential decay based on age and memory source.
16
+ * Source-aware: manual memories have no decay (Infinity half-life),
17
+ * file_index = 180d, task_completion = 14d, session_summary = 7d.
14
18
  */
15
- export function recencyDecay(createdAt: string, now: Date): number {
19
+ export function recencyDecay(createdAt: string, now: Date, source?: AgentMemorySource): number {
20
+ const halfLife = source ? RECENCY_DECAY_HALF_LIFE[source] : RECENCY_DECAY_HALF_LIFE_DAYS;
21
+ if (!Number.isFinite(halfLife)) return 1.0;
16
22
  const ageDays = (now.getTime() - new Date(createdAt).getTime()) / MS_PER_DAY;
17
23
  if (ageDays <= 0) return 1.0;
18
- return 2 ** (-ageDays / RECENCY_DECAY_HALF_LIFE_DAYS);
24
+ return 2 ** (-ageDays / halfLife);
19
25
  }
20
26
 
21
27
  /**
@@ -31,6 +37,14 @@ export function accessBoost(accessedAt: string, accessCount: number, now: Date):
31
37
  return boost;
32
38
  }
33
39
 
40
+ /**
41
+ * Source-quality multiplier. Manual memories get a 1.5× boost,
42
+ * session summaries get 0.5×. Unknown sources default to 1.0.
43
+ */
44
+ export function sourceQuality(source: AgentMemorySource): number {
45
+ return SOURCE_QUALITY_MULTIPLIER[source] ?? 1.0;
46
+ }
47
+
34
48
  /**
35
49
  * Beta-Binomial usefulness factor for reranking.
36
50
  *
@@ -56,33 +70,37 @@ export function usefulness(alpha: number, beta: number): number {
56
70
  }
57
71
 
58
72
  /**
59
- * Final score combining similarity, recency decay, access boost, and
60
- * Beta-Binomial usefulness. With default Beta(1,1) and default
61
- * MEMORY_DEMOTION_FLOOR=1.0, the usefulness factor is exactly 1.0 and this
62
- * computation matches the pre-rater behaviour byte-for-byte.
63
- *
64
- * v2: optional edge-aware boost — see thoughts/taras/plans/2026-05-05-memory-rater-v1.5/root.md
73
+ * Final score combining similarity, recency decay, access boost,
74
+ * source quality, and Beta-Binomial usefulness.
65
75
  */
66
76
  export function computeScore(candidate: MemoryCandidate, now: Date): number {
67
77
  return (
68
78
  candidate.similarity *
69
- recencyDecay(candidate.createdAt, now) *
79
+ recencyDecay(candidate.createdAt, now, candidate.source) *
70
80
  accessBoost(candidate.accessedAt, candidate.accessCount, now) *
81
+ sourceQuality(candidate.source) *
71
82
  usefulness(candidate.alpha, candidate.beta)
72
83
  );
73
84
  }
74
85
 
75
86
  /**
76
- * Rerank candidates by combining similarity with recency and access signals.
77
- * Returns the top `limit` candidates sorted by final score.
87
+ * Rerank candidates by combining similarity with recency, source quality,
88
+ * and access signals. Returns the top `limit` candidates sorted by composite
89
+ * score. Preserves raw similarity in `rawSimilarity` and sets `compositeScore`.
78
90
  */
79
91
  export function rerank(candidates: MemoryCandidate[], options: RerankOptions): MemoryCandidate[] {
80
92
  const { limit, now = new Date() } = options;
81
93
 
82
- const scored = candidates.map((candidate) => ({
83
- ...candidate,
84
- similarity: computeScore(candidate, now),
85
- }));
94
+ const scored = candidates.map((candidate) => {
95
+ const rawSimilarity = candidate.similarity;
96
+ const compositeScore = computeScore(candidate, now);
97
+ return {
98
+ ...candidate,
99
+ rawSimilarity,
100
+ compositeScore,
101
+ similarity: compositeScore,
102
+ };
103
+ });
86
104
 
87
105
  scored.sort((a, b) => b.similarity - a.similarity);
88
106
  return scored.slice(0, limit);
@@ -22,6 +22,10 @@ export interface MemoryStore {
22
22
  peek(id: string): AgentMemory | null;
23
23
  search(embedding: Float32Array, agentId: string, options: MemorySearchOptions): MemoryCandidate[];
24
24
  list(agentId: string, options: MemoryListOptions): AgentMemory[];
25
+ isSourceProtected(source: AgentMemorySource): boolean;
26
+ listForCuration(
27
+ agentId?: string,
28
+ ): { id: string; source: string; name: string; createdAt: string }[];
25
29
  listForReembedding(options?: { agentId?: string }): { id: string; content: string }[];
26
30
  delete(id: string): boolean;
27
31
  deleteBySourcePath(sourcePath: string, agentId: string): number;
@@ -51,6 +55,10 @@ export interface MemoryInput {
51
55
 
52
56
  export interface MemoryCandidate extends AgentMemory {
53
57
  similarity: number;
58
+ /** Raw cosine similarity before reranking (preserved for diagnostics). */
59
+ rawSimilarity?: number;
60
+ /** Final composite score after reranking (recency × source × usefulness × access). */
61
+ compositeScore?: number;
54
62
  accessCount: number;
55
63
  expiresAt: string | null;
56
64
  embeddingModel: string | null;