@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.
- package/README.md +2 -0
- package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
- package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
- package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
- package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
- package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
- package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
- package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
- package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
- package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
- package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
- package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
- package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
- package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
- package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
- package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
- package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
- package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
- package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
- package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
- package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
- package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
- package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
- package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
- package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
- package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
- package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
- package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
- package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
- package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
- package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
- package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
- package/dist/cli.js +10 -10
- package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
- package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
- package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
- package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
- package/dist/http-59ygpn2h.js +17263 -0
- package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
- package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
- package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
- package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
- package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
- package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
- package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
- package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
- package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
- package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
- package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
- package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
- package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
- package/openapi.json +90 -4
- package/package.json +3 -3
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/providers/sqlite-store.ts +468 -71
- package/src/be/memory/raters/retrieval.ts +4 -2
- package/src/be/memory/reranker.ts +5 -2
- package/src/be/memory/retrieval-store.ts +2 -0
- package/src/be/memory/types.ts +46 -0
- package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
- package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
- package/src/be/migrations/101_script_connections.sql +67 -0
- package/src/be/script-connections.ts +744 -0
- package/src/be/script-credential-broker.ts +38 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
- package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
- package/src/commands/runner.ts +8 -6
- package/src/heartbeat/heartbeat.ts +42 -1
- package/src/heartbeat/index.ts +7 -1
- package/src/http/memory.ts +127 -14
- package/src/http/scripts.ts +15 -14
- package/src/scripts-runtime/api-client.ts +100 -0
- package/src/scripts-runtime/api-types.ts +45 -0
- package/src/scripts-runtime/credential-broker/README.md +40 -0
- package/src/scripts-runtime/credential-broker/broker.ts +51 -0
- package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
- package/src/scripts-runtime/credential-broker/index.ts +17 -0
- package/src/scripts-runtime/credential-broker/store.ts +72 -0
- package/src/scripts-runtime/credential-broker/types.ts +54 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/egress-secrets.ts +14 -76
- package/src/scripts-runtime/eval-harness.ts +1 -1
- package/src/scripts-runtime/executors/types.ts +5 -5
- package/src/scripts-runtime/loader.ts +6 -1
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
- package/src/server.ts +6 -0
- package/src/tests/credential-broker.test.ts +281 -0
- package/src/tests/heartbeat.test.ts +239 -0
- package/src/tests/memory-edit.test.ts +177 -0
- package/src/tests/memory-hybrid.test.ts +212 -0
- package/src/tests/runner-repo-autostash.test.ts +39 -8
- package/src/tests/script-connections.test.ts +360 -0
- package/src/tests/scripts-http.test.ts +16 -10
- package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
- package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
- package/src/tools/credential-bindings/index.ts +1 -0
- package/src/tools/credential-bindings/tool.ts +238 -0
- package/src/tools/memory-edit.ts +148 -0
- package/src/tools/memory-search.ts +17 -10
- package/src/tools/schedules/delete-schedule.ts +18 -8
- package/src/tools/schedules/update-schedule.ts +4 -9
- package/src/tools/script-connections/index.ts +1 -0
- package/src/tools/script-connections/tool.ts +167 -0
- package/src/tools/swarm-config/delete-config.ts +5 -0
- package/src/tools/swarm-config/set-config.ts +23 -1
- package/src/tools/tool-config.ts +6 -1
- package/src/types.ts +6 -0
- package/src/workflows/executors/swarm-script.ts +2 -0
- package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
- package/templates/community/dora-metrics/README.md +39 -0
- package/templates/community/dora-metrics/lead-prompt.md +56 -0
- package/templates/community/dora-metrics/report.mjs +393 -0
- package/templates/community/dora-metrics/run.sh +113 -0
- package/templates/schedules/weekly-dora-metrics/config.json +13 -0
- package/templates/schedules/weekly-dora-metrics/content.md +61 -0
- package/dist/cli-2307phk1.js +0 -24223
- 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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
[
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
input.
|
|
276
|
-
input.
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|