@desplega.ai/agent-swarm 1.103.0 → 1.104.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 (122) hide show
  1. package/README.md +2 -0
  2. package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
  3. package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
  4. package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
  5. package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
  6. package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
  7. package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
  8. package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
  9. package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
  10. package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
  11. package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
  12. package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
  13. package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
  14. package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
  15. package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
  16. package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
  17. package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
  18. package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
  19. package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
  20. package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
  21. package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
  22. package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
  23. package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
  24. package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
  25. package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
  26. package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
  27. package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
  28. package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
  29. package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
  30. package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
  31. package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
  32. package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
  33. package/dist/cli.js +10 -10
  34. package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
  35. package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
  36. package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
  37. package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
  38. package/dist/http-59ygpn2h.js +17263 -0
  39. package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
  40. package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
  41. package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
  42. package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
  43. package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
  44. package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
  45. package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
  46. package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
  47. package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
  48. package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
  49. package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
  50. package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
  51. package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
  52. package/openapi.json +90 -4
  53. package/package.json +3 -3
  54. package/src/be/memory/constants.ts +6 -0
  55. package/src/be/memory/providers/sqlite-store.ts +468 -71
  56. package/src/be/memory/raters/retrieval.ts +4 -2
  57. package/src/be/memory/reranker.ts +5 -2
  58. package/src/be/memory/retrieval-store.ts +2 -0
  59. package/src/be/memory/types.ts +46 -0
  60. package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
  61. package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
  62. package/src/be/migrations/101_script_connections.sql +67 -0
  63. package/src/be/script-connections.ts +744 -0
  64. package/src/be/script-credential-broker.ts +38 -0
  65. package/src/be/scripts/typecheck.ts +23 -2
  66. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
  67. package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
  68. package/src/commands/runner.ts +8 -6
  69. package/src/heartbeat/heartbeat.ts +42 -1
  70. package/src/heartbeat/index.ts +7 -1
  71. package/src/http/memory.ts +127 -14
  72. package/src/http/scripts.ts +15 -14
  73. package/src/scripts-runtime/api-client.ts +100 -0
  74. package/src/scripts-runtime/api-types.ts +45 -0
  75. package/src/scripts-runtime/credential-broker/README.md +40 -0
  76. package/src/scripts-runtime/credential-broker/broker.ts +51 -0
  77. package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
  78. package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
  79. package/src/scripts-runtime/credential-broker/index.ts +17 -0
  80. package/src/scripts-runtime/credential-broker/store.ts +72 -0
  81. package/src/scripts-runtime/credential-broker/types.ts +54 -0
  82. package/src/scripts-runtime/ctx.ts +11 -1
  83. package/src/scripts-runtime/egress-secrets.ts +14 -76
  84. package/src/scripts-runtime/eval-harness.ts +1 -1
  85. package/src/scripts-runtime/executors/types.ts +5 -5
  86. package/src/scripts-runtime/loader.ts +6 -1
  87. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  88. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  89. package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
  90. package/src/server.ts +6 -0
  91. package/src/tests/credential-broker.test.ts +281 -0
  92. package/src/tests/heartbeat.test.ts +239 -0
  93. package/src/tests/memory-edit.test.ts +177 -0
  94. package/src/tests/memory-hybrid.test.ts +212 -0
  95. package/src/tests/runner-repo-autostash.test.ts +39 -8
  96. package/src/tests/script-connections.test.ts +360 -0
  97. package/src/tests/scripts-http.test.ts +16 -10
  98. package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
  99. package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
  100. package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
  101. package/src/tools/credential-bindings/index.ts +1 -0
  102. package/src/tools/credential-bindings/tool.ts +238 -0
  103. package/src/tools/memory-edit.ts +148 -0
  104. package/src/tools/memory-search.ts +17 -10
  105. package/src/tools/schedules/delete-schedule.ts +18 -8
  106. package/src/tools/schedules/update-schedule.ts +4 -9
  107. package/src/tools/script-connections/index.ts +1 -0
  108. package/src/tools/script-connections/tool.ts +167 -0
  109. package/src/tools/swarm-config/delete-config.ts +5 -0
  110. package/src/tools/swarm-config/set-config.ts +23 -1
  111. package/src/tools/tool-config.ts +6 -1
  112. package/src/types.ts +6 -0
  113. package/src/workflows/executors/swarm-script.ts +2 -0
  114. package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
  115. package/templates/community/dora-metrics/README.md +39 -0
  116. package/templates/community/dora-metrics/lead-prompt.md +56 -0
  117. package/templates/community/dora-metrics/report.mjs +393 -0
  118. package/templates/community/dora-metrics/run.sh +113 -0
  119. package/templates/schedules/weekly-dora-metrics/config.json +13 -0
  120. package/templates/schedules/weekly-dora-metrics/content.md +61 -0
  121. package/dist/cli-2307phk1.js +0 -24223
  122. package/dist/cli-2deg0cen.js +0 -120
@@ -1,17 +1,23 @@
1
1
  import { getDb, isSqliteVecAvailable } from "@/be/db";
2
2
  import { cosineSimilarity, deserializeEmbedding, serializeEmbedding } from "@/be/embedding";
3
+ import { contentSha256 } from "@/commands/profile-sync";
3
4
  import type { AgentMemory, AgentMemoryScope, AgentMemorySource } from "@/types";
4
5
  import {
5
6
  EMBEDDING_DIMENSIONS,
7
+ isHybridSearchEnabled,
6
8
  MIN_SIMILARITY,
7
9
  PROTECTED_SOURCES,
8
10
  TTL_DEFAULTS,
9
11
  } from "../constants";
12
+ import { recencyDecay } from "../reranker";
10
13
  import type {
11
14
  MemoryCandidate,
15
+ MemoryEditInput,
16
+ MemoryEditResult,
12
17
  MemoryHealth,
13
18
  MemoryInput,
14
19
  MemoryListOptions,
20
+ MemoryRetrievalSource,
15
21
  MemorySearchOptions,
16
22
  MemoryStats,
17
23
  MemoryStore,
@@ -41,6 +47,10 @@ type AgentMemoryRow = {
41
47
  embeddingModel: string | null;
42
48
  alpha: number;
43
49
  beta: number;
50
+ key: string | null;
51
+ contentHash: string | null;
52
+ version: number;
53
+ updatedAt: string | null;
44
54
  };
45
55
 
46
56
  function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
@@ -48,6 +58,7 @@ function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
48
58
  id: row.id,
49
59
  agentId: row.agentId,
50
60
  scope: row.scope as AgentMemoryScope,
61
+ key: row.key ?? null,
51
62
  name: row.name,
52
63
  content: row.content,
53
64
  summary: row.summary,
@@ -58,10 +69,13 @@ function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
58
69
  totalChunks: row.totalChunks,
59
70
  tags: JSON.parse(row.tags || "[]"),
60
71
  createdAt: row.createdAt,
72
+ updatedAt: row.updatedAt ?? null,
61
73
  accessedAt: row.accessedAt,
62
74
  expiresAt: row.expiresAt ?? null,
63
75
  accessCount: row.accessCount ?? 0,
64
76
  embeddingModel: row.embeddingModel ?? null,
77
+ contentHash: row.contentHash ?? null,
78
+ version: row.version ?? 1,
65
79
  };
66
80
  }
67
81
 
@@ -77,6 +91,60 @@ function rowToCandidate(row: AgentMemoryRow, similarity: number): MemoryCandidat
77
91
  };
78
92
  }
79
93
 
94
+ function retrievalSourceFor(sources: Set<MemoryRetrievalSource>): MemoryRetrievalSource {
95
+ if (sources.has("fts") && sources.has("vec")) return "hybrid";
96
+ if (sources.has("fts")) return "fts";
97
+ if (sources.has("vec")) return "vec";
98
+ return "fallback";
99
+ }
100
+
101
+ /**
102
+ * RRF (Reciprocal Rank Fusion) score for hybrid search.
103
+ *
104
+ * Combines rankings from vector (semantic) and FTS (keyword) arms into a single
105
+ * comparable score using the standard RRF formula: score = Σ 1/(k + rank + 1)
106
+ * where k=60 is the smoothing constant that prevents top-ranked results from
107
+ * dominating. The score is then modulated by source-aware recency decay so
108
+ * ephemeral sources (session_summary, task_completion) naturally age out.
109
+ *
110
+ * A memory that appears in BOTH vec and FTS arms receives two reciprocal-rank
111
+ * contributions that SUM, boosting it above single-arm matches (compounding).
112
+ *
113
+ * Score range: (0, ~0.033] per arm contribution (1/(60+0+1)=0.0164 max per arm).
114
+ * After decay: [0, ~0.033]. Scores are directly comparable across results.
115
+ */
116
+ export function computeRrfScore(rank: number, decayFactor: number, k = 60): number {
117
+ return (1 / (k + rank + 1)) * decayFactor;
118
+ }
119
+
120
+ /**
121
+ * Compute the next content for a memory edit. Returns the new content string.
122
+ * Throws if validation fails (missing fields, oldString not found, ambiguous match).
123
+ */
124
+ export function applyEditMode(
125
+ mode: "replace" | "exact",
126
+ currentContent: string,
127
+ fields: { content?: string; oldString?: string; newString?: string },
128
+ ): string {
129
+ if (mode === "replace") {
130
+ if (fields.content == null) throw new Error("replace mode requires content");
131
+ return fields.content;
132
+ }
133
+ if (!fields.oldString || fields.newString == null) {
134
+ throw new Error("exact mode requires oldString and newString");
135
+ }
136
+ const first = currentContent.indexOf(fields.oldString);
137
+ if (first === -1) throw new Error("oldString not found");
138
+ if (currentContent.indexOf(fields.oldString, first + fields.oldString.length) !== -1) {
139
+ throw new Error("oldString is ambiguous");
140
+ }
141
+ return (
142
+ currentContent.slice(0, first) +
143
+ fields.newString +
144
+ currentContent.slice(first + fields.oldString.length)
145
+ );
146
+ }
147
+
80
148
  function computeExpiresAt(source: AgentMemorySource): string | null {
81
149
  const ttlDays = TTL_DEFAULTS[source];
82
150
  if (ttlDays == null) return null;
@@ -85,10 +153,91 @@ function computeExpiresAt(source: AgentMemorySource): string | null {
85
153
 
86
154
  export class SqliteMemoryStore implements MemoryStore {
87
155
  private vecInitialized = false;
156
+ private ftsInitialized = false;
88
157
  private lastPopulate: MemoryVecPopulateStats | null = null;
89
158
 
90
159
  constructor() {
91
160
  this.ensureVecTable();
161
+ this.ensureFtsTable();
162
+ }
163
+
164
+ private ensureFtsTable(): void {
165
+ if (this.ftsInitialized) return;
166
+ const db = getDb();
167
+ try {
168
+ db.run(`
169
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
170
+ memory_id UNINDEXED,
171
+ name,
172
+ content,
173
+ tokenize='porter unicode61'
174
+ )
175
+ `);
176
+ this.populateFtsTable();
177
+ this.ftsInitialized = true;
178
+ } catch (err) {
179
+ this.ftsInitialized = false;
180
+ console.error("[memory-fts] Failed to initialize memory_fts:", (err as Error).message);
181
+ }
182
+ }
183
+
184
+ private getFtsTableSchema(): string | null {
185
+ try {
186
+ return (
187
+ getDb()
188
+ .prepare<{ sql: string | null }, []>(
189
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_fts'",
190
+ )
191
+ .get()?.sql ?? null
192
+ );
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ private populateFtsTable(): void {
199
+ const db = getDb();
200
+ const deletedExtra = db
201
+ .prepare(
202
+ `DELETE FROM memory_fts
203
+ WHERE memory_id NOT IN (SELECT id FROM agent_memory)`,
204
+ )
205
+ .run();
206
+ if (deletedExtra.changes > 0) {
207
+ console.warn(`[memory-fts] removed_extra_rows count=${deletedExtra.changes}`);
208
+ }
209
+
210
+ const inserted = db
211
+ .prepare(
212
+ `INSERT INTO memory_fts(memory_id, name, content)
213
+ SELECT m.id, m.name, m.content
214
+ FROM agent_memory m
215
+ WHERE NOT EXISTS (SELECT 1 FROM memory_fts f WHERE f.memory_id = m.id)`,
216
+ )
217
+ .run();
218
+ console.log(`[memory-fts] populate inserted=${inserted.changes}`);
219
+ }
220
+
221
+ private syncFtsRow(memoryId: string, name: string, content: string): void {
222
+ if (!this.ftsInitialized && !this.getFtsTableSchema()) return;
223
+ const db = getDb();
224
+ try {
225
+ db.prepare("DELETE FROM memory_fts WHERE memory_id = ?").run(memoryId);
226
+ db.prepare("INSERT INTO memory_fts(memory_id, name, content) VALUES (?, ?, ?)").run(
227
+ memoryId,
228
+ name,
229
+ content,
230
+ );
231
+ } catch (err) {
232
+ console.error(`[memory-fts] sync failed memory_id=${memoryId}: ${(err as Error).message}`);
233
+ }
234
+ }
235
+
236
+ private deleteFtsRows(ids: string[]): void {
237
+ if (ids.length === 0 || (!this.ftsInitialized && !this.getFtsTableSchema())) return;
238
+ const db = getDb();
239
+ const placeholders = ids.map(() => "?").join(",");
240
+ db.prepare(`DELETE FROM memory_fts WHERE memory_id IN (${placeholders})`).run(...ids);
92
241
  }
93
242
 
94
243
  private ensureVecTable(): void {
@@ -237,56 +386,65 @@ export class SqliteMemoryStore implements MemoryStore {
237
386
  const id = crypto.randomUUID();
238
387
  const now = new Date().toISOString();
239
388
  const expiresAt = computeExpiresAt(input.source);
389
+ const key = input.key ?? `${input.scope}/${input.source}/${id}`;
390
+ const contentHash = contentSha256(input.content);
391
+ const version = 1;
392
+ const db = getDb();
240
393
 
241
- const row = getDb()
242
- .prepare<
243
- AgentMemoryRow,
244
- [
245
- string,
246
- string | null,
247
- string,
248
- string,
249
- string,
250
- string | null,
251
- string,
252
- string | null,
253
- string | null,
254
- number,
255
- number,
256
- string,
257
- string,
258
- string,
259
- string | null,
260
- number,
261
- string | null,
262
- string | null,
263
- ]
264
- >(
265
- `INSERT INTO agent_memory (id, agentId, scope, name, content, summary, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt, expiresAt, accessCount, embeddingModel, contextKey)
266
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
267
- )
268
- .get(
269
- id,
270
- input.agentId ?? null,
271
- input.scope,
272
- input.name,
394
+ let row: AgentMemoryRow | null | undefined;
395
+ const tx = db.transaction(() => {
396
+ row = db
397
+ .prepare<AgentMemoryRow, (string | number | null)[]>(
398
+ `INSERT INTO agent_memory (id, agentId, scope, key, name, content, summary, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, updatedAt, accessedAt, expiresAt, accessCount, embeddingModel, contextKey, contentHash, version)
399
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
400
+ )
401
+ .get(
402
+ id,
403
+ input.agentId ?? null,
404
+ input.scope,
405
+ key,
406
+ input.name,
407
+ input.content,
408
+ input.summary ?? null,
409
+ input.source,
410
+ input.sourceTaskId ?? null,
411
+ input.sourcePath ?? null,
412
+ input.chunkIndex ?? 0,
413
+ input.totalChunks ?? 1,
414
+ JSON.stringify(input.tags ?? []),
415
+ now,
416
+ now,
417
+ now,
418
+ expiresAt,
419
+ 0,
420
+ null,
421
+ input.contextKey ?? null,
422
+ contentHash,
423
+ version,
424
+ );
425
+
426
+ if (!row) throw new Error("Failed to create memory");
427
+ db.prepare(
428
+ `INSERT INTO agent_memory_version (id, memory_id, version, content, contentHash, intent, operation, changedByAgentId, createdAt, updatedAt, created_by, updated_by)
429
+ VALUES (?, ?, ?, ?, ?, ?, 'create', ?, ?, ?, ?, ?)`,
430
+ ).run(
431
+ crypto.randomUUID(),
432
+ row.id,
433
+ version,
273
434
  input.content,
274
- input.summary ?? null,
275
- input.source,
276
- input.sourceTaskId ?? null,
277
- input.sourcePath ?? null,
278
- input.chunkIndex ?? 0,
279
- input.totalChunks ?? 1,
280
- JSON.stringify(input.tags ?? []),
435
+ contentHash,
436
+ input.intent ?? "create memory",
437
+ input.agentId ?? null,
281
438
  now,
282
439
  now,
283
- expiresAt,
284
- 0,
285
- null,
286
- input.contextKey ?? null,
440
+ input.agentId ?? null,
441
+ input.agentId ?? null,
287
442
  );
443
+ });
444
+ tx();
288
445
 
289
446
  if (!row) throw new Error("Failed to create memory");
447
+ this.syncFtsRow(row.id, row.name, row.content);
290
448
  return rowToAgentMemory(row);
291
449
  }
292
450
 
@@ -333,6 +491,26 @@ export class SqliteMemoryStore implements MemoryStore {
333
491
  const { scope = "all", limit = 10, source, isLead = false, includeExpired = false } = options;
334
492
 
335
493
  const health = this.getHealth();
494
+ if (
495
+ isHybridSearchEnabled() &&
496
+ options.queryText &&
497
+ this.ftsInitialized &&
498
+ this.getFtsTableSchema() &&
499
+ health.retrievalMode === "vec" &&
500
+ embedding.length === EMBEDDING_DIMENSIONS
501
+ ) {
502
+ console.log(
503
+ `[memory-search] retrieval_path=hybrid scope=${scope} limit=${limit} vec_rows=${health.counts.memoryVec} searchable=${health.counts.searchable}`,
504
+ );
505
+ return this.searchHybrid(embedding, options.queryText, agentId, {
506
+ scope,
507
+ limit,
508
+ source,
509
+ isLead,
510
+ includeExpired,
511
+ });
512
+ }
513
+
336
514
  if (health.retrievalMode === "vec" && embedding.length === EMBEDDING_DIMENSIONS) {
337
515
  console.log(
338
516
  `[memory-search] retrieval_path=vec scope=${scope} limit=${limit} vec_rows=${health.counts.memoryVec} searchable=${health.counts.searchable}`,
@@ -346,6 +524,19 @@ export class SqliteMemoryStore implements MemoryStore {
346
524
  });
347
525
  }
348
526
 
527
+ if (options.queryText && this.ftsInitialized && this.getFtsTableSchema()) {
528
+ console.log(
529
+ `[memory-search] retrieval_path=fts scope=${scope} limit=${limit} reason=${embedding.length !== EMBEDDING_DIMENSIONS ? "query_dimension_mismatch" : health.reasons.join("|") || "vec_unavailable"}`,
530
+ );
531
+ return this.searchFts(options.queryText, agentId, {
532
+ scope,
533
+ limit,
534
+ source,
535
+ isLead,
536
+ includeExpired,
537
+ });
538
+ }
539
+
349
540
  console.log(
350
541
  `[memory-search] retrieval_path=fallback scope=${scope} limit=${limit} reason=${embedding.length !== EMBEDDING_DIMENSIONS ? "query_dimension_mismatch" : health.reasons.join("|") || "vec_unavailable"}`,
351
542
  );
@@ -358,6 +549,132 @@ export class SqliteMemoryStore implements MemoryStore {
358
549
  });
359
550
  }
360
551
 
552
+ private searchHybrid(
553
+ queryEmbedding: Float32Array,
554
+ queryText: string,
555
+ agentId: string,
556
+ options: {
557
+ scope: string;
558
+ limit: number;
559
+ source?: AgentMemorySource;
560
+ isLead: boolean;
561
+ includeExpired: boolean;
562
+ },
563
+ ): MemoryCandidate[] {
564
+ const overfetchLimit = Math.min(Math.max(options.limit * 4, options.limit), 100);
565
+ const vectorCandidates = this.searchWithVec(queryEmbedding, agentId, {
566
+ ...options,
567
+ limit: overfetchLimit,
568
+ });
569
+ const ftsCandidates = this.searchFts(queryText, agentId, { ...options, limit: overfetchLimit });
570
+ if (ftsCandidates.length === 0) return vectorCandidates.slice(0, options.limit);
571
+
572
+ const byId = new Map<string, MemoryCandidate>();
573
+ const scores = new Map<string, number>();
574
+ const sources = new Map<string, Set<MemoryRetrievalSource>>();
575
+ const now = new Date();
576
+ const add = (candidate: MemoryCandidate, rank: number) => {
577
+ byId.set(candidate.id, byId.get(candidate.id) ?? candidate);
578
+ const retrievalSource = candidate.retrievalSource === "fts" ? "fts" : "vec";
579
+ const candidateSources = sources.get(candidate.id) ?? new Set<MemoryRetrievalSource>();
580
+ candidateSources.add(retrievalSource);
581
+ sources.set(candidate.id, candidateSources);
582
+
583
+ const decay = recencyDecay(candidate.createdAt, now, candidate.source);
584
+ scores.set(candidate.id, (scores.get(candidate.id) ?? 0) + computeRrfScore(rank, decay));
585
+ };
586
+
587
+ vectorCandidates.forEach(add);
588
+ ftsCandidates.forEach(add);
589
+
590
+ return [...byId.values()]
591
+ .map((candidate) => ({
592
+ ...candidate,
593
+ rawSimilarity: candidate.rawSimilarity ?? candidate.similarity,
594
+ similarity: scores.get(candidate.id) ?? candidate.similarity,
595
+ retrievalSource: retrievalSourceFor(sources.get(candidate.id) ?? new Set()),
596
+ recencyDecayApplied: true,
597
+ }))
598
+ .sort((a, b) => b.similarity - a.similarity)
599
+ .slice(0, options.limit);
600
+ }
601
+
602
+ private searchFts(
603
+ queryText: string,
604
+ agentId: string,
605
+ options: {
606
+ scope: string;
607
+ limit: number;
608
+ source?: AgentMemorySource;
609
+ isLead: boolean;
610
+ includeExpired: boolean;
611
+ },
612
+ ): MemoryCandidate[] {
613
+ const match = this.buildFtsMatch(queryText);
614
+ if (!match) return [];
615
+
616
+ const db = getDb();
617
+ const { scope, limit, source, isLead, includeExpired } = options;
618
+ const conditions: string[] = ["memory_fts MATCH ?"];
619
+ const params: (Buffer | string | number | null)[] = [match];
620
+
621
+ this.addScopeConditions(conditions, params, agentId, scope, isLead, "m");
622
+
623
+ if (source) {
624
+ conditions.push("m.source = ?");
625
+ params.push(source);
626
+ }
627
+
628
+ if (!includeExpired) {
629
+ conditions.push("(m.expiresAt IS NULL OR m.expiresAt > datetime('now'))");
630
+ }
631
+
632
+ try {
633
+ const sqlLimit = Math.min(Math.max(limit * 4, limit), 100);
634
+ const rows = db
635
+ .prepare<AgentMemoryRow & { rank: number }, (Buffer | string | number | null)[]>(
636
+ `SELECT m.*, bm25(memory_fts) AS rank
637
+ FROM memory_fts
638
+ JOIN agent_memory m ON m.id = memory_fts.memory_id
639
+ WHERE ${conditions.join(" AND ")}
640
+ ORDER BY rank
641
+ LIMIT ?`,
642
+ )
643
+ .all(...params, sqlLimit);
644
+
645
+ const now = new Date();
646
+ return rows
647
+ .map((row, index) => {
648
+ const rawSimilarity = 1 / (index + 1);
649
+ return {
650
+ ...rowToCandidate(
651
+ row,
652
+ rawSimilarity * recencyDecay(row.createdAt, now, row.source as AgentMemorySource),
653
+ ),
654
+ rawSimilarity,
655
+ retrievalSource: "fts" as const,
656
+ recencyDecayApplied: true,
657
+ };
658
+ })
659
+ .sort((a, b) => b.similarity - a.similarity)
660
+ .slice(0, limit);
661
+ } catch (err) {
662
+ console.warn("[memory-fts] query failed:", (err as Error).message);
663
+ return [];
664
+ }
665
+ }
666
+
667
+ private buildFtsMatch(queryText: string): string | null {
668
+ const terms = queryText
669
+ .trim()
670
+ .split(/[^\p{L}\p{N}_-]+/u)
671
+ .map((term) => term.trim())
672
+ .filter((term) => term.length > 0)
673
+ .slice(0, 12);
674
+ if (terms.length === 0) return null;
675
+ return terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
676
+ }
677
+
361
678
  private searchWithVec(
362
679
  queryEmbedding: Float32Array,
363
680
  agentId: string,
@@ -408,7 +725,7 @@ export class SqliteMemoryStore implements MemoryStore {
408
725
  for (const row of rows) {
409
726
  const similarity = 1 - row.distance;
410
727
  if (similarity < MIN_SIMILARITY) continue;
411
- candidates.push(rowToCandidate(row, similarity));
728
+ candidates.push({ ...rowToCandidate(row, similarity), retrievalSource: "vec" });
412
729
  }
413
730
 
414
731
  return candidates;
@@ -455,7 +772,7 @@ export class SqliteMemoryStore implements MemoryStore {
455
772
  if (emb.length !== queryEmbedding.length) continue;
456
773
  const similarity = cosineSimilarity(queryEmbedding, emb);
457
774
  if (similarity < MIN_SIMILARITY) continue;
458
- candidates.push(rowToCandidate(row, similarity));
775
+ candidates.push({ ...rowToCandidate(row, similarity), retrievalSource: "fallback" });
459
776
  }
460
777
 
461
778
  candidates.sort((a, b) => b.similarity - a.similarity);
@@ -553,6 +870,92 @@ export class SqliteMemoryStore implements MemoryStore {
553
870
  return PROTECTED_SOURCES.has(source);
554
871
  }
555
872
 
873
+ edit(input: MemoryEditInput): MemoryEditResult {
874
+ if (!input.id && !(input.key && input.scope)) {
875
+ throw new Error("memory edit requires either id or key+scope");
876
+ }
877
+
878
+ const db = getDb();
879
+ const row = input.id
880
+ ? db
881
+ .prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
882
+ .get(input.id)
883
+ : db
884
+ .prepare<AgentMemoryRow, [string, string, string | null]>(
885
+ `SELECT * FROM agent_memory
886
+ WHERE key = ? AND scope = ? AND coalesce(agentId, '') = coalesce(?, '')
887
+ ORDER BY chunkIndex ASC
888
+ LIMIT 1`,
889
+ )
890
+ .get(input.key!, input.scope!, input.agentId ?? null);
891
+
892
+ if (!row) throw new Error("memory not found");
893
+ if ((row.totalChunks ?? 1) !== 1)
894
+ throw new Error("memory edit only supports single-chunk rows");
895
+ if (input.expectedVersion && input.expectedVersion !== (row.version ?? 1)) {
896
+ throw new Error("memory version conflict");
897
+ }
898
+
899
+ const previousVersion = row.version ?? 1;
900
+ const nextContent = applyEditMode(input.mode, row.content, {
901
+ content: input.content,
902
+ oldString: input.oldString,
903
+ newString: input.newString,
904
+ });
905
+
906
+ const nextHash = contentSha256(nextContent);
907
+ if (nextHash === row.contentHash) {
908
+ return {
909
+ memory: rowToAgentMemory(row),
910
+ changed: false,
911
+ previousVersion,
912
+ version: previousVersion,
913
+ contentHash: nextHash,
914
+ };
915
+ }
916
+
917
+ const nextVersion = previousVersion + 1;
918
+ const now = new Date().toISOString();
919
+ const tx = db.transaction(() => {
920
+ db.prepare(
921
+ `INSERT INTO agent_memory_version (id, memory_id, version, content, contentHash, intent, operation, changedByAgentId, createdAt, updatedAt, created_by, updated_by)
922
+ VALUES (?, ?, ?, ?, ?, ?, 'edit', ?, ?, ?, ?, ?)`,
923
+ ).run(
924
+ crypto.randomUUID(),
925
+ row.id,
926
+ nextVersion,
927
+ nextContent,
928
+ nextHash,
929
+ input.intent,
930
+ input.changedByAgentId ?? null,
931
+ now,
932
+ now,
933
+ input.changedByAgentId ?? null,
934
+ input.changedByAgentId ?? null,
935
+ );
936
+ db.prepare(
937
+ `UPDATE agent_memory
938
+ SET content = ?, contentHash = ?, version = ?, updatedAt = ?
939
+ WHERE id = ?`,
940
+ ).run(nextContent, nextHash, nextVersion, now, row.id);
941
+ });
942
+ tx();
943
+
944
+ this.syncFtsRow(row.id, row.name, nextContent);
945
+ const updated = db
946
+ .prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
947
+ .get(row.id);
948
+ if (!updated) throw new Error("memory disappeared after edit");
949
+
950
+ return {
951
+ memory: rowToAgentMemory(updated),
952
+ changed: true,
953
+ previousVersion,
954
+ version: nextVersion,
955
+ contentHash: nextHash,
956
+ };
957
+ }
958
+
556
959
  listForCuration(
557
960
  agentId?: string,
558
961
  ): { id: string; source: string; name: string; createdAt: string }[] {
@@ -588,11 +991,19 @@ export class SqliteMemoryStore implements MemoryStore {
588
991
  .all();
589
992
  }
590
993
 
591
- delete(id: string): boolean {
994
+ private purgeByIds(ids: string[]): void {
995
+ if (ids.length === 0) return;
592
996
  const db = getDb();
593
997
  if (this.vecInitialized && this.getVecTableSchema()) {
594
- db.prepare("DELETE FROM memory_vec WHERE memory_id = ?").run(id);
998
+ const placeholders = ids.map(() => "?").join(",");
999
+ db.prepare(`DELETE FROM memory_vec WHERE memory_id IN (${placeholders})`).run(...ids);
595
1000
  }
1001
+ this.deleteFtsRows(ids);
1002
+ }
1003
+
1004
+ delete(id: string): boolean {
1005
+ const db = getDb();
1006
+ this.purgeByIds([id]);
596
1007
  const result = db.prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
597
1008
  return result.changes > 0;
598
1009
  }
@@ -600,26 +1011,18 @@ export class SqliteMemoryStore implements MemoryStore {
600
1011
  deleteBySourcePath(sourcePath: string, agentId: string): number {
601
1012
  const db = getDb();
602
1013
 
603
- if (this.vecInitialized && this.getVecTableSchema()) {
604
- // Get IDs first for vec table cleanup
605
- const ids = db
606
- .prepare<{ id: string }, [string, string]>(
607
- "SELECT id FROM agent_memory WHERE sourcePath = ? AND agentId = ?",
608
- )
609
- .all(sourcePath, agentId);
1014
+ const ids = db
1015
+ .prepare<{ id: string }, [string, string]>(
1016
+ "SELECT id FROM agent_memory WHERE sourcePath = ? AND agentId = ?",
1017
+ )
1018
+ .all(sourcePath, agentId);
610
1019
 
611
- if (ids.length > 0) {
612
- const placeholders = ids.map(() => "?").join(",");
613
- db.prepare(`DELETE FROM memory_vec WHERE memory_id IN (${placeholders})`).run(
614
- ...ids.map((r) => r.id),
615
- );
616
- }
617
- }
1020
+ this.purgeByIds(ids.map((r) => r.id));
618
1021
 
619
1022
  const result = db
620
1023
  .prepare("DELETE FROM agent_memory WHERE sourcePath = ? AND agentId = ?")
621
1024
  .run(sourcePath, agentId);
622
- return result.changes;
1025
+ return ids.length || result.changes;
623
1026
  }
624
1027
 
625
1028
  purgeExpired(): number {
@@ -633,15 +1036,9 @@ export class SqliteMemoryStore implements MemoryStore {
633
1036
 
634
1037
  if (expiredIds.length === 0) return 0;
635
1038
 
636
- if (this.vecInitialized && this.getVecTableSchema()) {
637
- const batchSize = 500;
638
- for (let i = 0; i < expiredIds.length; i += batchSize) {
639
- const batch = expiredIds.slice(i, i + batchSize);
640
- const placeholders = batch.map(() => "?").join(",");
641
- db.prepare(`DELETE FROM memory_vec WHERE memory_id IN (${placeholders})`).run(
642
- ...batch.map((r) => r.id),
643
- );
644
- }
1039
+ const batchSize = 500;
1040
+ for (let i = 0; i < expiredIds.length; i += batchSize) {
1041
+ this.purgeByIds(expiredIds.slice(i, i + batchSize).map((r) => r.id));
645
1042
  }
646
1043
 
647
1044
  const result = db