@clude/sdk 3.2.0 → 3.3.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.
@@ -89,14 +89,47 @@ CREATE TABLE IF NOT EXISTS memories (
89
89
  compacted_into TEXT, -- hash_id of the memory this was compacted into
90
90
  encrypted BOOLEAN DEFAULT FALSE, -- whether content is client-side encrypted
91
91
  encryption_pubkey TEXT, -- Solana pubkey that encrypted this memory
92
+ provider_delegated BOOLEAN DEFAULT TRUE, -- envelope: provider may read while delegated; revoke flips to FALSE (encryption §7)
93
+ summary_ciphertext TEXT, -- secretbox(summary) — populated on revoke (§9)
94
+ embedding_ciphertext TEXT, -- secretbox(embedding) — populated on revoke (§9)
92
95
  owner_wallet TEXT, -- Solana pubkey of the memory owner
93
96
  event_date TIMESTAMPTZ DEFAULT NULL, -- explicit event date extracted from content (temporal indexing)
94
97
  event_date_precision TEXT DEFAULT NULL CHECK (event_date_precision IN ('day', 'week', 'month', 'year')),
95
98
  ts_summary tsvector GENERATED ALWAYS AS (
96
- setweight(to_tsvector('english', COALESCE(summary, '')), 'A') ||
97
- setweight(to_tsvector('english', COALESCE(LEFT(content, 2000), '')), 'B')
98
- ) STORED -- auto-generated tsvector for BM25-like full-text search
99
+ setweight(to_tsvector('english', COALESCE(summary, '')), 'A')
100
+ ) STORED, -- summary-only lexemes; content lexemes live in content_tokens (encryption §9)
101
+ content_tokens tsvector, -- content lexemes, app-maintained via set_memory_content_tokens; cleared on revoke
102
+ -- Memory 3.0 Phase 1 (migration 044): bi-temporal validity + provenance (all nullable, inert until MEMORY_RECONCILE)
103
+ valid_from TIMESTAMPTZ, -- fact lifetime start (defaults to created_at at reconcile time)
104
+ invalid_at TIMESTAMPTZ, -- NULL = currently valid; set = superseded/retired (never DELETE)
105
+ superseded_by TEXT, -- hash_id of the memory that replaced this one
106
+ fact_key TEXT, -- (entity, attribute) reconciliation key
107
+ extractor_version TEXT, -- provenance: which extractor produced this
108
+ extraction_confidence REAL, -- 0..1 extractor confidence
109
+ source_turn_ref JSONB, -- pointer back to the source conversation turn
110
+ hash_id_v2 TEXT -- 16-byte server-minted id (dual identity; legacy hash_id untouched)
99
111
  );
112
+ -- Phase 1 hot-path indexes: recall filters to currently-valid rows; reconciliation groups by fact_key.
113
+ CREATE INDEX IF NOT EXISTS idx_memories_valid ON memories(id) WHERE invalid_at IS NULL;
114
+ CREATE INDEX IF NOT EXISTS idx_memories_fact_key ON memories(fact_key) WHERE fact_key IS NOT NULL;
115
+ CREATE INDEX IF NOT EXISTS idx_memories_hash_id_v2 ON memories(hash_id_v2) WHERE hash_id_v2 IS NOT NULL;
116
+
117
+ -- Memory 3.0 Phase 1 (migration 044): the C2 durable write outbox — replaces the
118
+ -- fire-and-forget embed/link/extract triad (errors were silently swallowed).
119
+ CREATE TABLE IF NOT EXISTS memory_write_jobs (
120
+ id BIGSERIAL PRIMARY KEY,
121
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
122
+ job_type TEXT NOT NULL CHECK (job_type IN ('embed', 'link', 'extract', 'reconcile', 'backfill_v2')),
123
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'done', 'failed')),
124
+ attempts INT NOT NULL DEFAULT 0,
125
+ next_retry_at TIMESTAMPTZ DEFAULT NOW(),
126
+ last_error TEXT,
127
+ created_at TIMESTAMPTZ DEFAULT NOW(),
128
+ updated_at TIMESTAMPTZ DEFAULT NOW()
129
+ );
130
+ CREATE INDEX IF NOT EXISTS idx_write_jobs_claim ON memory_write_jobs(status, next_retry_at)
131
+ WHERE status IN ('pending', 'failed');
132
+ CREATE INDEX IF NOT EXISTS idx_write_jobs_memory ON memory_write_jobs(memory_id);
100
133
 
101
134
  -- Memory fragments: granular vector decomposition for precision retrieval
102
135
  -- Each memory is split into semantic fragments (summary, content chunks, tag context)
@@ -110,6 +143,37 @@ CREATE TABLE IF NOT EXISTS memory_fragments (
110
143
  created_at TIMESTAMPTZ DEFAULT NOW()
111
144
  );
112
145
 
146
+ -- ============================================================
147
+ -- ENCRYPTION (PMP memory encryption-at-rest — see migration 021,
148
+ -- docs/pmp/memory-encryption-design.md §9). Owner-held envelope encryption.
149
+ -- ============================================================
150
+
151
+ -- Owner public-key registry. No private keys, ever.
152
+ CREATE TABLE IF NOT EXISTS encryption_keys (
153
+ owner_wallet TEXT PRIMARY KEY,
154
+ x25519_pubkey TEXT NOT NULL,
155
+ verifier_ct TEXT NOT NULL, -- secretbox("clude-key-verifier-v1") under derived key (H2)
156
+ created_at TIMESTAMPTZ DEFAULT NOW(),
157
+ updated_at TIMESTAMPTZ DEFAULT NOW()
158
+ );
159
+
160
+ -- Per-memory wrapped DEKs. Wrap = sealed box; wrap_pubkey is the ephemeral sender pubkey.
161
+ CREATE TABLE IF NOT EXISTS memory_dek_wraps (
162
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
163
+ recipient TEXT NOT NULL, -- 'owner' | 'provider' | 'title_holder'
164
+ wrapped_dek TEXT NOT NULL, -- base64(nonce || box(DEK))
165
+ wrap_pubkey TEXT NOT NULL, -- base64 ephemeral sender X25519 pubkey
166
+ holder_wallet TEXT, -- (034) names the title_holder; NULL for owner/provider
167
+ created_at TIMESTAMPTZ DEFAULT NOW(),
168
+ CONSTRAINT memory_dek_wraps_recipient_chk CHECK (recipient IN ('owner', 'provider', 'title_holder')),
169
+ CONSTRAINT memory_dek_wraps_holder_chk CHECK ((recipient = 'title_holder') = (holder_wallet IS NOT NULL))
170
+ );
171
+ -- (036) holder-aware uniqueness: one owner + one provider per memory (NULLS NOT DISTINCT), and one
172
+ -- title_holder per (memory, holder) so a title sale's seller + buyer wraps coexist (RT7 additive).
173
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_dek_wraps_identity
174
+ ON memory_dek_wraps (memory_id, recipient, holder_wallet) NULLS NOT DISTINCT;
175
+ CREATE INDEX IF NOT EXISTS idx_dek_wraps_memory ON memory_dek_wraps(memory_id);
176
+
113
177
  -- Dream logs: consolidation, reflection, emergence sessions
114
178
  CREATE TABLE IF NOT EXISTS dream_logs (
115
179
  id BIGSERIAL PRIMARY KEY,
@@ -133,6 +197,8 @@ CREATE INDEX IF NOT EXISTS idx_memories_evidence ON memories USING GIN(evidence_
133
197
  CREATE INDEX IF NOT EXISTS idx_memories_owner ON memories(owner_wallet);
134
198
  CREATE INDEX IF NOT EXISTS idx_memories_event_date ON memories(event_date) WHERE event_date IS NOT NULL;
135
199
  CREATE INDEX IF NOT EXISTS idx_memories_ts_summary ON memories USING GIN (ts_summary);
200
+ CREATE INDEX IF NOT EXISTS idx_memories_content_tokens ON memories USING GIN (content_tokens);
201
+ CREATE INDEX IF NOT EXISTS idx_memories_delegated ON memories(provider_delegated) WHERE encrypted = TRUE;
136
202
 
137
203
  -- Vector similarity indexes (HNSW for fast approximate nearest neighbor)
138
204
  CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw (embedding vector_cosine_ops);
@@ -168,7 +234,11 @@ BEGIN
168
234
  AND m.decay_factor >= min_decay
169
235
  AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
170
236
  AND (filter_user IS NULL OR m.related_user = filter_user)
171
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
237
+ AND (
238
+ filter_owner IS NULL
239
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
240
+ OR m.owner_wallet = filter_owner
241
+ )
172
242
  AND (filter_tags IS NULL OR m.tags && filter_tags)
173
243
  AND (1 - (m.embedding <=> query_embedding)) > match_threshold
174
244
  ORDER BY m.embedding <=> query_embedding
@@ -201,7 +271,11 @@ BEGIN
201
271
  AND m.decay_factor >= min_decay
202
272
  AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
203
273
  AND (filter_user IS NULL OR m.related_user = filter_user)
204
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
274
+ AND (
275
+ filter_owner IS NULL
276
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
277
+ OR m.owner_wallet = filter_owner
278
+ )
205
279
  AND (filter_tags IS NULL OR m.tags && filter_tags)
206
280
  AND (1 - (m.embedding <=> query_embedding)) > match_threshold
207
281
  AND (start_date IS NULL OR COALESCE(m.event_date, m.created_at) >= start_date)
@@ -213,6 +287,66 @@ $$;
213
287
 
214
288
  -- BM25-ranked full-text search using tsvector/tsquery (Exp 8)
215
289
  -- Replaces ilike-based keyword search with stemming and TF-IDF ranking.
290
+ -- Populate content_tokens from a transient plaintext arg (PostgREST can't express to_tsvector inline).
291
+ -- setweight 'B' mirrors the old combined ts_summary weighting (summary 'A' > content 'B').
292
+ CREATE OR REPLACE FUNCTION set_memory_content_tokens(p_memory_id bigint, p_text text)
293
+ RETURNS void LANGUAGE sql AS $$
294
+ UPDATE memories
295
+ SET content_tokens = CASE
296
+ WHEN p_text IS NULL OR p_text = '' THEN NULL
297
+ ELSE setweight(to_tsvector('english', p_text), 'B')
298
+ END
299
+ WHERE id = p_memory_id;
300
+ $$;
301
+
302
+ -- Atomic revoke: clear plaintext + drop the provider wrap in one transaction (encryption §7).
303
+ CREATE OR REPLACE FUNCTION revoke_memory(p_memory_id bigint, p_summary_ct text, p_embedding_ct text)
304
+ RETURNS void LANGUAGE plpgsql AS $$
305
+ BEGIN
306
+ UPDATE memories SET
307
+ summary = '',
308
+ summary_ciphertext = p_summary_ct,
309
+ embedding = NULL,
310
+ embedding_ciphertext = NULLIF(p_embedding_ct, ''),
311
+ content_tokens = NULL,
312
+ provider_delegated = false
313
+ WHERE id = p_memory_id;
314
+ DELETE FROM memory_dek_wraps WHERE memory_id = p_memory_id AND recipient = 'provider';
315
+ -- Fragment parity (migration 043): fragments hold plaintext + live embeddings,
316
+ -- which kept revoked memories findable through the fragment lane.
317
+ DELETE FROM memory_fragments WHERE memory_id = p_memory_id;
318
+ END;
319
+ $$;
320
+
321
+ -- Atomic re-delegate (encryption §7) — inverse of revoke_memory. Restores plaintext
322
+ -- summary/embedding, rebuilds content_tokens via the canonical builder, clears ciphertext
323
+ -- cols, sets provider_delegated=true, (re-)inserts the provider wrap. Caller passes decrypted
324
+ -- plaintext (it holds the validated DEK); plaintext is transient, not stored beyond columns.
325
+ CREATE OR REPLACE FUNCTION redelegate_memory(
326
+ p_memory_id bigint,
327
+ p_summary text,
328
+ p_embedding text,
329
+ p_content text,
330
+ p_wrapped_dek text,
331
+ p_wrap_pubkey text
332
+ )
333
+ RETURNS void LANGUAGE plpgsql AS $$
334
+ BEGIN
335
+ UPDATE memories SET
336
+ summary = p_summary,
337
+ summary_ciphertext = NULL,
338
+ embedding = NULLIF(p_embedding, '')::vector,
339
+ embedding_ciphertext = NULL,
340
+ provider_delegated = true
341
+ WHERE id = p_memory_id;
342
+ PERFORM set_memory_content_tokens(p_memory_id, p_content);
343
+ INSERT INTO memory_dek_wraps (memory_id, recipient, wrapped_dek, wrap_pubkey)
344
+ VALUES (p_memory_id, 'provider', p_wrapped_dek, p_wrap_pubkey)
345
+ ON CONFLICT (memory_id, recipient) DO UPDATE
346
+ SET wrapped_dek = EXCLUDED.wrapped_dek, wrap_pubkey = EXCLUDED.wrap_pubkey;
347
+ END;
348
+ $$;
349
+
216
350
  CREATE OR REPLACE FUNCTION bm25_search_memories(
217
351
  search_query text,
218
352
  match_count int DEFAULT 20,
@@ -231,11 +365,17 @@ BEGIN
231
365
  RETURN;
232
366
  END IF;
233
367
  RETURN QUERY
234
- SELECT m.id, ts_rank_cd(m.ts_summary, tsquery_val, 32)::float AS rank
368
+ SELECT m.id,
369
+ (ts_rank_cd(COALESCE(m.ts_summary, ''::tsvector), tsquery_val, 32)
370
+ + ts_rank_cd(COALESCE(m.content_tokens, ''::tsvector), tsquery_val, 32))::float AS rank
235
371
  FROM memories m
236
- WHERE m.ts_summary @@ tsquery_val
372
+ WHERE (m.ts_summary @@ tsquery_val OR m.content_tokens @@ tsquery_val)
237
373
  AND m.decay_factor >= min_decay
238
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
374
+ AND (
375
+ filter_owner IS NULL
376
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
377
+ OR m.owner_wallet = filter_owner
378
+ )
239
379
  AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
240
380
  AND (filter_tags IS NULL OR m.tags && filter_tags)
241
381
  ORDER BY rank DESC
@@ -262,6 +402,7 @@ CREATE TABLE IF NOT EXISTS memory_links (
262
402
  'follows', -- temporal sequence
263
403
  'relates', -- general semantic association
264
404
  'resolves', -- contradiction resolution outcome
405
+ 'supersedes', -- decisive replacement: new fact overrides an older one (Memory 3.0 C1, migration 044)
265
406
  'happens_before', -- temporal ordering
266
407
  'happens_after', -- temporal ordering
267
408
  'concurrent_with' -- temporal co-occurrence
@@ -300,7 +441,11 @@ LANGUAGE sql AS $$
300
441
  WHERE ml.source_id = ANY(seed_ids)
301
442
  AND ml.target_id != ALL(seed_ids)
302
443
  AND ml.strength >= min_strength
303
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
444
+ AND (
445
+ filter_owner IS NULL
446
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
447
+ OR m.owner_wallet = filter_owner
448
+ )
304
449
  UNION
305
450
  SELECT DISTINCT ON (ml.source_id, ml.link_type)
306
451
  ml.source_id AS memory_id,
@@ -312,7 +457,11 @@ LANGUAGE sql AS $$
312
457
  WHERE ml.target_id = ANY(seed_ids)
313
458
  AND ml.source_id != ALL(seed_ids)
314
459
  AND ml.strength >= min_strength
315
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
460
+ AND (
461
+ filter_owner IS NULL
462
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
463
+ OR m.owner_wallet = filter_owner
464
+ )
316
465
  ORDER BY strength DESC
317
466
  LIMIT max_results;
318
467
  $$;
@@ -411,7 +560,9 @@ CREATE OR REPLACE FUNCTION match_memory_fragments(
411
560
  query_embedding vector(1024),
412
561
  match_threshold float DEFAULT 0.3,
413
562
  match_count int DEFAULT 10,
414
- filter_owner text DEFAULT NULL
563
+ filter_owner text DEFAULT NULL,
564
+ min_decay float DEFAULT 0.0,
565
+ filter_types text[] DEFAULT NULL
415
566
  )
416
567
  RETURNS TABLE (memory_id bigint, max_similarity float)
417
568
  LANGUAGE plpgsql AS $$
@@ -422,7 +573,13 @@ BEGIN
422
573
  JOIN memories m ON m.id = f.memory_id
423
574
  WHERE f.embedding IS NOT NULL
424
575
  AND (1 - (f.embedding <=> query_embedding)) > match_threshold
425
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
576
+ AND m.decay_factor >= min_decay
577
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
578
+ AND (
579
+ filter_owner IS NULL
580
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
581
+ OR m.owner_wallet = filter_owner
582
+ )
426
583
  GROUP BY f.memory_id
427
584
  ORDER BY max_similarity DESC
428
585
  LIMIT match_count;