@withone/cli 1.42.0 → 1.43.3
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 +57 -17
- package/dist/chunk-3ZJVO4GP.js +59 -0
- package/dist/chunk-44CV5IMX.js +38 -0
- package/dist/chunk-AU2ZEEMS.js +477 -0
- package/dist/chunk-C3ORS3RG.js +762 -0
- package/dist/{chunk-PK2RAVAF.js → chunk-EMAQVPRD.js} +6 -36
- package/dist/chunk-IAFQVFCB.js +1708 -0
- package/dist/chunk-MRGKKO54.js +158 -0
- package/dist/embedding-O4XWBL6T.js +10 -0
- package/dist/{flow-runner-Y2CXXU3U.js → flow-runner-XSJ4US5S.js} +2 -1
- package/dist/index.js +2856 -1691
- package/dist/migrate-BEARUBLO.js +16 -0
- package/dist/runtime-NVC7KAJZ.js +15 -0
- package/dist/sql-EGTWQIU5.js +11 -0
- package/package.json +6 -2
- package/profiles/attio/attioCompanies.json +5 -3
- package/profiles/attio/attioPeople.json +5 -4
- package/skills/one/SKILL.md +83 -10
|
@@ -0,0 +1,1708 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MEMORY_CONFIG,
|
|
3
|
+
defaultSearchableText,
|
|
4
|
+
embed,
|
|
5
|
+
getMemoryConfig,
|
|
6
|
+
getMemoryConfigOrDefault,
|
|
7
|
+
getOpenAiApiKey,
|
|
8
|
+
updateMemoryConfig
|
|
9
|
+
} from "./chunk-AU2ZEEMS.js";
|
|
10
|
+
|
|
11
|
+
// src/lib/memory/schema.ts
|
|
12
|
+
var SCHEMA_VERSION = "2.1.0";
|
|
13
|
+
var EXTENSIONS_SQL = ``;
|
|
14
|
+
var VECTOR_EXTENSION_SQL = `
|
|
15
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
16
|
+
`;
|
|
17
|
+
var TABLES_SQL = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS mem_records (
|
|
19
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
20
|
+
|
|
21
|
+
type TEXT NOT NULL,
|
|
22
|
+
data JSONB NOT NULL,
|
|
23
|
+
tags TEXT[],
|
|
24
|
+
keys TEXT[],
|
|
25
|
+
|
|
26
|
+
sources JSONB NOT NULL DEFAULT '{}',
|
|
27
|
+
|
|
28
|
+
searchable_text TEXT,
|
|
29
|
+
searchable tsvector GENERATED ALWAYS AS (to_tsvector('english', COALESCE(searchable_text, ''))) STORED,
|
|
30
|
+
|
|
31
|
+
content_hash TEXT,
|
|
32
|
+
|
|
33
|
+
weight SMALLINT NOT NULL DEFAULT 5 CHECK (weight BETWEEN 1 AND 10),
|
|
34
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
last_accessed_at TIMESTAMPTZ,
|
|
36
|
+
|
|
37
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),
|
|
38
|
+
archived_reason TEXT,
|
|
39
|
+
|
|
40
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
41
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS mem_links (
|
|
45
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
46
|
+
from_id UUID NOT NULL REFERENCES mem_records(id) ON DELETE CASCADE,
|
|
47
|
+
to_id UUID NOT NULL REFERENCES mem_records(id) ON DELETE CASCADE,
|
|
48
|
+
relation TEXT NOT NULL,
|
|
49
|
+
bidirectional BOOLEAN NOT NULL DEFAULT false,
|
|
50
|
+
metadata JSONB,
|
|
51
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
52
|
+
UNIQUE(from_id, to_id, relation)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS mem_sync_state (
|
|
56
|
+
platform TEXT NOT NULL,
|
|
57
|
+
model TEXT NOT NULL,
|
|
58
|
+
last_sync_at TIMESTAMPTZ,
|
|
59
|
+
last_cursor JSONB,
|
|
60
|
+
since TIMESTAMPTZ,
|
|
61
|
+
total_records INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
pages_processed INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
status TEXT NOT NULL DEFAULT 'idle' CHECK (status IN ('idle', 'syncing', 'failed')),
|
|
64
|
+
last_error TEXT,
|
|
65
|
+
PRIMARY KEY (platform, model)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS mem_meta (
|
|
69
|
+
key TEXT PRIMARY KEY,
|
|
70
|
+
value TEXT NOT NULL,
|
|
71
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
72
|
+
);
|
|
73
|
+
`;
|
|
74
|
+
var VECTOR_COLUMNS_SQL = `
|
|
75
|
+
ALTER TABLE mem_records ADD COLUMN IF NOT EXISTS embedding vector(1536);
|
|
76
|
+
ALTER TABLE mem_records ADD COLUMN IF NOT EXISTS embedded_at TIMESTAMPTZ;
|
|
77
|
+
ALTER TABLE mem_records ADD COLUMN IF NOT EXISTS embedding_model TEXT;
|
|
78
|
+
`;
|
|
79
|
+
var INDEXES_SQL = `
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_records_type ON mem_records(type);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_records_status ON mem_records(status);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_records_keys ON mem_records USING GIN(keys);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_records_tags ON mem_records USING GIN(tags);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_records_data ON mem_records USING GIN(data jsonb_path_ops);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_records_sources ON mem_records USING GIN(sources jsonb_path_ops);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_records_searchable ON mem_records USING GIN(searchable);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_records_relevance ON mem_records(status, weight DESC, access_count DESC, last_accessed_at DESC NULLS LAST);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_records_content_hash ON mem_records(content_hash);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_links_from ON mem_links(from_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_links_to ON mem_links(to_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_links_relation ON mem_links(relation);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_links_bidirectional ON mem_links(bidirectional) WHERE bidirectional = true;
|
|
94
|
+
`;
|
|
95
|
+
var VECTOR_INDEX_SQL = `
|
|
96
|
+
DO $$
|
|
97
|
+
BEGIN
|
|
98
|
+
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_records_embedding') THEN
|
|
99
|
+
CREATE INDEX idx_records_embedding
|
|
100
|
+
ON mem_records USING HNSW(embedding vector_cosine_ops)
|
|
101
|
+
WHERE embedding IS NOT NULL;
|
|
102
|
+
END IF;
|
|
103
|
+
END $$;
|
|
104
|
+
`;
|
|
105
|
+
var FUNCTIONS_SQL = `
|
|
106
|
+
-- Enforce global uniqueness of the "keys" array across records.
|
|
107
|
+
CREATE OR REPLACE FUNCTION mem_enforce_key_uniqueness()
|
|
108
|
+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
109
|
+
DECLARE
|
|
110
|
+
conflicting_id UUID;
|
|
111
|
+
BEGIN
|
|
112
|
+
IF NEW.keys IS NULL THEN RETURN NEW; END IF;
|
|
113
|
+
SELECT id INTO conflicting_id
|
|
114
|
+
FROM mem_records
|
|
115
|
+
WHERE keys && NEW.keys AND id != NEW.id
|
|
116
|
+
LIMIT 1;
|
|
117
|
+
IF conflicting_id IS NOT NULL THEN
|
|
118
|
+
RAISE EXCEPTION 'Key conflict: one or more keys in % already exist on record %',
|
|
119
|
+
NEW.keys, conflicting_id USING ERRCODE = 'unique_violation';
|
|
120
|
+
END IF;
|
|
121
|
+
RETURN NEW;
|
|
122
|
+
END;
|
|
123
|
+
$$;
|
|
124
|
+
|
|
125
|
+
DROP TRIGGER IF EXISTS mem_enforce_key_uniqueness_trigger ON mem_records;
|
|
126
|
+
CREATE TRIGGER mem_enforce_key_uniqueness_trigger
|
|
127
|
+
BEFORE INSERT OR UPDATE ON mem_records
|
|
128
|
+
FOR EACH ROW EXECUTE FUNCTION mem_enforce_key_uniqueness();
|
|
129
|
+
|
|
130
|
+
-- Bump updated_at on write. searchable_text is owned by the caller (profile-driven).
|
|
131
|
+
CREATE OR REPLACE FUNCTION mem_records_touch()
|
|
132
|
+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
133
|
+
BEGIN
|
|
134
|
+
NEW.updated_at := NOW();
|
|
135
|
+
RETURN NEW;
|
|
136
|
+
END;
|
|
137
|
+
$$;
|
|
138
|
+
|
|
139
|
+
DROP TRIGGER IF EXISTS mem_records_touch_trigger ON mem_records;
|
|
140
|
+
CREATE TRIGGER mem_records_touch_trigger
|
|
141
|
+
BEFORE UPDATE ON mem_records
|
|
142
|
+
FOR EACH ROW EXECUTE FUNCTION mem_records_touch();
|
|
143
|
+
|
|
144
|
+
-- Relevance scoring (mirrors TS scoring.ts). See docs/plans/unified-memory.md \xA74.5.
|
|
145
|
+
CREATE OR REPLACE FUNCTION mem_calculate_relevance(
|
|
146
|
+
p_weight INTEGER,
|
|
147
|
+
p_access_count INTEGER,
|
|
148
|
+
p_last_accessed_at TIMESTAMPTZ,
|
|
149
|
+
p_created_at TIMESTAMPTZ,
|
|
150
|
+
max_access_count INTEGER DEFAULT 100
|
|
151
|
+
) RETURNS FLOAT LANGUAGE plpgsql AS $$
|
|
152
|
+
DECLARE
|
|
153
|
+
weight_score FLOAT;
|
|
154
|
+
access_score FLOAT;
|
|
155
|
+
recency_score FLOAT;
|
|
156
|
+
days_since_access FLOAT;
|
|
157
|
+
BEGIN
|
|
158
|
+
weight_score := (p_weight - 1) / 9.0;
|
|
159
|
+
access_score := LEAST(p_access_count::FLOAT / max_access_count, 1.0);
|
|
160
|
+
|
|
161
|
+
IF p_last_accessed_at IS NOT NULL THEN
|
|
162
|
+
days_since_access := EXTRACT(EPOCH FROM (NOW() - p_last_accessed_at)) / 86400.0;
|
|
163
|
+
recency_score := GREATEST(1.0 - (days_since_access / 30.0) * 0.9, 0.1);
|
|
164
|
+
ELSE
|
|
165
|
+
days_since_access := EXTRACT(EPOCH FROM (NOW() - p_created_at)) / 86400.0;
|
|
166
|
+
recency_score := GREATEST(0.5 - (days_since_access / 60.0) * 0.4, 0.1);
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
RETURN (weight_score * 0.4) + (access_score * 0.3) + (recency_score * 0.3);
|
|
170
|
+
END;
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
-- Access tracking for search results.
|
|
174
|
+
CREATE OR REPLACE FUNCTION mem_increment_access(record_ids UUID[])
|
|
175
|
+
RETURNS VOID LANGUAGE plpgsql AS $$
|
|
176
|
+
BEGIN
|
|
177
|
+
UPDATE mem_records
|
|
178
|
+
SET access_count = access_count + 1,
|
|
179
|
+
last_accessed_at = NOW()
|
|
180
|
+
WHERE id = ANY(record_ids);
|
|
181
|
+
END;
|
|
182
|
+
$$;
|
|
183
|
+
|
|
184
|
+
-- Upsert by keys: merge (or replace) into the first record whose keys overlap, else insert new.
|
|
185
|
+
-- Returns the resulting record id + whether the operation was insert or update.
|
|
186
|
+
--
|
|
187
|
+
-- p_replace=FALSE (default): shallow-merge existing data with p_data. Right
|
|
188
|
+
-- semantics for user-authored memories that get progressively enriched.
|
|
189
|
+
-- p_replace=TRUE: REPLACE data with p_data. Right for synced rows \u2014 if the
|
|
190
|
+
-- source removed a field, it must disappear from memory too.
|
|
191
|
+
--
|
|
192
|
+
-- Upsert-by-keys is the self-heal primitive for --full-refresh reconcile:
|
|
193
|
+
-- if the upsert finds an archived row, the source re-surfaced it, so flip
|
|
194
|
+
-- status back to 'active' and clear archived_reason. Without this, rows
|
|
195
|
+
-- archived by a buggy reconcile would stay dead until manually un-archived.
|
|
196
|
+
-- Core (no-vector) variant of mem_upsert_by_keys. p_embedding and
|
|
197
|
+
-- p_embedding_model are accepted for source compatibility with the
|
|
198
|
+
-- vector variant but are ignored \u2014 the embedding column doesn't exist
|
|
199
|
+
-- in this schema. The vector variant (VECTOR_FUNCTIONS_SQL) overrides
|
|
200
|
+
-- this with CREATE OR REPLACE when caps.vectorSearch is true.
|
|
201
|
+
--
|
|
202
|
+
-- p_embedding is TEXT (not vector(1536)) so this function compiles on
|
|
203
|
+
-- backends without pgvector loaded. The vector variant uses the same
|
|
204
|
+
-- TEXT signature and casts to vector inside the body.
|
|
205
|
+
CREATE OR REPLACE FUNCTION mem_upsert_by_keys(
|
|
206
|
+
p_type TEXT,
|
|
207
|
+
p_data JSONB,
|
|
208
|
+
p_tags TEXT[],
|
|
209
|
+
p_keys TEXT[],
|
|
210
|
+
p_sources JSONB,
|
|
211
|
+
p_searchable_text TEXT,
|
|
212
|
+
p_content_hash TEXT,
|
|
213
|
+
p_weight INTEGER DEFAULT NULL,
|
|
214
|
+
p_embedding TEXT DEFAULT NULL,
|
|
215
|
+
p_embedding_model TEXT DEFAULT NULL,
|
|
216
|
+
p_replace BOOLEAN DEFAULT FALSE
|
|
217
|
+
) RETURNS TABLE (id UUID, action TEXT) LANGUAGE plpgsql AS $$
|
|
218
|
+
DECLARE
|
|
219
|
+
existing_id UUID;
|
|
220
|
+
result_id UUID;
|
|
221
|
+
result_action TEXT;
|
|
222
|
+
BEGIN
|
|
223
|
+
-- Deterministic pick when more than one row's keys[] overlap p_keys
|
|
224
|
+
-- (e.g. the incoming row identity-merges with an old Attio-id row AND
|
|
225
|
+
-- a newer email-keyed row from a separate sync). Newest-updated wins;
|
|
226
|
+
-- ties broken by id. Without ORDER BY, PG returns whichever row the
|
|
227
|
+
-- planner happens to surface first, leaving the loser persisted with
|
|
228
|
+
-- overlapping keys and breaking the keys-are-unique invariant.
|
|
229
|
+
SELECT r.id INTO existing_id
|
|
230
|
+
FROM mem_records r
|
|
231
|
+
WHERE r.keys && p_keys
|
|
232
|
+
ORDER BY r.updated_at DESC NULLS LAST, r.id ASC
|
|
233
|
+
LIMIT 1;
|
|
234
|
+
|
|
235
|
+
IF existing_id IS NOT NULL THEN
|
|
236
|
+
-- p_replace=TRUE: this call is the new ground truth for the row's
|
|
237
|
+
-- content (typical for sync runs where a field disappearing
|
|
238
|
+
-- upstream must disappear in memory). Replace data, tags,
|
|
239
|
+
-- searchable_text, content_hash with what the caller supplied.
|
|
240
|
+
-- Keys still union (never lose a key \u2014 losing the identity key
|
|
241
|
+
-- would orphan the row), and sources still merge (multi-source
|
|
242
|
+
-- provenance is independent of any single source's payload).
|
|
243
|
+
UPDATE mem_records r
|
|
244
|
+
SET data = CASE WHEN p_replace THEN p_data ELSE r.data || p_data END,
|
|
245
|
+
tags = CASE
|
|
246
|
+
WHEN p_replace THEN COALESCE(p_tags, '{}'::text[])
|
|
247
|
+
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.tags, '{}') || COALESCE(p_tags, '{}'))))
|
|
248
|
+
END,
|
|
249
|
+
keys = (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.keys, '{}') || COALESCE(p_keys, '{}')))),
|
|
250
|
+
sources = r.sources || COALESCE(p_sources, '{}'::jsonb),
|
|
251
|
+
searchable_text = CASE
|
|
252
|
+
WHEN p_replace THEN p_searchable_text
|
|
253
|
+
ELSE COALESCE(p_searchable_text, r.searchable_text)
|
|
254
|
+
END,
|
|
255
|
+
content_hash = CASE
|
|
256
|
+
WHEN p_replace THEN p_content_hash
|
|
257
|
+
ELSE COALESCE(p_content_hash, r.content_hash)
|
|
258
|
+
END,
|
|
259
|
+
weight = COALESCE(p_weight, r.weight),
|
|
260
|
+
status = 'active',
|
|
261
|
+
archived_reason = NULL
|
|
262
|
+
WHERE r.id = existing_id;
|
|
263
|
+
|
|
264
|
+
result_id := existing_id;
|
|
265
|
+
result_action := 'updated';
|
|
266
|
+
ELSE
|
|
267
|
+
INSERT INTO mem_records (
|
|
268
|
+
type, data, tags, keys, sources, searchable_text, content_hash, weight
|
|
269
|
+
) VALUES (
|
|
270
|
+
p_type,
|
|
271
|
+
p_data,
|
|
272
|
+
p_tags,
|
|
273
|
+
p_keys,
|
|
274
|
+
COALESCE(p_sources, '{}'::jsonb),
|
|
275
|
+
p_searchable_text,
|
|
276
|
+
p_content_hash,
|
|
277
|
+
COALESCE(p_weight, 5)
|
|
278
|
+
)
|
|
279
|
+
RETURNING mem_records.id INTO result_id;
|
|
280
|
+
result_action := 'inserted';
|
|
281
|
+
END IF;
|
|
282
|
+
|
|
283
|
+
RETURN QUERY SELECT result_id, result_action;
|
|
284
|
+
END;
|
|
285
|
+
$$;
|
|
286
|
+
`;
|
|
287
|
+
var VECTOR_FUNCTIONS_SQL = `
|
|
288
|
+
CREATE OR REPLACE FUNCTION mem_upsert_by_keys(
|
|
289
|
+
p_type TEXT,
|
|
290
|
+
p_data JSONB,
|
|
291
|
+
p_tags TEXT[],
|
|
292
|
+
p_keys TEXT[],
|
|
293
|
+
p_sources JSONB,
|
|
294
|
+
p_searchable_text TEXT,
|
|
295
|
+
p_content_hash TEXT,
|
|
296
|
+
p_weight INTEGER DEFAULT NULL,
|
|
297
|
+
p_embedding TEXT DEFAULT NULL,
|
|
298
|
+
p_embedding_model TEXT DEFAULT NULL,
|
|
299
|
+
p_replace BOOLEAN DEFAULT FALSE
|
|
300
|
+
) RETURNS TABLE (id UUID, action TEXT) LANGUAGE plpgsql AS $$
|
|
301
|
+
DECLARE
|
|
302
|
+
existing_id UUID;
|
|
303
|
+
result_id UUID;
|
|
304
|
+
result_action TEXT;
|
|
305
|
+
v_embedding vector(1536);
|
|
306
|
+
BEGIN
|
|
307
|
+
v_embedding := CASE WHEN p_embedding IS NOT NULL THEN p_embedding::vector ELSE NULL END;
|
|
308
|
+
|
|
309
|
+
-- See no-vector variant for ORDER BY rationale (deterministic pick
|
|
310
|
+
-- when multiple rows' keys overlap p_keys).
|
|
311
|
+
SELECT r.id INTO existing_id
|
|
312
|
+
FROM mem_records r
|
|
313
|
+
WHERE r.keys && p_keys
|
|
314
|
+
ORDER BY r.updated_at DESC NULLS LAST, r.id ASC
|
|
315
|
+
LIMIT 1;
|
|
316
|
+
|
|
317
|
+
IF existing_id IS NOT NULL THEN
|
|
318
|
+
-- See no-vector variant for p_replace gating rationale (replace
|
|
319
|
+
-- data, tags, searchable_text, content_hash; keys union, sources
|
|
320
|
+
-- merge; embedding follows existing v_embedding logic).
|
|
321
|
+
UPDATE mem_records r
|
|
322
|
+
SET data = CASE WHEN p_replace THEN p_data ELSE r.data || p_data END,
|
|
323
|
+
tags = CASE
|
|
324
|
+
WHEN p_replace THEN COALESCE(p_tags, '{}'::text[])
|
|
325
|
+
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.tags, '{}') || COALESCE(p_tags, '{}'))))
|
|
326
|
+
END,
|
|
327
|
+
keys = (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.keys, '{}') || COALESCE(p_keys, '{}')))),
|
|
328
|
+
sources = r.sources || COALESCE(p_sources, '{}'::jsonb),
|
|
329
|
+
searchable_text = CASE
|
|
330
|
+
WHEN p_replace THEN p_searchable_text
|
|
331
|
+
ELSE COALESCE(p_searchable_text, r.searchable_text)
|
|
332
|
+
END,
|
|
333
|
+
content_hash = CASE
|
|
334
|
+
WHEN p_replace THEN p_content_hash
|
|
335
|
+
ELSE COALESCE(p_content_hash, r.content_hash)
|
|
336
|
+
END,
|
|
337
|
+
weight = COALESCE(p_weight, r.weight),
|
|
338
|
+
embedding = COALESCE(v_embedding, r.embedding),
|
|
339
|
+
embedded_at = CASE WHEN v_embedding IS NOT NULL THEN NOW() ELSE r.embedded_at END,
|
|
340
|
+
embedding_model = COALESCE(p_embedding_model, r.embedding_model),
|
|
341
|
+
status = 'active',
|
|
342
|
+
archived_reason = NULL
|
|
343
|
+
WHERE r.id = existing_id;
|
|
344
|
+
|
|
345
|
+
result_id := existing_id;
|
|
346
|
+
result_action := 'updated';
|
|
347
|
+
ELSE
|
|
348
|
+
INSERT INTO mem_records (
|
|
349
|
+
type, data, tags, keys, sources, searchable_text, content_hash,
|
|
350
|
+
weight, embedding, embedded_at, embedding_model
|
|
351
|
+
) VALUES (
|
|
352
|
+
p_type,
|
|
353
|
+
p_data,
|
|
354
|
+
p_tags,
|
|
355
|
+
p_keys,
|
|
356
|
+
COALESCE(p_sources, '{}'::jsonb),
|
|
357
|
+
p_searchable_text,
|
|
358
|
+
p_content_hash,
|
|
359
|
+
COALESCE(p_weight, 5),
|
|
360
|
+
v_embedding,
|
|
361
|
+
CASE WHEN v_embedding IS NOT NULL THEN NOW() ELSE NULL END,
|
|
362
|
+
p_embedding_model
|
|
363
|
+
)
|
|
364
|
+
RETURNING mem_records.id INTO result_id;
|
|
365
|
+
result_action := 'inserted';
|
|
366
|
+
END IF;
|
|
367
|
+
|
|
368
|
+
RETURN QUERY SELECT result_id, result_action;
|
|
369
|
+
END;
|
|
370
|
+
$$;
|
|
371
|
+
`;
|
|
372
|
+
var HYBRID_SEARCH_SQL = `
|
|
373
|
+
CREATE OR REPLACE FUNCTION mem_hybrid_search(
|
|
374
|
+
query_text TEXT,
|
|
375
|
+
query_embedding vector(1536),
|
|
376
|
+
match_count INT DEFAULT 10,
|
|
377
|
+
filter_type TEXT DEFAULT NULL,
|
|
378
|
+
full_text_weight FLOAT DEFAULT 0.3,
|
|
379
|
+
semantic_weight FLOAT DEFAULT 0.7,
|
|
380
|
+
rrf_k INT DEFAULT 50,
|
|
381
|
+
include_archived BOOLEAN DEFAULT FALSE
|
|
382
|
+
) RETURNS TABLE (
|
|
383
|
+
id UUID,
|
|
384
|
+
type TEXT,
|
|
385
|
+
data JSONB,
|
|
386
|
+
tags TEXT[],
|
|
387
|
+
fts_rank FLOAT,
|
|
388
|
+
semantic_rank FLOAT,
|
|
389
|
+
combined_score FLOAT
|
|
390
|
+
) LANGUAGE plpgsql AS $$
|
|
391
|
+
BEGIN
|
|
392
|
+
RETURN QUERY
|
|
393
|
+
WITH fts_results AS (
|
|
394
|
+
SELECT r.id,
|
|
395
|
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(r.searchable, websearch_to_tsquery('english', query_text)) DESC) AS rank
|
|
396
|
+
FROM mem_records r
|
|
397
|
+
WHERE r.searchable @@ websearch_to_tsquery('english', query_text)
|
|
398
|
+
AND (filter_type IS NULL OR r.type = filter_type)
|
|
399
|
+
AND (include_archived OR r.status = 'active')
|
|
400
|
+
LIMIT match_count * 2
|
|
401
|
+
),
|
|
402
|
+
semantic_results AS (
|
|
403
|
+
SELECT r.id,
|
|
404
|
+
ROW_NUMBER() OVER (ORDER BY r.embedding <=> query_embedding) AS rank
|
|
405
|
+
FROM mem_records r
|
|
406
|
+
WHERE r.embedding IS NOT NULL
|
|
407
|
+
AND (filter_type IS NULL OR r.type = filter_type)
|
|
408
|
+
AND (include_archived OR r.status = 'active')
|
|
409
|
+
ORDER BY r.embedding <=> query_embedding
|
|
410
|
+
LIMIT match_count * 2
|
|
411
|
+
),
|
|
412
|
+
combined AS (
|
|
413
|
+
SELECT COALESCE(fts.id, sem.id) AS id,
|
|
414
|
+
COALESCE(1.0 / (rrf_k + fts.rank), 0.0) AS fts_score,
|
|
415
|
+
COALESCE(1.0 / (rrf_k + sem.rank), 0.0) AS sem_score
|
|
416
|
+
FROM fts_results fts
|
|
417
|
+
FULL OUTER JOIN semantic_results sem ON fts.id = sem.id
|
|
418
|
+
)
|
|
419
|
+
SELECT r.id,
|
|
420
|
+
r.type,
|
|
421
|
+
r.data,
|
|
422
|
+
r.tags,
|
|
423
|
+
c.fts_score::FLOAT AS fts_rank,
|
|
424
|
+
c.sem_score::FLOAT AS semantic_rank,
|
|
425
|
+
(c.fts_score * full_text_weight + c.sem_score * semantic_weight)::FLOAT AS combined_score
|
|
426
|
+
FROM combined c
|
|
427
|
+
JOIN mem_records r ON r.id = c.id
|
|
428
|
+
ORDER BY (c.fts_score * full_text_weight + c.sem_score * semantic_weight) DESC
|
|
429
|
+
LIMIT match_count;
|
|
430
|
+
END;
|
|
431
|
+
$$;
|
|
432
|
+
`;
|
|
433
|
+
function getMetaInsertSQL(version) {
|
|
434
|
+
return `INSERT INTO mem_meta (key, value) VALUES ('version', '${version}')
|
|
435
|
+
ON CONFLICT (key) DO UPDATE SET value = '${version}', updated_at = NOW();`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/lib/memory/plugins/postgres-core/sql-guard.ts
|
|
439
|
+
var ALLOWED_LEADING = /^\s*(SELECT|WITH|EXPLAIN)\b/i;
|
|
440
|
+
var FORBIDDEN = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|TRUNCATE|COPY|GRANT|REVOKE|VACUUM|ATTACH|DETACH|PRAGMA|CALL|LOAD|RESET|SET\s+SESSION|SET\s+LOCAL|DO|COMMIT|ROLLBACK|BEGIN|START)\b/i;
|
|
441
|
+
function validateReadOnlySql(sql) {
|
|
442
|
+
const trimmed = sql.trim();
|
|
443
|
+
if (!trimmed) {
|
|
444
|
+
throw new Error("SQL is empty.");
|
|
445
|
+
}
|
|
446
|
+
if (!ALLOWED_LEADING.test(trimmed)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
"Only SELECT / WITH / EXPLAIN statements are allowed. The unified memory store is read-only from this surface \u2014 use `one mem add` / `one sync run` for writes."
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const stripped = trimmed.replace(/;\s*$/, "");
|
|
452
|
+
if (stripped.includes(";")) {
|
|
453
|
+
throw new Error("Multi-statement SQL is not allowed \u2014 submit a single SELECT / WITH / EXPLAIN.");
|
|
454
|
+
}
|
|
455
|
+
if (FORBIDDEN.test(stripped)) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"DDL / DML / session-control keywords are blocked. Allowed: SELECT, WITH, EXPLAIN, plus standard read-only operators (JOIN, WHERE, GROUP BY, aggregates, JSONB path operators)."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/lib/memory/plugins/postgres-core/client.ts
|
|
463
|
+
function vectorLiteral(embedding) {
|
|
464
|
+
if (!embedding || embedding.length === 0) return null;
|
|
465
|
+
return "[" + embedding.join(",") + "]";
|
|
466
|
+
}
|
|
467
|
+
function jsonPathArray(dotPath) {
|
|
468
|
+
const parts = dotPath.split(".").map((p) => p.replace(/"/g, '""'));
|
|
469
|
+
return "{" + parts.map((p) => `"${p}"`).join(",") + "}";
|
|
470
|
+
}
|
|
471
|
+
function hotColumnIndexName(type, jsonPath) {
|
|
472
|
+
const slug = (s) => s.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase();
|
|
473
|
+
const name = `idx_records_${slug(type)}_${slug(jsonPath)}`;
|
|
474
|
+
return name.slice(0, 63);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/lib/memory/plugins/postgres-core/backend.ts
|
|
478
|
+
var SCHEMA_LOCK_ID = 7193095520723763200n.toString();
|
|
479
|
+
function toRecord(row) {
|
|
480
|
+
return {
|
|
481
|
+
id: row.id,
|
|
482
|
+
type: row.type,
|
|
483
|
+
data: row.data,
|
|
484
|
+
tags: row.tags ?? void 0,
|
|
485
|
+
keys: row.keys ?? void 0,
|
|
486
|
+
sources: row.sources ?? {},
|
|
487
|
+
searchable_text: row.searchable_text,
|
|
488
|
+
embedded_at: row.embedded_at,
|
|
489
|
+
embedding_model: row.embedding_model,
|
|
490
|
+
content_hash: row.content_hash,
|
|
491
|
+
weight: row.weight,
|
|
492
|
+
access_count: row.access_count,
|
|
493
|
+
last_accessed_at: row.last_accessed_at,
|
|
494
|
+
status: row.status,
|
|
495
|
+
archived_reason: row.archived_reason,
|
|
496
|
+
created_at: row.created_at,
|
|
497
|
+
updated_at: row.updated_at
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
var CoreBackend = class {
|
|
501
|
+
constructor(client, caps) {
|
|
502
|
+
this.client = client;
|
|
503
|
+
this.caps = caps;
|
|
504
|
+
}
|
|
505
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
506
|
+
async init() {
|
|
507
|
+
}
|
|
508
|
+
async close() {
|
|
509
|
+
await this.client.close();
|
|
510
|
+
}
|
|
511
|
+
async ensureSchema() {
|
|
512
|
+
const caps = this.caps;
|
|
513
|
+
await this.client.transaction(async (tx) => {
|
|
514
|
+
try {
|
|
515
|
+
await tx.query(`SELECT pg_advisory_xact_lock(${SCHEMA_LOCK_ID}::bigint)`);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
if (EXTENSIONS_SQL.trim()) await tx.query(EXTENSIONS_SQL);
|
|
519
|
+
await tx.query(TABLES_SQL);
|
|
520
|
+
await tx.query(INDEXES_SQL);
|
|
521
|
+
await tx.query(FUNCTIONS_SQL);
|
|
522
|
+
if (caps.vectorSearch) {
|
|
523
|
+
await tx.query(VECTOR_EXTENSION_SQL);
|
|
524
|
+
await tx.query(VECTOR_COLUMNS_SQL);
|
|
525
|
+
await tx.query(VECTOR_FUNCTIONS_SQL);
|
|
526
|
+
await tx.query(VECTOR_INDEX_SQL);
|
|
527
|
+
await tx.query(HYBRID_SEARCH_SQL);
|
|
528
|
+
}
|
|
529
|
+
await tx.query(getMetaInsertSQL(SCHEMA_VERSION));
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async getSchemaVersion() {
|
|
533
|
+
const res = await this.client.query(
|
|
534
|
+
`SELECT value FROM mem_meta WHERE key = 'version'`
|
|
535
|
+
);
|
|
536
|
+
return res.rows[0]?.value ?? null;
|
|
537
|
+
}
|
|
538
|
+
// ── Records ──────────────────────────────────────────────────────────
|
|
539
|
+
async insert(row) {
|
|
540
|
+
const embedding = vectorLiteral(row.embedding ?? null);
|
|
541
|
+
const sql = this.caps.vectorSearch ? `INSERT INTO mem_records
|
|
542
|
+
(type, data, tags, keys, sources, searchable_text, content_hash, weight,
|
|
543
|
+
embedding, embedded_at, embedding_model)
|
|
544
|
+
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::jsonb, $6, $7, $8,
|
|
545
|
+
$9::vector, CASE WHEN $9 IS NOT NULL THEN NOW() ELSE NULL END, $10)
|
|
546
|
+
RETURNING *` : `INSERT INTO mem_records
|
|
547
|
+
(type, data, tags, keys, sources, searchable_text, content_hash, weight)
|
|
548
|
+
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::jsonb, $6, $7, $8)
|
|
549
|
+
RETURNING *`;
|
|
550
|
+
const params = this.caps.vectorSearch ? [
|
|
551
|
+
row.type,
|
|
552
|
+
JSON.stringify(row.data),
|
|
553
|
+
row.tags ?? null,
|
|
554
|
+
row.keys ?? null,
|
|
555
|
+
JSON.stringify(row.sources ?? {}),
|
|
556
|
+
row.searchable_text ?? null,
|
|
557
|
+
row.content_hash ?? null,
|
|
558
|
+
row.weight ?? 5,
|
|
559
|
+
embedding,
|
|
560
|
+
row.embedding_model ?? null
|
|
561
|
+
] : [
|
|
562
|
+
row.type,
|
|
563
|
+
JSON.stringify(row.data),
|
|
564
|
+
row.tags ?? null,
|
|
565
|
+
row.keys ?? null,
|
|
566
|
+
JSON.stringify(row.sources ?? {}),
|
|
567
|
+
row.searchable_text ?? null,
|
|
568
|
+
row.content_hash ?? null,
|
|
569
|
+
row.weight ?? 5
|
|
570
|
+
];
|
|
571
|
+
const res = await this.client.query(sql, params);
|
|
572
|
+
return toRecord(res.rows[0]);
|
|
573
|
+
}
|
|
574
|
+
async upsertByKeys(row, opts = {}) {
|
|
575
|
+
const embedding = vectorLiteral(row.embedding ?? null);
|
|
576
|
+
const embeddingModel = row.embedding_model ?? null;
|
|
577
|
+
const res = await this.client.query(
|
|
578
|
+
`SELECT id, action FROM mem_upsert_by_keys(
|
|
579
|
+
$1::text, $2::jsonb, $3::text[], $4::text[], $5::jsonb, $6::text, $7::text,
|
|
580
|
+
$8::integer, $9::text, $10::text, $11::boolean
|
|
581
|
+
)`,
|
|
582
|
+
[
|
|
583
|
+
row.type,
|
|
584
|
+
JSON.stringify(row.data),
|
|
585
|
+
row.tags ?? null,
|
|
586
|
+
row.keys ?? null,
|
|
587
|
+
JSON.stringify(row.sources ?? {}),
|
|
588
|
+
row.searchable_text ?? null,
|
|
589
|
+
row.content_hash ?? null,
|
|
590
|
+
row.weight ?? null,
|
|
591
|
+
embedding,
|
|
592
|
+
embeddingModel,
|
|
593
|
+
opts.replace ?? false
|
|
594
|
+
]
|
|
595
|
+
);
|
|
596
|
+
const { id, action } = res.rows[0];
|
|
597
|
+
const record = await this.getById(id);
|
|
598
|
+
if (!record) throw new Error(`upsertByKeys: record ${id} vanished mid-operation`);
|
|
599
|
+
return { record, action };
|
|
600
|
+
}
|
|
601
|
+
async getById(id, opts = {}) {
|
|
602
|
+
const res = await this.client.query(
|
|
603
|
+
`SELECT * FROM mem_records WHERE id = $1`,
|
|
604
|
+
[id]
|
|
605
|
+
);
|
|
606
|
+
const row = res.rows[0];
|
|
607
|
+
if (!row) return null;
|
|
608
|
+
const rec = toRecord(row);
|
|
609
|
+
if (!opts.withLinks) return rec;
|
|
610
|
+
const outgoing = await this.linked(id, { direction: "outgoing" });
|
|
611
|
+
const incoming = await this.linked(id, { direction: "incoming" });
|
|
612
|
+
return { ...rec, outgoing, incoming };
|
|
613
|
+
}
|
|
614
|
+
async update(id, patch) {
|
|
615
|
+
const existing = await this.getById(id);
|
|
616
|
+
if (!existing) return null;
|
|
617
|
+
const data = patch.data ? { ...existing.data, ...patch.data } : existing.data;
|
|
618
|
+
const tags = patch.tags ?? existing.tags ?? null;
|
|
619
|
+
const keys = patch.keys ?? existing.keys ?? null;
|
|
620
|
+
const sources = patch.sources ? { ...existing.sources, ...patch.sources } : existing.sources;
|
|
621
|
+
const searchable = patch.searchable_text ?? existing.searchable_text ?? null;
|
|
622
|
+
const hash = patch.content_hash ?? existing.content_hash ?? null;
|
|
623
|
+
const weight = patch.weight ?? existing.weight;
|
|
624
|
+
const res = await this.client.query(
|
|
625
|
+
`UPDATE mem_records
|
|
626
|
+
SET data = $2::jsonb,
|
|
627
|
+
tags = $3::text[],
|
|
628
|
+
keys = $4::text[],
|
|
629
|
+
sources = $5::jsonb,
|
|
630
|
+
searchable_text = $6,
|
|
631
|
+
content_hash = $7,
|
|
632
|
+
weight = $8
|
|
633
|
+
WHERE id = $1
|
|
634
|
+
RETURNING *`,
|
|
635
|
+
[id, JSON.stringify(data), tags, keys, JSON.stringify(sources), searchable, hash, weight]
|
|
636
|
+
);
|
|
637
|
+
return res.rows[0] ? toRecord(res.rows[0]) : null;
|
|
638
|
+
}
|
|
639
|
+
async remove(id) {
|
|
640
|
+
const res = await this.client.query(`DELETE FROM mem_records WHERE id = $1`, [id]);
|
|
641
|
+
return (res.rowCount ?? 0) > 0;
|
|
642
|
+
}
|
|
643
|
+
async archive(id, reason) {
|
|
644
|
+
const res = await this.client.query(
|
|
645
|
+
`UPDATE mem_records SET status = 'archived', archived_reason = $2
|
|
646
|
+
WHERE id = $1 AND status = 'active'`,
|
|
647
|
+
[id, reason ?? null]
|
|
648
|
+
);
|
|
649
|
+
return (res.rowCount ?? 0) > 0;
|
|
650
|
+
}
|
|
651
|
+
async unarchive(id) {
|
|
652
|
+
const res = await this.client.query(
|
|
653
|
+
`UPDATE mem_records SET status = 'active', archived_reason = NULL
|
|
654
|
+
WHERE id = $1 AND status = 'archived'`,
|
|
655
|
+
[id]
|
|
656
|
+
);
|
|
657
|
+
return (res.rowCount ?? 0) > 0;
|
|
658
|
+
}
|
|
659
|
+
async list(type, opts = {}) {
|
|
660
|
+
const limit = opts.limit ?? 100;
|
|
661
|
+
const offset = opts.offset ?? 0;
|
|
662
|
+
const status = opts.status ?? "active";
|
|
663
|
+
const res = await this.client.query(
|
|
664
|
+
`SELECT * FROM mem_records
|
|
665
|
+
WHERE type = $1 AND status = $2
|
|
666
|
+
ORDER BY updated_at DESC
|
|
667
|
+
LIMIT $3 OFFSET $4`,
|
|
668
|
+
[type, status, limit, offset]
|
|
669
|
+
);
|
|
670
|
+
return res.rows.map(toRecord);
|
|
671
|
+
}
|
|
672
|
+
async count(type, opts = {}) {
|
|
673
|
+
const status = opts.status ?? "active";
|
|
674
|
+
if (status === "all") {
|
|
675
|
+
const res2 = await this.client.query(
|
|
676
|
+
`SELECT COUNT(*)::text AS count FROM mem_records WHERE type = $1`,
|
|
677
|
+
[type]
|
|
678
|
+
);
|
|
679
|
+
return Number(res2.rows[0]?.count ?? 0);
|
|
680
|
+
}
|
|
681
|
+
const res = await this.client.query(
|
|
682
|
+
`SELECT COUNT(*)::text AS count FROM mem_records WHERE type = $1 AND status = $2`,
|
|
683
|
+
[type, status]
|
|
684
|
+
);
|
|
685
|
+
return Number(res.rows[0]?.count ?? 0);
|
|
686
|
+
}
|
|
687
|
+
async listForReindex(opts = {}) {
|
|
688
|
+
if (!this.caps.vectorSearch) return [];
|
|
689
|
+
const limit = opts.limit ?? 1e3;
|
|
690
|
+
const offset = opts.offset ?? 0;
|
|
691
|
+
const params = [];
|
|
692
|
+
const clauses = [
|
|
693
|
+
`status = 'active'`,
|
|
694
|
+
`searchable_text IS NOT NULL`,
|
|
695
|
+
`searchable_text <> ''`
|
|
696
|
+
];
|
|
697
|
+
if (opts.type) {
|
|
698
|
+
params.push(opts.type);
|
|
699
|
+
clauses.push(`type = $${params.length}`);
|
|
700
|
+
}
|
|
701
|
+
if (!opts.includeAlreadyEmbedded) {
|
|
702
|
+
if (opts.targetEmbeddingModel) {
|
|
703
|
+
params.push(opts.targetEmbeddingModel);
|
|
704
|
+
clauses.push(`(embedded_at IS NULL OR embedding_model IS DISTINCT FROM $${params.length})`);
|
|
705
|
+
} else {
|
|
706
|
+
clauses.push(`embedded_at IS NULL`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
params.push(limit);
|
|
710
|
+
params.push(offset);
|
|
711
|
+
const res = await this.client.query(
|
|
712
|
+
`SELECT id, type, searchable_text, content_hash, embedding_model
|
|
713
|
+
FROM mem_records
|
|
714
|
+
WHERE ${clauses.join(" AND ")}
|
|
715
|
+
ORDER BY embedded_at ASC NULLS FIRST, id ASC
|
|
716
|
+
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
|
717
|
+
params
|
|
718
|
+
);
|
|
719
|
+
return res.rows;
|
|
720
|
+
}
|
|
721
|
+
async updateEmbedding(id, vector, model) {
|
|
722
|
+
if (!this.caps.vectorSearch) return;
|
|
723
|
+
const literal = vectorLiteral(vector);
|
|
724
|
+
if (literal === null) return;
|
|
725
|
+
await this.client.query(
|
|
726
|
+
`UPDATE mem_records
|
|
727
|
+
SET embedding = $2::vector,
|
|
728
|
+
embedding_model = $3,
|
|
729
|
+
embedded_at = NOW()
|
|
730
|
+
WHERE id = $1`,
|
|
731
|
+
[id, literal, model]
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
async listKeysByType(type) {
|
|
735
|
+
const res = await this.client.query(
|
|
736
|
+
`SELECT id, keys FROM mem_records WHERE type = $1 AND status = 'active'`,
|
|
737
|
+
[type]
|
|
738
|
+
);
|
|
739
|
+
return res.rows;
|
|
740
|
+
}
|
|
741
|
+
// ── Search ───────────────────────────────────────────────────────────
|
|
742
|
+
async search(q, opts) {
|
|
743
|
+
const limit = opts.limit ?? 10;
|
|
744
|
+
const type = opts.type ?? null;
|
|
745
|
+
const includeArchived = opts.includeArchived ?? false;
|
|
746
|
+
const ftsWeight = opts.ftsWeight ?? 0.3;
|
|
747
|
+
const semanticWeight = opts.semanticWeight ?? 0.7;
|
|
748
|
+
const embedding = vectorLiteral(opts.queryEmbedding ?? null);
|
|
749
|
+
const canSemantic = this.caps.vectorSearch && embedding !== null;
|
|
750
|
+
let rows;
|
|
751
|
+
if (canSemantic) {
|
|
752
|
+
const res = await this.client.query(
|
|
753
|
+
`SELECT * FROM mem_hybrid_search($1, $2::vector, $3, $4, $5, $6, 50, $7)`,
|
|
754
|
+
[q, embedding, limit, type, ftsWeight, semanticWeight, includeArchived]
|
|
755
|
+
);
|
|
756
|
+
rows = res.rows;
|
|
757
|
+
} else {
|
|
758
|
+
const res = await this.client.query(
|
|
759
|
+
`SELECT r.id,
|
|
760
|
+
r.type,
|
|
761
|
+
r.data,
|
|
762
|
+
r.tags,
|
|
763
|
+
ts_rank_cd(r.searchable, websearch_to_tsquery('english', $1))::float AS fts_rank,
|
|
764
|
+
0.0::float AS semantic_rank,
|
|
765
|
+
ts_rank_cd(r.searchable, websearch_to_tsquery('english', $1))::float AS combined_score
|
|
766
|
+
FROM mem_records r
|
|
767
|
+
WHERE r.searchable @@ websearch_to_tsquery('english', $1)
|
|
768
|
+
AND ($2::text IS NULL OR r.type = $2)
|
|
769
|
+
AND ($3 OR r.status = 'active')
|
|
770
|
+
ORDER BY combined_score DESC
|
|
771
|
+
LIMIT $4`,
|
|
772
|
+
[q, type, includeArchived, limit]
|
|
773
|
+
);
|
|
774
|
+
rows = res.rows;
|
|
775
|
+
}
|
|
776
|
+
if (opts.trackAccess !== false && rows.length > 0) {
|
|
777
|
+
await this.trackAccess(rows.map((r) => r.id));
|
|
778
|
+
}
|
|
779
|
+
return rows;
|
|
780
|
+
}
|
|
781
|
+
async context(opts) {
|
|
782
|
+
const limit = opts.limit ?? 20;
|
|
783
|
+
const types = opts.types ?? null;
|
|
784
|
+
const res = await this.client.query(
|
|
785
|
+
`SELECT id, type, data, tags, keys, weight, access_count,
|
|
786
|
+
mem_calculate_relevance(weight, access_count, last_accessed_at, created_at)::float AS relevance_score
|
|
787
|
+
FROM mem_records
|
|
788
|
+
WHERE status = 'active'
|
|
789
|
+
AND ($1::text[] IS NULL OR type = ANY($1))
|
|
790
|
+
ORDER BY relevance_score DESC
|
|
791
|
+
LIMIT $2`,
|
|
792
|
+
[types, limit]
|
|
793
|
+
);
|
|
794
|
+
return res.rows;
|
|
795
|
+
}
|
|
796
|
+
async trackAccess(ids) {
|
|
797
|
+
if (ids.length === 0) return;
|
|
798
|
+
await this.client.query(`SELECT mem_increment_access($1::uuid[])`, [ids]);
|
|
799
|
+
}
|
|
800
|
+
// ── Graph ────────────────────────────────────────────────────────────
|
|
801
|
+
async link(fromId, toId, relation, opts = {}) {
|
|
802
|
+
const bidirectional = opts.bidirectional ?? false;
|
|
803
|
+
const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
|
|
804
|
+
const res = await this.client.query(
|
|
805
|
+
`INSERT INTO mem_links (from_id, to_id, relation, bidirectional, metadata)
|
|
806
|
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
807
|
+
ON CONFLICT (from_id, to_id, relation) DO UPDATE
|
|
808
|
+
SET bidirectional = EXCLUDED.bidirectional,
|
|
809
|
+
metadata = COALESCE(EXCLUDED.metadata, mem_links.metadata)
|
|
810
|
+
RETURNING id`,
|
|
811
|
+
[fromId, toId, relation, bidirectional, metadata]
|
|
812
|
+
);
|
|
813
|
+
return res.rows[0].id;
|
|
814
|
+
}
|
|
815
|
+
async unlink(fromId, toId, relation) {
|
|
816
|
+
const res = await this.client.query(
|
|
817
|
+
`DELETE FROM mem_links WHERE from_id = $1 AND to_id = $2 AND relation = $3`,
|
|
818
|
+
[fromId, toId, relation]
|
|
819
|
+
);
|
|
820
|
+
return (res.rowCount ?? 0) > 0;
|
|
821
|
+
}
|
|
822
|
+
async linked(id, opts = {}) {
|
|
823
|
+
const relation = opts.relation ?? null;
|
|
824
|
+
const direction = opts.direction ?? "outgoing";
|
|
825
|
+
const includeOutgoing = direction === "outgoing" || direction === "both";
|
|
826
|
+
const includeIncoming = direction === "incoming" || direction === "both";
|
|
827
|
+
const includeBidiAsIncoming = direction === "outgoing";
|
|
828
|
+
const res = await this.client.query(
|
|
829
|
+
`SELECT r.id, r.type, r.data, l.relation, l.bidirectional, l.metadata
|
|
830
|
+
FROM mem_links l
|
|
831
|
+
JOIN mem_records r ON r.id = l.to_id
|
|
832
|
+
WHERE l.from_id = $1
|
|
833
|
+
AND ($2::text IS NULL OR l.relation = $2)
|
|
834
|
+
AND $3::boolean
|
|
835
|
+
UNION ALL
|
|
836
|
+
SELECT r.id, r.type, r.data, l.relation, l.bidirectional, l.metadata
|
|
837
|
+
FROM mem_links l
|
|
838
|
+
JOIN mem_records r ON r.id = l.from_id
|
|
839
|
+
WHERE l.to_id = $1
|
|
840
|
+
AND ($2::text IS NULL OR l.relation = $2)
|
|
841
|
+
AND ($4::boolean OR ($5::boolean AND l.bidirectional = true))`,
|
|
842
|
+
[id, relation, includeOutgoing, includeIncoming, includeBidiAsIncoming]
|
|
843
|
+
);
|
|
844
|
+
return res.rows;
|
|
845
|
+
}
|
|
846
|
+
// ── Sources ──────────────────────────────────────────────────────────
|
|
847
|
+
async addSource(recordId, ref) {
|
|
848
|
+
const entry = {
|
|
849
|
+
url: ref.url ?? null,
|
|
850
|
+
metadata: ref.metadata ?? {},
|
|
851
|
+
last_synced_at: ref.last_synced_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
852
|
+
};
|
|
853
|
+
await this.client.query(
|
|
854
|
+
`UPDATE mem_records
|
|
855
|
+
SET sources = sources || jsonb_build_object($2::text, $3::jsonb),
|
|
856
|
+
keys = (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(keys, '{}') || ARRAY[$2::text])))
|
|
857
|
+
WHERE id = $1`,
|
|
858
|
+
[recordId, ref.sourceKey, JSON.stringify(entry)]
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
async removeSource(recordId, sourceKey) {
|
|
862
|
+
const res = await this.client.query(
|
|
863
|
+
`UPDATE mem_records
|
|
864
|
+
SET sources = sources - $2::text
|
|
865
|
+
WHERE id = $1 AND sources ? $2`,
|
|
866
|
+
[recordId, sourceKey]
|
|
867
|
+
);
|
|
868
|
+
return (res.rowCount ?? 0) > 0;
|
|
869
|
+
}
|
|
870
|
+
async findBySource(sourceKey) {
|
|
871
|
+
const res = await this.client.query(
|
|
872
|
+
`SELECT * FROM mem_records WHERE $1 = ANY(keys) LIMIT 1`,
|
|
873
|
+
[sourceKey]
|
|
874
|
+
);
|
|
875
|
+
return res.rows[0] ? toRecord(res.rows[0]) : null;
|
|
876
|
+
}
|
|
877
|
+
async listSources(recordId) {
|
|
878
|
+
const res = await this.client.query(
|
|
879
|
+
`SELECT sources FROM mem_records WHERE id = $1`,
|
|
880
|
+
[recordId]
|
|
881
|
+
);
|
|
882
|
+
return res.rows[0]?.sources ?? {};
|
|
883
|
+
}
|
|
884
|
+
// ── Sync state ───────────────────────────────────────────────────────
|
|
885
|
+
async getSyncState(platform, model) {
|
|
886
|
+
const res = await this.client.query(
|
|
887
|
+
`SELECT * FROM mem_sync_state WHERE platform = $1 AND model = $2`,
|
|
888
|
+
[platform, model]
|
|
889
|
+
);
|
|
890
|
+
return res.rows[0] ?? null;
|
|
891
|
+
}
|
|
892
|
+
async setSyncState(state) {
|
|
893
|
+
await this.client.query(
|
|
894
|
+
`INSERT INTO mem_sync_state (platform, model, last_sync_at, last_cursor, since,
|
|
895
|
+
total_records, pages_processed, status, last_error)
|
|
896
|
+
VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
|
|
897
|
+
ON CONFLICT (platform, model) DO UPDATE SET
|
|
898
|
+
last_sync_at = EXCLUDED.last_sync_at,
|
|
899
|
+
last_cursor = EXCLUDED.last_cursor,
|
|
900
|
+
since = EXCLUDED.since,
|
|
901
|
+
total_records = EXCLUDED.total_records,
|
|
902
|
+
pages_processed = EXCLUDED.pages_processed,
|
|
903
|
+
status = EXCLUDED.status,
|
|
904
|
+
last_error = EXCLUDED.last_error`,
|
|
905
|
+
[
|
|
906
|
+
state.platform,
|
|
907
|
+
state.model,
|
|
908
|
+
state.last_sync_at ?? null,
|
|
909
|
+
JSON.stringify(state.last_cursor ?? null),
|
|
910
|
+
state.since ?? null,
|
|
911
|
+
state.total_records,
|
|
912
|
+
state.pages_processed,
|
|
913
|
+
state.status,
|
|
914
|
+
state.last_error ?? null
|
|
915
|
+
]
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
async listSyncStates() {
|
|
919
|
+
const res = await this.client.query(
|
|
920
|
+
`SELECT * FROM mem_sync_state ORDER BY platform, model`
|
|
921
|
+
);
|
|
922
|
+
return res.rows;
|
|
923
|
+
}
|
|
924
|
+
async removeSyncState(platform, model) {
|
|
925
|
+
if (model) {
|
|
926
|
+
await this.client.query(
|
|
927
|
+
`DELETE FROM mem_sync_state WHERE platform = $1 AND model = $2`,
|
|
928
|
+
[platform, model]
|
|
929
|
+
);
|
|
930
|
+
} else {
|
|
931
|
+
await this.client.query(`DELETE FROM mem_sync_state WHERE platform = $1`, [platform]);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
// ── Raw read-only SQL ───────────────────────────────────────────────
|
|
935
|
+
//
|
|
936
|
+
// Exposed to users via `one mem sql` and `one sync sql` so they can run
|
|
937
|
+
// joins / aggregates / JSONB paths that the high-level helpers can't.
|
|
938
|
+
// The guard rejects anything that isn't SELECT / WITH / EXPLAIN before
|
|
939
|
+
// the query reaches the client.
|
|
940
|
+
async raw(sql, params) {
|
|
941
|
+
validateReadOnlySql(sql);
|
|
942
|
+
const res = await this.client.query(sql, params ?? []);
|
|
943
|
+
const columns = res.rows.length > 0 ? Object.keys(res.rows[0]) : [];
|
|
944
|
+
return {
|
|
945
|
+
columns,
|
|
946
|
+
rows: res.rows,
|
|
947
|
+
rowCount: res.rows.length
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
// ── Hot columns ──────────────────────────────────────────────────────
|
|
951
|
+
async ensureHotColumn(type, jsonPath) {
|
|
952
|
+
if (!this.caps.partialIndexes) return;
|
|
953
|
+
const name = hotColumnIndexName(type, jsonPath);
|
|
954
|
+
const pathArr = jsonPathArray(jsonPath);
|
|
955
|
+
await this.client.query(
|
|
956
|
+
`CREATE INDEX IF NOT EXISTS ${name}
|
|
957
|
+
ON mem_records ((data #>> '${pathArr}'::text[]))
|
|
958
|
+
WHERE type = $1`,
|
|
959
|
+
[type]
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
async dropHotColumn(type, jsonPath) {
|
|
963
|
+
const name = hotColumnIndexName(type, jsonPath);
|
|
964
|
+
await this.client.query(`DROP INDEX IF EXISTS ${name}`);
|
|
965
|
+
}
|
|
966
|
+
// ── Maintenance ──────────────────────────────────────────────────────
|
|
967
|
+
async vacuum() {
|
|
968
|
+
await this.client.query(`VACUUM ANALYZE mem_records`);
|
|
969
|
+
await this.client.query(`VACUUM ANALYZE mem_links`);
|
|
970
|
+
await this.client.query(`VACUUM ANALYZE mem_sync_state`);
|
|
971
|
+
}
|
|
972
|
+
async stats() {
|
|
973
|
+
const embeddedCountSql = this.caps.vectorSearch ? `(SELECT COUNT(*) FROM mem_records WHERE embedding IS NOT NULL)` : `(SELECT 0)`;
|
|
974
|
+
const res = await this.client.query(
|
|
975
|
+
`SELECT
|
|
976
|
+
(SELECT COUNT(*) FROM mem_records) AS record_count,
|
|
977
|
+
(SELECT COUNT(*) FROM mem_records WHERE status = 'active') AS active_count,
|
|
978
|
+
(SELECT COUNT(*) FROM mem_records WHERE status = 'archived') AS archived_count,
|
|
979
|
+
(SELECT COUNT(*) FROM mem_links) AS link_count,
|
|
980
|
+
${embeddedCountSql} AS embedded_count`
|
|
981
|
+
);
|
|
982
|
+
const r = res.rows[0];
|
|
983
|
+
return {
|
|
984
|
+
recordCount: Number(r.record_count),
|
|
985
|
+
activeCount: Number(r.active_count),
|
|
986
|
+
archivedCount: Number(r.archived_count),
|
|
987
|
+
linkCount: Number(r.link_count),
|
|
988
|
+
embeddedCount: Number(r.embedded_count)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
capabilities() {
|
|
992
|
+
return this.caps;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/lib/memory/plugins/postgres/index.ts
|
|
997
|
+
var CAPABILITIES = {
|
|
998
|
+
vectorSearch: true,
|
|
999
|
+
fullTextSearch: true,
|
|
1000
|
+
partialIndexes: true,
|
|
1001
|
+
jsonPathQuery: true,
|
|
1002
|
+
triggers: true,
|
|
1003
|
+
concurrentWriters: true,
|
|
1004
|
+
maxVectorDims: 2e3,
|
|
1005
|
+
rawSql: true
|
|
1006
|
+
};
|
|
1007
|
+
function parseConfig(raw) {
|
|
1008
|
+
const r = raw ?? {};
|
|
1009
|
+
const envOverride = process.env.MEM_DATABASE_URL;
|
|
1010
|
+
const connectionString = envOverride ?? r.connectionString ?? "";
|
|
1011
|
+
if (!connectionString) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
"Postgres backend requires a connection string. Set MEM_DATABASE_URL, or run `one mem config set memory.postgres.connectionString <url>`."
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
const schema = typeof r.schema === "string" && r.schema ? r.schema : "public";
|
|
1017
|
+
return { connectionString, schema };
|
|
1018
|
+
}
|
|
1019
|
+
function wrapClient(pool) {
|
|
1020
|
+
return {
|
|
1021
|
+
async query(text, params) {
|
|
1022
|
+
const res = await pool.query(text, params);
|
|
1023
|
+
return { rows: res.rows, rowCount: res.rowCount ?? void 0 };
|
|
1024
|
+
},
|
|
1025
|
+
async transaction(fn) {
|
|
1026
|
+
const tx = await pool.connect();
|
|
1027
|
+
const txClient = {
|
|
1028
|
+
async query(text, params) {
|
|
1029
|
+
const res = await tx.query(text, params);
|
|
1030
|
+
return { rows: res.rows, rowCount: res.rowCount ?? void 0 };
|
|
1031
|
+
},
|
|
1032
|
+
async transaction() {
|
|
1033
|
+
throw new Error("Nested transactions are not supported.");
|
|
1034
|
+
},
|
|
1035
|
+
async close() {
|
|
1036
|
+
tx.release();
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
try {
|
|
1040
|
+
await tx.query("BEGIN");
|
|
1041
|
+
const result = await fn(txClient);
|
|
1042
|
+
await tx.query("COMMIT");
|
|
1043
|
+
return result;
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
try {
|
|
1046
|
+
await tx.query("ROLLBACK");
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
throw err;
|
|
1050
|
+
} finally {
|
|
1051
|
+
tx.release();
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
async close() {
|
|
1055
|
+
await pool.end();
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
var LazyPostgresBackend = class {
|
|
1060
|
+
backend = null;
|
|
1061
|
+
config;
|
|
1062
|
+
constructor(config) {
|
|
1063
|
+
this.config = config;
|
|
1064
|
+
}
|
|
1065
|
+
async ensure() {
|
|
1066
|
+
if (this.backend) return this.backend;
|
|
1067
|
+
const pg = await import("pg").catch((err) => {
|
|
1068
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`pg is not installed. Run \`npm i -g pg\` or pick a different backend. (${msg})`
|
|
1071
|
+
);
|
|
1072
|
+
});
|
|
1073
|
+
const PoolCtor = pg.Pool ?? pg.default.Pool;
|
|
1074
|
+
const pool = new PoolCtor({ connectionString: this.config.connectionString });
|
|
1075
|
+
this.backend = new CoreBackend(wrapClient(pool), CAPABILITIES);
|
|
1076
|
+
return this.backend;
|
|
1077
|
+
}
|
|
1078
|
+
capabilities() {
|
|
1079
|
+
return CAPABILITIES;
|
|
1080
|
+
}
|
|
1081
|
+
async init() {
|
|
1082
|
+
await this.ensure();
|
|
1083
|
+
}
|
|
1084
|
+
async close() {
|
|
1085
|
+
if (this.backend) await this.backend.close();
|
|
1086
|
+
this.backend = null;
|
|
1087
|
+
}
|
|
1088
|
+
async ensureSchema() {
|
|
1089
|
+
return (await this.ensure()).ensureSchema();
|
|
1090
|
+
}
|
|
1091
|
+
async getSchemaVersion() {
|
|
1092
|
+
return (await this.ensure()).getSchemaVersion();
|
|
1093
|
+
}
|
|
1094
|
+
async insert(...a) {
|
|
1095
|
+
return (await this.ensure()).insert(...a);
|
|
1096
|
+
}
|
|
1097
|
+
async upsertByKeys(...a) {
|
|
1098
|
+
return (await this.ensure()).upsertByKeys(...a);
|
|
1099
|
+
}
|
|
1100
|
+
async getById(...a) {
|
|
1101
|
+
return (await this.ensure()).getById(...a);
|
|
1102
|
+
}
|
|
1103
|
+
async update(...a) {
|
|
1104
|
+
return (await this.ensure()).update(...a);
|
|
1105
|
+
}
|
|
1106
|
+
async remove(...a) {
|
|
1107
|
+
return (await this.ensure()).remove(...a);
|
|
1108
|
+
}
|
|
1109
|
+
async archive(...a) {
|
|
1110
|
+
return (await this.ensure()).archive(...a);
|
|
1111
|
+
}
|
|
1112
|
+
async unarchive(...a) {
|
|
1113
|
+
return (await this.ensure()).unarchive(...a);
|
|
1114
|
+
}
|
|
1115
|
+
async list(...a) {
|
|
1116
|
+
return (await this.ensure()).list(...a);
|
|
1117
|
+
}
|
|
1118
|
+
async count(...a) {
|
|
1119
|
+
return (await this.ensure()).count(...a);
|
|
1120
|
+
}
|
|
1121
|
+
async listForReindex(...a) {
|
|
1122
|
+
return (await this.ensure()).listForReindex(...a);
|
|
1123
|
+
}
|
|
1124
|
+
async listKeysByType(...a) {
|
|
1125
|
+
return (await this.ensure()).listKeysByType(...a);
|
|
1126
|
+
}
|
|
1127
|
+
async updateEmbedding(...a) {
|
|
1128
|
+
return (await this.ensure()).updateEmbedding(...a);
|
|
1129
|
+
}
|
|
1130
|
+
async raw(sql, params) {
|
|
1131
|
+
const b = await this.ensure();
|
|
1132
|
+
if (!b.raw) throw new Error("Backend does not support raw SQL");
|
|
1133
|
+
return b.raw(sql, params);
|
|
1134
|
+
}
|
|
1135
|
+
async search(...a) {
|
|
1136
|
+
return (await this.ensure()).search(...a);
|
|
1137
|
+
}
|
|
1138
|
+
async context(...a) {
|
|
1139
|
+
return (await this.ensure()).context(...a);
|
|
1140
|
+
}
|
|
1141
|
+
async trackAccess(...a) {
|
|
1142
|
+
return (await this.ensure()).trackAccess(...a);
|
|
1143
|
+
}
|
|
1144
|
+
async link(...a) {
|
|
1145
|
+
return (await this.ensure()).link(...a);
|
|
1146
|
+
}
|
|
1147
|
+
async unlink(...a) {
|
|
1148
|
+
return (await this.ensure()).unlink(...a);
|
|
1149
|
+
}
|
|
1150
|
+
async linked(...a) {
|
|
1151
|
+
return (await this.ensure()).linked(...a);
|
|
1152
|
+
}
|
|
1153
|
+
async addSource(...a) {
|
|
1154
|
+
return (await this.ensure()).addSource(...a);
|
|
1155
|
+
}
|
|
1156
|
+
async removeSource(...a) {
|
|
1157
|
+
return (await this.ensure()).removeSource(...a);
|
|
1158
|
+
}
|
|
1159
|
+
async findBySource(...a) {
|
|
1160
|
+
return (await this.ensure()).findBySource(...a);
|
|
1161
|
+
}
|
|
1162
|
+
async listSources(...a) {
|
|
1163
|
+
return (await this.ensure()).listSources(...a);
|
|
1164
|
+
}
|
|
1165
|
+
async getSyncState(...a) {
|
|
1166
|
+
return (await this.ensure()).getSyncState(...a);
|
|
1167
|
+
}
|
|
1168
|
+
async setSyncState(...a) {
|
|
1169
|
+
return (await this.ensure()).setSyncState(...a);
|
|
1170
|
+
}
|
|
1171
|
+
async listSyncStates() {
|
|
1172
|
+
return (await this.ensure()).listSyncStates();
|
|
1173
|
+
}
|
|
1174
|
+
async removeSyncState(...a) {
|
|
1175
|
+
return (await this.ensure()).removeSyncState(...a);
|
|
1176
|
+
}
|
|
1177
|
+
async ensureHotColumn(...a) {
|
|
1178
|
+
return (await this.ensure()).ensureHotColumn(...a);
|
|
1179
|
+
}
|
|
1180
|
+
async dropHotColumn(...a) {
|
|
1181
|
+
return (await this.ensure()).dropHotColumn(...a);
|
|
1182
|
+
}
|
|
1183
|
+
async vacuum() {
|
|
1184
|
+
return (await this.ensure()).vacuum();
|
|
1185
|
+
}
|
|
1186
|
+
async stats() {
|
|
1187
|
+
return (await this.ensure()).stats();
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var postgresPlugin = {
|
|
1191
|
+
name: "postgres",
|
|
1192
|
+
description: "Postgres over node-pg. Works with Supabase, Neon, self-hosted.",
|
|
1193
|
+
version: "0.1.0",
|
|
1194
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1195
|
+
capabilities: CAPABILITIES,
|
|
1196
|
+
parseConfig,
|
|
1197
|
+
create(config) {
|
|
1198
|
+
return new LazyPostgresBackend(config);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// src/lib/memory/plugins/embedded-postgres/index.ts
|
|
1203
|
+
import fs from "fs";
|
|
1204
|
+
import net from "net";
|
|
1205
|
+
import path from "path";
|
|
1206
|
+
import os from "os";
|
|
1207
|
+
import { createRequire } from "module";
|
|
1208
|
+
import { spawn } from "child_process";
|
|
1209
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
1210
|
+
var BASE_CAPABILITIES = {
|
|
1211
|
+
vectorSearch: true,
|
|
1212
|
+
fullTextSearch: true,
|
|
1213
|
+
partialIndexes: true,
|
|
1214
|
+
jsonPathQuery: true,
|
|
1215
|
+
triggers: true,
|
|
1216
|
+
concurrentWriters: true,
|
|
1217
|
+
maxVectorDims: 2e3,
|
|
1218
|
+
rawSql: true
|
|
1219
|
+
};
|
|
1220
|
+
var DEFAULTS = {
|
|
1221
|
+
dataDir: path.join(os.homedir(), ".one", "pg"),
|
|
1222
|
+
database: "one_mem",
|
|
1223
|
+
schema: "public",
|
|
1224
|
+
pgvector: true,
|
|
1225
|
+
host: "127.0.0.1",
|
|
1226
|
+
port: 5434,
|
|
1227
|
+
// pgserve auto-provisions databases; the bundled superuser is `postgres`
|
|
1228
|
+
// with no password by default. We don't expose the password to the user
|
|
1229
|
+
// since the daemon only listens on 127.0.0.1.
|
|
1230
|
+
user: "postgres",
|
|
1231
|
+
password: "",
|
|
1232
|
+
logLevel: "warn",
|
|
1233
|
+
startupTimeoutMs: 3e4
|
|
1234
|
+
};
|
|
1235
|
+
function parseConfig2(raw) {
|
|
1236
|
+
const r = raw ?? {};
|
|
1237
|
+
return {
|
|
1238
|
+
dataDir: r.dataDir ?? DEFAULTS.dataDir,
|
|
1239
|
+
database: r.database ?? DEFAULTS.database,
|
|
1240
|
+
schema: r.schema ?? DEFAULTS.schema,
|
|
1241
|
+
pgvector: r.pgvector ?? DEFAULTS.pgvector,
|
|
1242
|
+
host: r.host ?? DEFAULTS.host,
|
|
1243
|
+
port: r.port ?? DEFAULTS.port,
|
|
1244
|
+
user: r.user ?? DEFAULTS.user,
|
|
1245
|
+
password: r.password ?? DEFAULTS.password,
|
|
1246
|
+
logLevel: r.logLevel ?? DEFAULTS.logLevel,
|
|
1247
|
+
startupTimeoutMs: r.startupTimeoutMs ?? DEFAULTS.startupTimeoutMs
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function wrapClient2(pool) {
|
|
1251
|
+
return {
|
|
1252
|
+
async query(text, params) {
|
|
1253
|
+
const res = await pool.query(text, params);
|
|
1254
|
+
return { rows: res.rows, rowCount: res.rowCount ?? void 0 };
|
|
1255
|
+
},
|
|
1256
|
+
async transaction(fn) {
|
|
1257
|
+
const tx = await pool.connect();
|
|
1258
|
+
const txClient = {
|
|
1259
|
+
async query(text, params) {
|
|
1260
|
+
const res = await tx.query(text, params);
|
|
1261
|
+
return { rows: res.rows, rowCount: res.rowCount ?? void 0 };
|
|
1262
|
+
},
|
|
1263
|
+
async transaction() {
|
|
1264
|
+
throw new Error("Nested transactions are not supported.");
|
|
1265
|
+
},
|
|
1266
|
+
async close() {
|
|
1267
|
+
tx.release();
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
try {
|
|
1271
|
+
await tx.query("BEGIN");
|
|
1272
|
+
const result = await fn(txClient);
|
|
1273
|
+
await tx.query("COMMIT");
|
|
1274
|
+
return result;
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
try {
|
|
1277
|
+
await tx.query("ROLLBACK");
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
throw err;
|
|
1281
|
+
} finally {
|
|
1282
|
+
tx.release();
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
async close() {
|
|
1286
|
+
await pool.end();
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function pidFilePath(dataDir) {
|
|
1291
|
+
return path.join(dataDir, ".pgserve.json");
|
|
1292
|
+
}
|
|
1293
|
+
function readPidFile(dataDir) {
|
|
1294
|
+
try {
|
|
1295
|
+
const raw = fs.readFileSync(pidFilePath(dataDir), "utf8");
|
|
1296
|
+
return JSON.parse(raw);
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function writePidFile(dataDir, rec) {
|
|
1302
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
1303
|
+
fs.writeFileSync(pidFilePath(dataDir), JSON.stringify(rec, null, 2), { mode: 384 });
|
|
1304
|
+
}
|
|
1305
|
+
function isPidAlive(pid) {
|
|
1306
|
+
try {
|
|
1307
|
+
process.kill(pid, 0);
|
|
1308
|
+
return true;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function isPortListening(host, port, timeoutMs = 1e3) {
|
|
1314
|
+
return new Promise((resolve) => {
|
|
1315
|
+
const socket = new net.Socket();
|
|
1316
|
+
let resolved = false;
|
|
1317
|
+
const finish = (ok) => {
|
|
1318
|
+
if (resolved) return;
|
|
1319
|
+
resolved = true;
|
|
1320
|
+
socket.destroy();
|
|
1321
|
+
resolve(ok);
|
|
1322
|
+
};
|
|
1323
|
+
socket.setTimeout(timeoutMs);
|
|
1324
|
+
socket.once("connect", () => finish(true));
|
|
1325
|
+
socket.once("timeout", () => finish(false));
|
|
1326
|
+
socket.once("error", () => finish(false));
|
|
1327
|
+
socket.connect(port, host);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
async function waitForPort(host, port, deadlineAt) {
|
|
1331
|
+
while (Date.now() < deadlineAt) {
|
|
1332
|
+
if (await isPortListening(host, port)) return;
|
|
1333
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1334
|
+
}
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`pgserve did not start listening on ${host}:${port} within ${Math.round((deadlineAt - Date.now() + 3e4) / 1e3)}s. Check the data dir for the postgres log, or remove ${path.basename(pidFilePath("<dataDir>"))} and retry.`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
function resolvePgserveBin() {
|
|
1340
|
+
const pkg = requireFromHere.resolve("pgserve/package.json");
|
|
1341
|
+
return path.resolve(path.dirname(pkg), "bin/pgserve-wrapper.cjs");
|
|
1342
|
+
}
|
|
1343
|
+
async function ensureRunning(cfg) {
|
|
1344
|
+
const existing = readPidFile(cfg.dataDir);
|
|
1345
|
+
if (existing && isPidAlive(existing.pid)) {
|
|
1346
|
+
if (await isPortListening(cfg.host, existing.port)) {
|
|
1347
|
+
cfg.port = existing.port;
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
fs.mkdirSync(cfg.dataDir, { recursive: true });
|
|
1352
|
+
const clusterDir = path.join(cfg.dataDir, "cluster");
|
|
1353
|
+
fs.mkdirSync(clusterDir, { recursive: true });
|
|
1354
|
+
const args = [
|
|
1355
|
+
"--data",
|
|
1356
|
+
clusterDir,
|
|
1357
|
+
"--port",
|
|
1358
|
+
String(cfg.port),
|
|
1359
|
+
"--host",
|
|
1360
|
+
cfg.host,
|
|
1361
|
+
"--log",
|
|
1362
|
+
cfg.logLevel,
|
|
1363
|
+
"--no-stats"
|
|
1364
|
+
];
|
|
1365
|
+
if (cfg.pgvector) args.push("--pgvector");
|
|
1366
|
+
const bin = resolvePgserveBin();
|
|
1367
|
+
const out = fs.openSync(path.join(cfg.dataDir, "pgserve.log"), "a");
|
|
1368
|
+
const child = spawn(process.execPath, [bin, ...args], {
|
|
1369
|
+
detached: true,
|
|
1370
|
+
stdio: ["ignore", out, out],
|
|
1371
|
+
// pgserve resolves the bundled bun binary relative to its own package
|
|
1372
|
+
// location, not cwd, so cwd doesn't matter — but pin it for clarity.
|
|
1373
|
+
cwd: cfg.dataDir
|
|
1374
|
+
});
|
|
1375
|
+
child.unref();
|
|
1376
|
+
if (typeof child.pid !== "number") {
|
|
1377
|
+
throw new Error("Failed to spawn pgserve \u2014 no PID returned.");
|
|
1378
|
+
}
|
|
1379
|
+
const deadlineAt = Date.now() + cfg.startupTimeoutMs;
|
|
1380
|
+
await waitForPort(cfg.host, cfg.port, deadlineAt);
|
|
1381
|
+
writePidFile(cfg.dataDir, {
|
|
1382
|
+
pid: child.pid,
|
|
1383
|
+
port: cfg.port,
|
|
1384
|
+
dataDir: cfg.dataDir,
|
|
1385
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async function probePgvector(client) {
|
|
1389
|
+
try {
|
|
1390
|
+
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
1391
|
+
return true;
|
|
1392
|
+
} catch {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
var LazyEmbeddedPostgresBackend = class {
|
|
1397
|
+
backend = null;
|
|
1398
|
+
config;
|
|
1399
|
+
resolvedCaps = BASE_CAPABILITIES;
|
|
1400
|
+
constructor(config) {
|
|
1401
|
+
this.config = config;
|
|
1402
|
+
}
|
|
1403
|
+
async ensure() {
|
|
1404
|
+
if (this.backend) return this.backend;
|
|
1405
|
+
await ensureRunning(this.config);
|
|
1406
|
+
const pgMod = await import("pg").catch((err) => {
|
|
1407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1408
|
+
throw new Error(`pg is not installed. Run \`npm i pg\` or pick a different backend. (${msg})`);
|
|
1409
|
+
});
|
|
1410
|
+
const PoolCtor = pgMod.Pool ?? pgMod.default.Pool;
|
|
1411
|
+
const pool = new PoolCtor({
|
|
1412
|
+
host: this.config.host,
|
|
1413
|
+
port: this.config.port,
|
|
1414
|
+
database: this.config.database,
|
|
1415
|
+
user: this.config.user,
|
|
1416
|
+
password: this.config.password
|
|
1417
|
+
});
|
|
1418
|
+
const client = wrapClient2(pool);
|
|
1419
|
+
const hasVector = await probePgvector(client);
|
|
1420
|
+
this.resolvedCaps = hasVector ? BASE_CAPABILITIES : { ...BASE_CAPABILITIES, vectorSearch: false, maxVectorDims: null };
|
|
1421
|
+
this.backend = new CoreBackend(client, this.resolvedCaps);
|
|
1422
|
+
return this.backend;
|
|
1423
|
+
}
|
|
1424
|
+
capabilities() {
|
|
1425
|
+
return this.resolvedCaps;
|
|
1426
|
+
}
|
|
1427
|
+
async init() {
|
|
1428
|
+
await this.ensure();
|
|
1429
|
+
}
|
|
1430
|
+
async close() {
|
|
1431
|
+
if (this.backend) await this.backend.close();
|
|
1432
|
+
this.backend = null;
|
|
1433
|
+
}
|
|
1434
|
+
async ensureSchema() {
|
|
1435
|
+
return (await this.ensure()).ensureSchema();
|
|
1436
|
+
}
|
|
1437
|
+
async getSchemaVersion() {
|
|
1438
|
+
return (await this.ensure()).getSchemaVersion();
|
|
1439
|
+
}
|
|
1440
|
+
async insert(...a) {
|
|
1441
|
+
return (await this.ensure()).insert(...a);
|
|
1442
|
+
}
|
|
1443
|
+
async upsertByKeys(...a) {
|
|
1444
|
+
return (await this.ensure()).upsertByKeys(...a);
|
|
1445
|
+
}
|
|
1446
|
+
async getById(...a) {
|
|
1447
|
+
return (await this.ensure()).getById(...a);
|
|
1448
|
+
}
|
|
1449
|
+
async update(...a) {
|
|
1450
|
+
return (await this.ensure()).update(...a);
|
|
1451
|
+
}
|
|
1452
|
+
async remove(...a) {
|
|
1453
|
+
return (await this.ensure()).remove(...a);
|
|
1454
|
+
}
|
|
1455
|
+
async archive(...a) {
|
|
1456
|
+
return (await this.ensure()).archive(...a);
|
|
1457
|
+
}
|
|
1458
|
+
async unarchive(...a) {
|
|
1459
|
+
return (await this.ensure()).unarchive(...a);
|
|
1460
|
+
}
|
|
1461
|
+
async list(...a) {
|
|
1462
|
+
return (await this.ensure()).list(...a);
|
|
1463
|
+
}
|
|
1464
|
+
async count(...a) {
|
|
1465
|
+
return (await this.ensure()).count(...a);
|
|
1466
|
+
}
|
|
1467
|
+
async listForReindex(...a) {
|
|
1468
|
+
return (await this.ensure()).listForReindex(...a);
|
|
1469
|
+
}
|
|
1470
|
+
async listKeysByType(...a) {
|
|
1471
|
+
return (await this.ensure()).listKeysByType(...a);
|
|
1472
|
+
}
|
|
1473
|
+
async updateEmbedding(...a) {
|
|
1474
|
+
return (await this.ensure()).updateEmbedding(...a);
|
|
1475
|
+
}
|
|
1476
|
+
async raw(sql, params) {
|
|
1477
|
+
const b = await this.ensure();
|
|
1478
|
+
if (!b.raw) throw new Error("Backend does not support raw SQL");
|
|
1479
|
+
return b.raw(sql, params);
|
|
1480
|
+
}
|
|
1481
|
+
async search(...a) {
|
|
1482
|
+
return (await this.ensure()).search(...a);
|
|
1483
|
+
}
|
|
1484
|
+
async context(...a) {
|
|
1485
|
+
return (await this.ensure()).context(...a);
|
|
1486
|
+
}
|
|
1487
|
+
async trackAccess(...a) {
|
|
1488
|
+
return (await this.ensure()).trackAccess(...a);
|
|
1489
|
+
}
|
|
1490
|
+
async link(...a) {
|
|
1491
|
+
return (await this.ensure()).link(...a);
|
|
1492
|
+
}
|
|
1493
|
+
async unlink(...a) {
|
|
1494
|
+
return (await this.ensure()).unlink(...a);
|
|
1495
|
+
}
|
|
1496
|
+
async linked(...a) {
|
|
1497
|
+
return (await this.ensure()).linked(...a);
|
|
1498
|
+
}
|
|
1499
|
+
async addSource(...a) {
|
|
1500
|
+
return (await this.ensure()).addSource(...a);
|
|
1501
|
+
}
|
|
1502
|
+
async removeSource(...a) {
|
|
1503
|
+
return (await this.ensure()).removeSource(...a);
|
|
1504
|
+
}
|
|
1505
|
+
async findBySource(...a) {
|
|
1506
|
+
return (await this.ensure()).findBySource(...a);
|
|
1507
|
+
}
|
|
1508
|
+
async listSources(...a) {
|
|
1509
|
+
return (await this.ensure()).listSources(...a);
|
|
1510
|
+
}
|
|
1511
|
+
async getSyncState(...a) {
|
|
1512
|
+
return (await this.ensure()).getSyncState(...a);
|
|
1513
|
+
}
|
|
1514
|
+
async setSyncState(...a) {
|
|
1515
|
+
return (await this.ensure()).setSyncState(...a);
|
|
1516
|
+
}
|
|
1517
|
+
async listSyncStates() {
|
|
1518
|
+
return (await this.ensure()).listSyncStates();
|
|
1519
|
+
}
|
|
1520
|
+
async removeSyncState(...a) {
|
|
1521
|
+
return (await this.ensure()).removeSyncState(...a);
|
|
1522
|
+
}
|
|
1523
|
+
async ensureHotColumn(...a) {
|
|
1524
|
+
return (await this.ensure()).ensureHotColumn(...a);
|
|
1525
|
+
}
|
|
1526
|
+
async dropHotColumn(...a) {
|
|
1527
|
+
return (await this.ensure()).dropHotColumn(...a);
|
|
1528
|
+
}
|
|
1529
|
+
async vacuum() {
|
|
1530
|
+
return (await this.ensure()).vacuum();
|
|
1531
|
+
}
|
|
1532
|
+
async stats() {
|
|
1533
|
+
return (await this.ensure()).stats();
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
var embeddedPostgresPlugin = {
|
|
1537
|
+
name: "embedded-postgres",
|
|
1538
|
+
description: "Local Postgres 18 bootstrapped on-demand via pgserve. Default backend for new installs. Semantic search (pgvector) auto-enables when the extension is locally installed; otherwise FTS-only with an upgrade hint.",
|
|
1539
|
+
version: "0.1.0",
|
|
1540
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1541
|
+
capabilities: BASE_CAPABILITIES,
|
|
1542
|
+
parseConfig: parseConfig2,
|
|
1543
|
+
create(config) {
|
|
1544
|
+
return new LazyEmbeddedPostgresBackend(config);
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// src/lib/memory/plugins.ts
|
|
1549
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1550
|
+
function registerBackend(plugin) {
|
|
1551
|
+
if (registry.has(plugin.name)) {
|
|
1552
|
+
const existing = registry.get(plugin.name);
|
|
1553
|
+
if (existing.version === plugin.version) return;
|
|
1554
|
+
throw new Error(
|
|
1555
|
+
`Backend plugin name conflict: "${plugin.name}" already registered (v${existing.version}); refusing to overwrite with v${plugin.version}.`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
registry.set(plugin.name, plugin);
|
|
1559
|
+
}
|
|
1560
|
+
function getBackendPlugin(name) {
|
|
1561
|
+
const plugin = registry.get(name);
|
|
1562
|
+
if (!plugin) {
|
|
1563
|
+
const available = [...registry.keys()].join(", ") || "(none registered)";
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
`Backend plugin "${name}" is not registered. Available: ${available}. If this is a third-party plugin, add it to "memory.plugins" in your config.`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
return plugin;
|
|
1569
|
+
}
|
|
1570
|
+
function listBackendPlugins() {
|
|
1571
|
+
return [...registry.values()];
|
|
1572
|
+
}
|
|
1573
|
+
async function loadBackendFromConfig(cfg) {
|
|
1574
|
+
for (const spec of cfg.plugins ?? []) {
|
|
1575
|
+
try {
|
|
1576
|
+
const mod = await import(spec);
|
|
1577
|
+
const plugin2 = mod.default ?? mod;
|
|
1578
|
+
if (!plugin2 || typeof plugin2 !== "object" || typeof plugin2.name !== "string") {
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`Package "${spec}" does not export a MemBackendPlugin as default. Ensure the package's default export is the plugin descriptor.`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
registerBackend(plugin2);
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1586
|
+
throw new Error(`Failed to load memory plugin "${spec}": ${message}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const plugin = getBackendPlugin(cfg.backend);
|
|
1590
|
+
const backendConfig = plugin.parseConfig(cfg[cfg.backend] ?? {});
|
|
1591
|
+
return plugin.create(backendConfig);
|
|
1592
|
+
}
|
|
1593
|
+
registerBackend(embeddedPostgresPlugin);
|
|
1594
|
+
registerBackend(postgresPlugin);
|
|
1595
|
+
|
|
1596
|
+
// src/lib/memory/canonical.ts
|
|
1597
|
+
import { createHash } from "crypto";
|
|
1598
|
+
function canonicalize(value) {
|
|
1599
|
+
return JSON.stringify(canonicalValue(value));
|
|
1600
|
+
}
|
|
1601
|
+
function canonicalValue(value) {
|
|
1602
|
+
if (value === null || typeof value !== "object") return value;
|
|
1603
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
1604
|
+
const obj = value;
|
|
1605
|
+
const sorted = {};
|
|
1606
|
+
for (const key of Object.keys(obj).sort()) {
|
|
1607
|
+
sorted[key] = canonicalValue(obj[key]);
|
|
1608
|
+
}
|
|
1609
|
+
return sorted;
|
|
1610
|
+
}
|
|
1611
|
+
function contentHash(data) {
|
|
1612
|
+
return createHash("sha256").update(canonicalize(data)).digest("hex");
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/lib/memory/runtime.ts
|
|
1616
|
+
var cached = null;
|
|
1617
|
+
async function getBackend() {
|
|
1618
|
+
if (cached) return cached;
|
|
1619
|
+
if (!getMemoryConfig()) {
|
|
1620
|
+
bootstrapMemoryDefaults();
|
|
1621
|
+
}
|
|
1622
|
+
const cfg = getMemoryConfigOrDefault();
|
|
1623
|
+
cached = (async () => {
|
|
1624
|
+
const backend = await loadBackendFromConfig(cfg);
|
|
1625
|
+
await backend.init();
|
|
1626
|
+
await backend.ensureSchema();
|
|
1627
|
+
return backend;
|
|
1628
|
+
})();
|
|
1629
|
+
return cached;
|
|
1630
|
+
}
|
|
1631
|
+
function bootstrapMemoryDefaults() {
|
|
1632
|
+
const hasOpenAiKey = !!getOpenAiApiKey();
|
|
1633
|
+
const next = {
|
|
1634
|
+
...DEFAULT_MEMORY_CONFIG,
|
|
1635
|
+
embedding: {
|
|
1636
|
+
...DEFAULT_MEMORY_CONFIG.embedding,
|
|
1637
|
+
provider: hasOpenAiKey ? "openai" : "none"
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
updateMemoryConfig(next);
|
|
1641
|
+
if (process.stderr.isTTY) {
|
|
1642
|
+
process.stderr.write(
|
|
1643
|
+
`one mem: initialized ${hasOpenAiKey ? "(embeddings enabled)" : "(FTS only; set OpenAI key for semantic search)"}
|
|
1644
|
+
`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function resetBackendSingleton() {
|
|
1649
|
+
cached = null;
|
|
1650
|
+
}
|
|
1651
|
+
async function closeBackendIfCached() {
|
|
1652
|
+
if (!cached) return;
|
|
1653
|
+
try {
|
|
1654
|
+
const backend = await cached;
|
|
1655
|
+
await backend.close();
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
cached = null;
|
|
1659
|
+
}
|
|
1660
|
+
async function addRecord(input, opts = {}) {
|
|
1661
|
+
const backend = await getBackend();
|
|
1662
|
+
const { searchable_text, content_hash, embedding, embedding_model } = await prepareRecord(input, opts, "add");
|
|
1663
|
+
const prepared = {
|
|
1664
|
+
...input,
|
|
1665
|
+
searchable_text: input.searchable_text ?? searchable_text,
|
|
1666
|
+
content_hash: input.content_hash ?? content_hash,
|
|
1667
|
+
embedding,
|
|
1668
|
+
embedding_model
|
|
1669
|
+
};
|
|
1670
|
+
return backend.insert(prepared);
|
|
1671
|
+
}
|
|
1672
|
+
async function upsertRecord(input, opts = {}) {
|
|
1673
|
+
const backend = await getBackend();
|
|
1674
|
+
const { searchable_text, content_hash, embedding, embedding_model } = await prepareRecord(input, opts, "sync");
|
|
1675
|
+
const prepared = {
|
|
1676
|
+
...input,
|
|
1677
|
+
searchable_text: input.searchable_text ?? searchable_text,
|
|
1678
|
+
content_hash: input.content_hash ?? content_hash,
|
|
1679
|
+
embedding,
|
|
1680
|
+
embedding_model
|
|
1681
|
+
};
|
|
1682
|
+
const backendOpts = { replace: opts.replace ?? false };
|
|
1683
|
+
return backend.upsertByKeys(prepared, backendOpts);
|
|
1684
|
+
}
|
|
1685
|
+
async function prepareRecord(input, opts, ctx) {
|
|
1686
|
+
const cfg = getMemoryConfigOrDefault();
|
|
1687
|
+
const searchable_text = input.searchable_text ?? defaultSearchableText(input.data);
|
|
1688
|
+
const content_hash = input.content_hash ?? contentHash(input.data);
|
|
1689
|
+
const wantEmbed = opts.embed ?? input.embed ?? (ctx === "add" ? cfg.defaults.embedOnAdd : cfg.defaults.embedOnSync);
|
|
1690
|
+
if (!wantEmbed || cfg.embedding.provider === "none" || !searchable_text) {
|
|
1691
|
+
return { searchable_text, content_hash, embedding: null, embedding_model: null };
|
|
1692
|
+
}
|
|
1693
|
+
const result = await embed(searchable_text, { model: opts.embeddingModel });
|
|
1694
|
+
if (!result) return { searchable_text, content_hash, embedding: null, embedding_model: null };
|
|
1695
|
+
return { searchable_text, content_hash, embedding: result.vector, embedding_model: result.model };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
export {
|
|
1699
|
+
SCHEMA_VERSION,
|
|
1700
|
+
getBackendPlugin,
|
|
1701
|
+
listBackendPlugins,
|
|
1702
|
+
loadBackendFromConfig,
|
|
1703
|
+
getBackend,
|
|
1704
|
+
resetBackendSingleton,
|
|
1705
|
+
closeBackendIfCached,
|
|
1706
|
+
addRecord,
|
|
1707
|
+
upsertRecord
|
|
1708
|
+
};
|