@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.
@@ -8,6 +8,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __esm = (fn, res) => function __init() {
9
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
10
  };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ };
11
14
  var __export = (target, all) => {
12
15
  for (var name in all)
13
16
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -688,7 +691,20 @@ var init_config = __esm({
688
691
  accessToken: requiredUnlessSiteOnly("X_ACCESS_TOKEN"),
689
692
  accessSecret: requiredUnlessSiteOnly("X_ACCESS_SECRET"),
690
693
  botUserId: requiredUnlessSiteOnly("X_BOT_USER_ID"),
691
- creatorUserId: optional("CREATOR_USER_ID", "")
694
+ creatorUserId: optional("CREATOR_USER_ID", ""),
695
+ /**
696
+ * Bearer token for the public "$ANSEM LIVE" feed (GET /api/ansem/feed).
697
+ * Reads-only X app bearer — used server-side for GET /2/tweets/search/recent.
698
+ * Empty → the feed endpoint returns { enabled:false } and the frontend panel hides.
699
+ * NEVER sent to the browser.
700
+ */
701
+ searchBearer: optional("X_SEARCH_BEARER", ""),
702
+ /** Min poll gap for the on-demand $ANSEM feed (ms). Idle → no polling → $0. */
703
+ ansemFeedIntervalMs: parseInt(optional("ANSEM_FEED_INTERVAL_MS", "90000"), 10),
704
+ /** Posts older than ~15min must clear this like floor (fresh posts are exempt). */
705
+ ansemFeedMinLikes: parseInt(optional("ANSEM_FEED_MIN_LIKES", "3"), 10),
706
+ /** Hard daily read-cap: once exceeded, serve the stale buffer (cost stop-loss). */
707
+ ansemFeedDailyReadCap: parseInt(optional("ANSEM_FEED_DAILY_READ_CAP", "40000"), 10)
692
708
  },
693
709
  supabase: {
694
710
  url: requiredUnlessSiteOnly("SUPABASE_URL"),
@@ -706,7 +722,9 @@ var init_config = __esm({
706
722
  optional("SOLANA_NETWORK", "mainnet-beta") === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com"
707
723
  ),
708
724
  botWalletPrivateKey: optional("BOT_WALLET_PRIVATE_KEY", ""),
709
- cludeTokenMint: optional("CLUUDE_TOKEN_MINT", "")
725
+ cludeTokenMint: optional("CLUUDE_TOKEN_MINT", ""),
726
+ /** Deployed memory-registry Anchor program ID. Empty → on-chain registration disabled. */
727
+ memoryRegistryProgramId: optional("CLUDE_PROGRAM_ID", "")
710
728
  },
711
729
  server: {
712
730
  port: parseInt(optional("PORT", "3000"), 10),
@@ -744,6 +762,69 @@ var init_config = __esm({
744
762
  freePromoCreditUsdc: parseFloat(optional("FREE_PROMO_CREDIT_USDC", "1")),
745
763
  freePromoExpiry: optional("FREE_PROMO_EXPIRY", "")
746
764
  },
765
+ memory: {
766
+ /**
767
+ * Memory 3.0 C2: route storeMemory enrichment (embed/link/extract) through the durable
768
+ * memory_write_jobs outbox instead of fire-and-forget. Default OFF — fire-and-forget stays
769
+ * the default until the worker is proven. Requires migrations 044 + 045; degrades gracefully
770
+ * (falls back to fire-and-forget) if unapplied.
771
+ */
772
+ outboxEnabled: optional("MEMORY_OUTBOX", "false") === "true",
773
+ /**
774
+ * Memory 3.0 C1: write-time reconciliation, SHADOW slice (LLM-free). When on, each write records
775
+ * a PROPOSED reconcile op (add / needs_router / skip) into memory_reconciliation_log WITHOUT
776
+ * applying it — the labeled sample the enforce path must earn its turn-on from. Default OFF;
777
+ * runs fully detached after embedMemory (zero write latency); degrades gracefully if migration
778
+ * 046 is unapplied. Disabled under BENCH_MODE. The router is a later slice; only the cosine gate
779
+ * runs here.
780
+ */
781
+ reconcileEnabled: optional("MEMORY_RECONCILE", "false") === "true",
782
+ /** Screen floor passed to match_memories_temporal — LOW so max_cosine is captured for below-LO
783
+ * writes (LO is tuned from the shadow data, not fixed up-front). */
784
+ reconcileFloor: Number(optional("MEMORY_RECONCILE_FLOOR", "0.5")),
785
+ /** "similar enough to reconcile" boundary → needs_router (vs add). A starting probe, not gospel. */
786
+ reconcileLo: Number(optional("MEMORY_RECONCILE_LO", "0.85")),
787
+ /** hi/mid band LABEL boundary for analysis (not a decision boundary). */
788
+ reconcileHi: Number(optional("MEMORY_RECONCILE_HI", "0.95")),
789
+ /**
790
+ * C1 slice 1.5: the LLM router. Its own sub-flag (default OFF, independent of reconcileEnabled)
791
+ * so gate-only instrumentation runs first and LO is calibrated from that data before any LLM
792
+ * spend. When on, a >= LO write is classified add/update/noop by reconcileModel (still SHADOW —
793
+ * the op is logged, never applied). Requires OpenRouter configured.
794
+ */
795
+ reconcileRouter: optional("MEMORY_RECONCILE_ROUTER", "false") === "true",
796
+ /** Router model — an explicit id (NEVER a cognitiveFunction, which would silently override it
797
+ * with the fast llama slot). Haiku-class default: capable enough for dup-vs-update, cheap. */
798
+ reconcileModel: optional("MEMORY_RECONCILE_MODEL", "anthropic/claude-haiku-4.5"),
799
+ /** Per-owner soft daily cap on router calls (approximate, per-process) — bounds LLM spend. */
800
+ reconcileBudget: Number(optional("MEMORY_RECONCILE_BUDGET", "200"))
801
+ },
802
+ oauth: {
803
+ /** HMAC secret for signing OAuth access-token JWTs. Empty disables the OAuth AS — bearer API-key auth still works. */
804
+ signingSecret: optional("OAUTH_SIGNING_SECRET", ""),
805
+ /** Issuer/audience identifier baked into tokens. Falls back to the request origin when empty. */
806
+ issuer: optional("OAUTH_ISSUER", ""),
807
+ /** Access-token lifetime in seconds (default 1h). */
808
+ accessTtlSec: parseInt(optional("OAUTH_ACCESS_TTL_SEC", "3600"), 10),
809
+ /** Refresh-token lifetime in seconds (default 30d). */
810
+ refreshTtlSec: parseInt(optional("OAUTH_REFRESH_TTL_SEC", "2592000"), 10),
811
+ /** Authorization-code lifetime in seconds (default 60s). */
812
+ codeTtlSec: parseInt(optional("OAUTH_CODE_TTL_SEC", "60"), 10)
813
+ },
814
+ stripe: {
815
+ /**
816
+ * Stripe secret API key (sk_live_… / sk_test_…). Empty disables the
817
+ * marketplace Stripe rail — the orchestrator/webhook fail closed without it.
818
+ * SDK/MCP consumers never need this, so it stays optional() (not required()).
819
+ */
820
+ secretKey: optional("STRIPE_SECRET_KEY", ""),
821
+ /**
822
+ * Stripe webhook signing secret (whsec_…) used by stripe.webhooks.constructEvent
823
+ * to verify the raw body BEFORE any DB write (Risk R6). Empty ⇒ every webhook is
824
+ * rejected, so a misconfigured deploy can never process an unverified event.
825
+ */
826
+ webhookSecret: optional("STRIPE_WEBHOOK_SECRET", "")
827
+ },
747
828
  campaign: {
748
829
  startDate: optional("CAMPAIGN_START", "")
749
830
  },
@@ -757,6 +838,51 @@ var init_config = __esm({
757
838
  queryApiKey: optional("EMBEDDING_QUERY_API_KEY", ""),
758
839
  queryModel: optional("EMBEDDING_QUERY_MODEL", "")
759
840
  },
841
+ migration: {
842
+ /**
843
+ * Database backend that getDb() targets during the GCP parallel-run.
844
+ * 'supabase' (default, current live infra) | 'cloudsql' (PostgREST /
845
+ * self-hosted Supabase in front of Cloud SQL). Flip per-layer for the
846
+ * shadow-stack cutover; Supabase stays the source of truth until sunset.
847
+ */
848
+ dbTarget: optional("DB_TARGET", "supabase"),
849
+ /** PostgREST endpoint for the Cloud SQL backend (used only when DB_TARGET=cloudsql). */
850
+ cloudsqlUrl: optional("CLOUDSQL_PGREST_URL", ""),
851
+ /** Service key for the Cloud SQL PostgREST backend (used only when DB_TARGET=cloudsql). */
852
+ cloudsqlServiceKey: optional("CLOUDSQL_SERVICE_KEY", ""),
853
+ /**
854
+ * Active embedding vector space for ingest + recall. 'voyage' (default,
855
+ * current corpus) | 'vertex' (shadow column, only after the LongMemEval gate
856
+ * passes). Separate from EMBEDDING_PROVIDER so the swap is A/B, not one-way.
857
+ */
858
+ embeddingActive: optional("EMBEDDING_ACTIVE", "voyage"),
859
+ /**
860
+ * Whether THIS process runs the in-server singleton timers (recall canary,
861
+ * marketplace delivery poller, title-mint reconciliation). Default true =
862
+ * current Railway behavior. Set false on the Cloud Run server so exactly one
863
+ * owner (the worker) runs them and autoscaling never duplicates them.
864
+ */
865
+ runInProcessTimers: optional("RUN_INPROCESS_TIMERS", "true") === "true"
866
+ },
867
+ vertex: {
868
+ /**
869
+ * Vertex AI embedding backend (the GCP replacement for Voyage), used only when
870
+ * ingest/backfill/recall target the 'vertex' space (EMBEDDING_ACTIVE=vertex or an
871
+ * explicit generateEmbeddingForSpace('vertex') call). Separate from config.embedding
872
+ * so the Vertex space can be backfilled + A/B-gated while Voyage stays live.
873
+ */
874
+ project: optional("VERTEX_PROJECT", optional("GCP_PROJECT", "")),
875
+ location: optional("VERTEX_LOCATION", "us-central1"),
876
+ model: optional("VERTEX_EMBEDDING_MODEL", "gemini-embedding-001"),
877
+ /** Output dims — MRL-truncated to 1024 so vector(1024) columns + HNSW + match_* RPCs are unchanged. */
878
+ dimensions: parseInt(optional("VERTEX_EMBEDDING_DIMENSIONS", "1024"), 10),
879
+ /**
880
+ * Optional static OAuth token override (from `gcloud auth print-access-token`) for
881
+ * local dev / smoke tests. Empty in prod: Cloud Run mints a token from the attached
882
+ * service account via the metadata server (SDK-free), no static key stored.
883
+ */
884
+ accessToken: optional("VERTEX_ACCESS_TOKEN", "")
885
+ },
760
886
  openrouter: {
761
887
  apiKey: optional("OPENROUTER_API_KEY", ""),
762
888
  model: optional("OPENROUTER_MODEL", "meta-llama/llama-3.3-70b-instruct")
@@ -770,6 +896,35 @@ var init_config = __esm({
770
896
  tavily: {
771
897
  apiKey: optional("TAVILY_API_KEY", "")
772
898
  },
899
+ higgsfield: {
900
+ /** Higgsfield API key ID — first half of the V2 `Authorization: Key <id>:<secret>` pair (server-only). Empty → /api/ansem/speak returns 501. */
901
+ apiKey: optional("HIGGSFIELD_API_KEY", ""),
902
+ /** Higgsfield API key SECRET — second half of the V2 `Key <id>:<secret>` pair. */
903
+ apiSecret: optional("HIGGSFIELD_API_SECRET", ""),
904
+ /** REST base URL (Higgsfield V2 API). */
905
+ apiBase: optional("HIGGSFIELD_API_BASE", "https://platform.higgsfield.ai"),
906
+ /**
907
+ * V2 model path for the seed_audio TTS model: create is POST {apiBase}/{ttsEndpoint}.
908
+ * Verified live against the Higgsfield V2 API — bytedance/seed-audio-1.0.
909
+ */
910
+ ttsEndpoint: optional("HIGGSFIELD_TTS_ENDPOINT", "bytedance/seed-audio-1.0"),
911
+ /** Ansem voice — Higgsfield "Sterling" preset (founder's choice). */
912
+ voiceId: optional("ANSEM_VOICE_ID", "dc382508-c8bd-443c-8cb2-46e57b8d2e6f"),
913
+ voiceType: optional("ANSEM_VOICE_TYPE", "preset"),
914
+ /** Deep-voice tuning: seed_audio pitch_rate / speech_rate (integers, default 0). */
915
+ voicePitch: parseInt(optional("ANSEM_VOICE_PITCH", "-9"), 10),
916
+ voiceSpeechRate: parseInt(optional("ANSEM_VOICE_SPEECH_RATE", "-12"), 10),
917
+ /** Output audio format (seed_audio supports wav|mp3|pcm|ogg_opus). */
918
+ audioFormat: optional("ANSEM_VOICE_FORMAT", "mp3")
919
+ },
920
+ elevenlabs: {
921
+ /** ElevenLabs API key (server-only). Set → PRIMARY TTS (~1-3s, no lag); empty → falls back to Higgsfield. */
922
+ apiKey: optional("ELEVENLABS_API_KEY", ""),
923
+ /** Voice id — default "Brian" (deep, resonant, american). Swap via ELEVENLABS_VOICE_ID. */
924
+ voiceId: optional("ELEVENLABS_VOICE_ID", "nPczCjzI2devNBz1zQrb"),
925
+ /** Model — turbo for lowest latency. */
926
+ model: optional("ELEVENLABS_MODEL", "eleven_turbo_v2_5")
927
+ },
773
928
  privy: {
774
929
  appId: optional("PRIVY_APP_ID", ""),
775
930
  appSecret: optional("PRIVY_APP_SECRET", ""),
@@ -846,28 +1001,70 @@ var init_logger = __esm({
846
1001
  }
847
1002
  });
848
1003
 
1004
+ // packages/shared/src/core/migration-profile.ts
1005
+ function defaultSupabaseConnection() {
1006
+ return { url: config.supabase.url, serviceKey: config.supabase.serviceKey };
1007
+ }
1008
+ function resolveDbConnection(profile = config.migration, supabase2 = defaultSupabaseConnection()) {
1009
+ if (!DB_TARGETS.includes(profile.dbTarget)) {
1010
+ throw new Error(
1011
+ `Invalid DB_TARGET '${profile.dbTarget}' (expected 'supabase' | 'cloudsql')`
1012
+ );
1013
+ }
1014
+ if (profile.dbTarget === "cloudsql") {
1015
+ if (!profile.cloudsqlUrl || !profile.cloudsqlServiceKey) {
1016
+ throw new Error(
1017
+ "DB_TARGET=cloudsql requires CLOUDSQL_PGREST_URL and CLOUDSQL_SERVICE_KEY"
1018
+ );
1019
+ }
1020
+ return { url: profile.cloudsqlUrl, serviceKey: profile.cloudsqlServiceKey };
1021
+ }
1022
+ return { url: supabase2.url, serviceKey: supabase2.serviceKey };
1023
+ }
1024
+ function activeEmbeddingSpace(profile = config.migration) {
1025
+ if (!EMBEDDING_SPACES.includes(profile.embeddingActive)) {
1026
+ throw new Error(
1027
+ `Invalid EMBEDDING_ACTIVE '${profile.embeddingActive}' (expected 'voyage' | 'vertex')`
1028
+ );
1029
+ }
1030
+ return profile.embeddingActive;
1031
+ }
1032
+ function vectorRpcName(baseRpc, profile = config.migration) {
1033
+ return activeEmbeddingSpace(profile) === "vertex" ? `${baseRpc}_vertex` : baseRpc;
1034
+ }
1035
+ var DB_TARGETS, EMBEDDING_SPACES;
1036
+ var init_migration_profile = __esm({
1037
+ "packages/shared/src/core/migration-profile.ts"() {
1038
+ "use strict";
1039
+ init_config();
1040
+ DB_TARGETS = ["supabase", "cloudsql"];
1041
+ EMBEDDING_SPACES = ["voyage", "vertex"];
1042
+ }
1043
+ });
1044
+
849
1045
  // packages/shared/src/core/database.ts
850
1046
  var database_exports = {};
851
1047
  __export(database_exports, {
852
1048
  _setDb: () => _setDb,
853
1049
  claimForProcessing: () => claimForProcessing,
854
1050
  getDb: () => getDb,
1051
+ getSchemaDriftReport: () => getSchemaDriftReport,
855
1052
  initDatabase: () => initDatabase2,
856
1053
  isAlreadyProcessed: () => isAlreadyProcessed,
857
1054
  markProcessed: () => markProcessed
858
1055
  });
859
1056
  function getDb() {
860
1057
  if (!supabase) {
861
- supabase = (0, import_supabase_js.createClient)(config.supabase.url, config.supabase.serviceKey);
862
- log.info("Supabase client initialized");
1058
+ const conn = resolveDbConnection();
1059
+ supabase = (0, import_supabase_js.createClient)(conn.url, conn.serviceKey);
1060
+ log.info({ dbTarget: config.migration.dbTarget }, "Database client initialized");
863
1061
  }
864
1062
  return supabase;
865
1063
  }
866
1064
  function _setDb(client) {
867
1065
  supabase = client;
868
1066
  }
869
- async function initDatabase2() {
870
- const db = getDb();
1067
+ async function runBootDdl(db) {
871
1068
  try {
872
1069
  const { error } = await db.rpc("exec_sql", {
873
1070
  query: `
@@ -939,6 +1136,7 @@ async function initDatabase2() {
939
1136
  input_memory_ids BIGINT[] DEFAULT '{}',
940
1137
  output TEXT NOT NULL,
941
1138
  new_memories_created BIGINT[] DEFAULT '{}',
1139
+ owner_wallet TEXT,
942
1140
  created_at TIMESTAMPTZ DEFAULT NOW()
943
1141
  );
944
1142
 
@@ -1023,7 +1221,7 @@ async function initDatabase2() {
1023
1221
  target_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
1024
1222
  link_type TEXT NOT NULL CHECK (link_type IN (
1025
1223
  'supports', 'contradicts', 'elaborates', 'causes', 'follows', 'relates', 'resolves',
1026
- 'happens_before', 'happens_after', 'concurrent_with'
1224
+ 'supersedes', 'happens_before', 'happens_after', 'concurrent_with'
1027
1225
  )),
1028
1226
  strength REAL DEFAULT 0.5,
1029
1227
  created_at TIMESTAMPTZ DEFAULT NOW(),
@@ -1058,7 +1256,11 @@ async function initDatabase2() {
1058
1256
  WHERE ml.source_id = ANY(seed_ids)
1059
1257
  AND ml.target_id != ALL(seed_ids)
1060
1258
  AND ml.strength >= min_strength
1061
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
1259
+ AND (
1260
+ filter_owner IS NULL
1261
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1262
+ OR m.owner_wallet = filter_owner
1263
+ )
1062
1264
  UNION
1063
1265
  SELECT DISTINCT ON (ml.source_id, ml.link_type)
1064
1266
  ml.source_id AS memory_id,
@@ -1070,7 +1272,11 @@ async function initDatabase2() {
1070
1272
  WHERE ml.target_id = ANY(seed_ids)
1071
1273
  AND ml.source_id != ALL(seed_ids)
1072
1274
  AND ml.strength >= min_strength
1073
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
1275
+ AND (
1276
+ filter_owner IS NULL
1277
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1278
+ OR m.owner_wallet = filter_owner
1279
+ )
1074
1280
  ORDER BY strength DESC
1075
1281
  LIMIT max_results;
1076
1282
  $$;
@@ -1098,6 +1304,10 @@ async function initDatabase2() {
1098
1304
  ALTER TABLE dream_logs ADD CONSTRAINT dream_logs_session_type_check
1099
1305
  CHECK (session_type IN ('consolidation', 'reflection', 'emergence', 'compaction', 'decay', 'contradiction_resolution'));
1100
1306
 
1307
+ -- Migration 021: per-owner attribution for dream_logs (fixes totalDreamSessions leak)
1308
+ ALTER TABLE dream_logs ADD COLUMN IF NOT EXISTS owner_wallet TEXT;
1309
+ CREATE INDEX IF NOT EXISTS idx_dream_logs_owner ON dream_logs(owner_wallet);
1310
+
1101
1311
  -- Migration: add 'resolves' + temporal link types
1102
1312
  ALTER TABLE memory_links DROP CONSTRAINT IF EXISTS memory_links_link_type_check;
1103
1313
  ALTER TABLE memory_links ADD CONSTRAINT memory_links_link_type_check
@@ -1279,6 +1489,170 @@ async function initDatabase2() {
1279
1489
  CREATE INDEX IF NOT EXISTS idx_memories_event_date ON memories(event_date)
1280
1490
  WHERE event_date IS NOT NULL;
1281
1491
 
1492
+ -- Lexical index for keyword/BM25 search (encryption \xA79). content_tokens is
1493
+ -- app-maintained; ts_summary (summary-only) is added on fresh deploys that lack it.
1494
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_tokens tsvector;
1495
+ CREATE INDEX IF NOT EXISTS idx_memories_content_tokens ON memories USING GIN(content_tokens);
1496
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS provider_delegated BOOLEAN DEFAULT TRUE;
1497
+ CREATE INDEX IF NOT EXISTS idx_memories_delegated ON memories(provider_delegated) WHERE encrypted = TRUE;
1498
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS summary_ciphertext TEXT;
1499
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS embedding_ciphertext TEXT;
1500
+
1501
+ -- Memory 3.0 Phase 1 (migration 044): bi-temporal validity + provenance (additive/nullable, inert until MEMORY_RECONCILE)
1502
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS valid_from TIMESTAMPTZ;
1503
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS invalid_at TIMESTAMPTZ;
1504
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS superseded_by TEXT;
1505
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS fact_key TEXT;
1506
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS extractor_version TEXT;
1507
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS extraction_confidence REAL;
1508
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS source_turn_ref JSONB;
1509
+ ALTER TABLE memories ADD COLUMN IF NOT EXISTS hash_id_v2 TEXT;
1510
+ CREATE INDEX IF NOT EXISTS idx_memories_valid ON memories(id) WHERE invalid_at IS NULL;
1511
+ CREATE INDEX IF NOT EXISTS idx_memories_fact_key ON memories(fact_key) WHERE fact_key IS NOT NULL;
1512
+ CREATE INDEX IF NOT EXISTS idx_memories_hash_id_v2 ON memories(hash_id_v2) WHERE hash_id_v2 IS NOT NULL;
1513
+
1514
+ -- Memory 3.0 Phase 1 (migration 044): the C2 durable write outbox.
1515
+ CREATE TABLE IF NOT EXISTS memory_write_jobs (
1516
+ id BIGSERIAL PRIMARY KEY,
1517
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
1518
+ job_type TEXT NOT NULL CHECK (job_type IN ('enrich', 'embed', 'link', 'extract', 'reconcile', 'backfill_v2')),
1519
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'done', 'failed')),
1520
+ attempts INT NOT NULL DEFAULT 0,
1521
+ next_retry_at TIMESTAMPTZ DEFAULT NOW(),
1522
+ last_error TEXT,
1523
+ created_at TIMESTAMPTZ DEFAULT NOW(),
1524
+ updated_at TIMESTAMPTZ DEFAULT NOW()
1525
+ );
1526
+ CREATE INDEX IF NOT EXISTS idx_write_jobs_claim ON memory_write_jobs(status, next_retry_at) WHERE status IN ('pending', 'failed');
1527
+ CREATE INDEX IF NOT EXISTS idx_write_jobs_memory ON memory_write_jobs(memory_id);
1528
+ CREATE INDEX IF NOT EXISTS idx_write_jobs_running ON memory_write_jobs(updated_at) WHERE status = 'running';
1529
+
1530
+ -- Memory 3.0 C2 outbox \u2014 MIRROR of migration 045 (byte-equivalent). The table above is in
1531
+ -- the boot blob, so the claim RPC + idempotency objects MUST be too: otherwise a
1532
+ -- boot-provisioned box gets the table but not the RPC and the worker silently never drains
1533
+ -- (the migration-028 class). Keep in sync with 045.
1534
+ DO $do$
1535
+ BEGIN
1536
+ ALTER TABLE memory_write_jobs DROP CONSTRAINT IF EXISTS memory_write_jobs_job_type_check;
1537
+ ALTER TABLE memory_write_jobs ADD CONSTRAINT memory_write_jobs_job_type_check
1538
+ CHECK (job_type IN ('enrich', 'embed', 'link', 'extract', 'reconcile', 'backfill_v2'));
1539
+ EXCEPTION WHEN undefined_table THEN NULL; END $do$;
1540
+
1541
+ CREATE OR REPLACE FUNCTION claim_memory_write_jobs(
1542
+ p_limit INT DEFAULT 20, p_stale_running INTERVAL DEFAULT INTERVAL '15 minutes', p_owner TEXT DEFAULT NULL
1543
+ )
1544
+ RETURNS TABLE (id BIGINT, memory_id BIGINT, job_type TEXT, attempts INT, owner_wallet TEXT)
1545
+ LANGUAGE plpgsql AS $claimfn$
1546
+ BEGIN
1547
+ RETURN QUERY
1548
+ UPDATE memory_write_jobs j
1549
+ SET status = 'running', attempts = j.attempts + 1, updated_at = NOW()
1550
+ FROM (
1551
+ SELECT jj.id FROM memory_write_jobs jj JOIN memories m ON m.id = jj.memory_id
1552
+ WHERE ((jj.status IN ('pending','failed') AND jj.next_retry_at IS NOT NULL AND jj.next_retry_at <= NOW())
1553
+ OR (jj.status = 'running' AND jj.updated_at < NOW() - p_stale_running))
1554
+ AND (p_owner IS NULL OR m.owner_wallet = p_owner OR (p_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL))
1555
+ ORDER BY jj.next_retry_at ASC NULLS LAST LIMIT p_limit FOR UPDATE SKIP LOCKED
1556
+ ) claimed
1557
+ WHERE j.id = claimed.id
1558
+ RETURNING j.id, j.memory_id, j.job_type, j.attempts,
1559
+ (SELECT mm.owner_wallet FROM memories mm WHERE mm.id = j.memory_id);
1560
+ END; $claimfn$;
1561
+
1562
+ DO $do$
1563
+ BEGIN
1564
+ DELETE FROM memory_links a USING memory_links b
1565
+ WHERE a.ctid < b.ctid AND a.source_id = b.source_id AND a.target_id = b.target_id AND a.link_type = b.link_type;
1566
+ ALTER TABLE memory_links DROP CONSTRAINT IF EXISTS memory_links_unique_edge;
1567
+ ALTER TABLE memory_links ADD CONSTRAINT memory_links_unique_edge UNIQUE (source_id, target_id, link_type);
1568
+ EXCEPTION WHEN undefined_table THEN NULL; END $do$;
1569
+
1570
+ CREATE OR REPLACE FUNCTION upsert_entity_relation(
1571
+ p_src BIGINT, p_tgt BIGINT, p_type TEXT, p_evidence BIGINT DEFAULT NULL, p_strength REAL DEFAULT 0.5
1572
+ )
1573
+ RETURNS VOID LANGUAGE sql AS $uerfn$
1574
+ INSERT INTO entity_relations (source_entity_id, target_entity_id, relation_type, strength, evidence_memory_ids)
1575
+ VALUES (p_src, p_tgt, p_type, LEAST(1.0, p_strength),
1576
+ CASE WHEN p_evidence IS NULL THEN '{}'::bigint[] ELSE ARRAY[p_evidence] END)
1577
+ ON CONFLICT (source_entity_id, target_entity_id, relation_type) DO UPDATE
1578
+ SET strength = LEAST(1.0, entity_relations.strength +
1579
+ CASE WHEN p_evidence IS NULL OR p_evidence = ANY(entity_relations.evidence_memory_ids) THEN 0.0 ELSE 0.1 END),
1580
+ evidence_memory_ids = (SELECT ARRAY(SELECT DISTINCT e FROM unnest(entity_relations.evidence_memory_ids || EXCLUDED.evidence_memory_ids) AS e));
1581
+ $uerfn$;
1582
+
1583
+ -- Memory 3.0 C1 (migration 046): reconciliation SHADOW decision log. MIRROR \u2014 byte-equivalent
1584
+ -- to migration 046. Records a PROPOSED reconcile op per write without applying it, so enforce
1585
+ -- can be greenlit from a labeled sample. Deliberately NOT added to CORE_TABLES (dormant,
1586
+ -- default-off; must not trip SCHEMA DRIFT at boot on an un-migrated prod box \u2014 C2 precedent).
1587
+ CREATE TABLE IF NOT EXISTS memory_reconciliation_log (
1588
+ id BIGSERIAL PRIMARY KEY,
1589
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
1590
+ owner_wallet TEXT,
1591
+ mode TEXT NOT NULL DEFAULT 'shadow' CHECK (mode IN ('shadow','enforce')),
1592
+ proposed_op TEXT NOT NULL CHECK (proposed_op IN ('add','update','noop','needs_router','skip')),
1593
+ target_memory_id BIGINT,
1594
+ max_cosine REAL,
1595
+ band TEXT CHECK (band IN ('hi','mid','lo','none')),
1596
+ router_used BOOLEAN NOT NULL DEFAULT false,
1597
+ router_model TEXT,
1598
+ fact_key TEXT,
1599
+ reason TEXT,
1600
+ label TEXT,
1601
+ labeled_at TIMESTAMPTZ,
1602
+ gate_version TEXT,
1603
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1604
+ );
1605
+ CREATE INDEX IF NOT EXISTS idx_reconcile_log_owner_created ON memory_reconciliation_log(owner_wallet, created_at);
1606
+ CREATE INDEX IF NOT EXISTS idx_reconcile_log_op ON memory_reconciliation_log(proposed_op);
1607
+ CREATE INDEX IF NOT EXISTS idx_reconcile_log_router ON memory_reconciliation_log(mode, router_used);
1608
+
1609
+ DO $do$
1610
+ BEGIN
1611
+ IF NOT EXISTS (
1612
+ SELECT 1 FROM information_schema.columns
1613
+ WHERE table_name = 'memories' AND column_name = 'ts_summary'
1614
+ ) THEN
1615
+ ALTER TABLE memories ADD COLUMN ts_summary tsvector GENERATED ALWAYS AS (
1616
+ setweight(to_tsvector('english', COALESCE(summary, '')), 'A')
1617
+ ) STORED;
1618
+ CREATE INDEX IF NOT EXISTS idx_memories_ts_summary ON memories USING GIN(ts_summary);
1619
+ END IF;
1620
+ END $do$;
1621
+
1622
+ -- Semantic search across memory-level embeddings with metadata filtering (the always-on
1623
+ -- recall vector lane). MIRROR of migration 028 / supabase-schema.sql \u2014 8-arg filter_tags form.
1624
+ CREATE OR REPLACE FUNCTION match_memories(
1625
+ query_embedding vector(1024),
1626
+ match_threshold float DEFAULT 0.3,
1627
+ match_count int DEFAULT 10,
1628
+ filter_types text[] DEFAULT NULL,
1629
+ filter_user text DEFAULT NULL,
1630
+ min_decay float DEFAULT 0.1,
1631
+ filter_owner text DEFAULT NULL,
1632
+ filter_tags text[] DEFAULT NULL
1633
+ )
1634
+ RETURNS TABLE (id bigint, similarity float)
1635
+ LANGUAGE plpgsql AS $$
1636
+ BEGIN
1637
+ RETURN QUERY
1638
+ SELECT m.id, (1 - (m.embedding <=> query_embedding))::float AS similarity
1639
+ FROM memories m
1640
+ WHERE m.embedding IS NOT NULL
1641
+ AND m.decay_factor >= min_decay
1642
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
1643
+ AND (filter_user IS NULL OR m.related_user = filter_user)
1644
+ AND (
1645
+ filter_owner IS NULL
1646
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1647
+ OR m.owner_wallet = filter_owner
1648
+ )
1649
+ AND (filter_tags IS NULL OR m.tags && filter_tags)
1650
+ AND (1 - (m.embedding <=> query_embedding)) > match_threshold
1651
+ ORDER BY m.embedding <=> query_embedding
1652
+ LIMIT match_count;
1653
+ END;
1654
+ $$;
1655
+
1282
1656
  -- Temporal-aware semantic search RPC (Exp 9)
1283
1657
  CREATE OR REPLACE FUNCTION match_memories_temporal(
1284
1658
  query_embedding vector(1024),
@@ -1302,7 +1676,11 @@ async function initDatabase2() {
1302
1676
  AND m.decay_factor >= min_decay
1303
1677
  AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
1304
1678
  AND (filter_user IS NULL OR m.related_user = filter_user)
1305
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
1679
+ AND (
1680
+ filter_owner IS NULL
1681
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1682
+ OR m.owner_wallet = filter_owner
1683
+ )
1306
1684
  AND (filter_tags IS NULL OR m.tags && filter_tags)
1307
1685
  AND (1 - (m.embedding <=> query_embedding)) > match_threshold
1308
1686
  AND (start_date IS NULL OR COALESCE(m.event_date, m.created_at) >= start_date)
@@ -1312,8 +1690,124 @@ async function initDatabase2() {
1312
1690
  END;
1313
1691
  $$;
1314
1692
 
1315
- -- BM25-ranked full-text search RPC (Exp 8)
1316
- -- Note: ts_summary column requires manual migration (GENERATED ALWAYS AS is not ALTER-able)
1693
+ -- Fragment-level semantic search with deduplication to parent memory. Returns the highest
1694
+ -- similarity fragment per memory (the non-skipExpansion recall lane). MIRROR of
1695
+ -- supabase-schema.sql \u2014 6-arg migration-043 form; the 4-arg (migration 009) call still
1696
+ -- resolves against it via parameter defaults, so a boot-provisioned box is never left on a
1697
+ -- stale fragment signature.
1698
+ CREATE OR REPLACE FUNCTION match_memory_fragments(
1699
+ query_embedding vector(1024),
1700
+ match_threshold float DEFAULT 0.3,
1701
+ match_count int DEFAULT 10,
1702
+ filter_owner text DEFAULT NULL,
1703
+ min_decay float DEFAULT 0.0,
1704
+ filter_types text[] DEFAULT NULL
1705
+ )
1706
+ RETURNS TABLE (memory_id bigint, max_similarity float)
1707
+ LANGUAGE plpgsql AS $$
1708
+ BEGIN
1709
+ RETURN QUERY
1710
+ SELECT f.memory_id, MAX((1 - (f.embedding <=> query_embedding))::float) AS max_similarity
1711
+ FROM memory_fragments f
1712
+ JOIN memories m ON m.id = f.memory_id
1713
+ WHERE f.embedding IS NOT NULL
1714
+ AND (1 - (f.embedding <=> query_embedding)) > match_threshold
1715
+ AND m.decay_factor >= min_decay
1716
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
1717
+ AND (
1718
+ filter_owner IS NULL
1719
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1720
+ OR m.owner_wallet = filter_owner
1721
+ )
1722
+ GROUP BY f.memory_id
1723
+ ORDER BY max_similarity DESC
1724
+ LIMIT match_count;
1725
+ END;
1726
+ $$;
1727
+
1728
+ -- Encryption: owner key registry + per-memory wrapped DEKs (encryption \xA79, Plan 1 sync).
1729
+ CREATE TABLE IF NOT EXISTS encryption_keys (
1730
+ owner_wallet TEXT PRIMARY KEY,
1731
+ x25519_pubkey TEXT NOT NULL,
1732
+ verifier_ct TEXT NOT NULL,
1733
+ created_at TIMESTAMPTZ DEFAULT NOW(),
1734
+ updated_at TIMESTAMPTZ DEFAULT NOW()
1735
+ );
1736
+ CREATE TABLE IF NOT EXISTS memory_dek_wraps (
1737
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
1738
+ recipient TEXT NOT NULL,
1739
+ wrapped_dek TEXT NOT NULL,
1740
+ wrap_pubkey TEXT NOT NULL,
1741
+ holder_wallet TEXT, -- (034) names the title_holder; NULL for owner/provider
1742
+ created_at TIMESTAMPTZ DEFAULT NOW(),
1743
+ CONSTRAINT memory_dek_wraps_recipient_chk CHECK (recipient IN ('owner', 'provider', 'title_holder')),
1744
+ CONSTRAINT memory_dek_wraps_holder_chk CHECK ((recipient = 'title_holder') = (holder_wallet IS NOT NULL))
1745
+ );
1746
+ -- (036) holder-aware uniqueness: one owner + one provider per memory (NULLS NOT DISTINCT),
1747
+ -- and one title_holder per (memory, holder) so a sale's seller + buyer wraps coexist (RT7).
1748
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_dek_wraps_identity
1749
+ ON memory_dek_wraps (memory_id, recipient, holder_wallet) NULLS NOT DISTINCT;
1750
+ CREATE INDEX IF NOT EXISTS idx_dek_wraps_memory ON memory_dek_wraps(memory_id);
1751
+
1752
+ -- Populate content_tokens from a transient plaintext arg (PostgREST can't express to_tsvector inline).
1753
+ -- setweight 'B' mirrors the old combined ts_summary weighting (summary 'A' > content 'B').
1754
+ CREATE OR REPLACE FUNCTION set_memory_content_tokens(p_memory_id bigint, p_text text)
1755
+ RETURNS void LANGUAGE sql AS $fn$
1756
+ UPDATE memories
1757
+ SET content_tokens = CASE
1758
+ WHEN p_text IS NULL OR p_text = '' THEN NULL
1759
+ ELSE setweight(to_tsvector('english', p_text), 'B')
1760
+ END
1761
+ WHERE id = p_memory_id;
1762
+ $fn$;
1763
+
1764
+ -- Atomic revoke: clear plaintext + drop the provider wrap in one transaction (encryption \xA77).
1765
+ CREATE OR REPLACE FUNCTION revoke_memory(p_memory_id bigint, p_summary_ct text, p_embedding_ct text)
1766
+ RETURNS void LANGUAGE plpgsql AS $rev$
1767
+ BEGIN
1768
+ UPDATE memories SET
1769
+ summary = '',
1770
+ summary_ciphertext = p_summary_ct,
1771
+ embedding = NULL,
1772
+ embedding_ciphertext = NULLIF(p_embedding_ct, ''),
1773
+ content_tokens = NULL,
1774
+ provider_delegated = false
1775
+ WHERE id = p_memory_id;
1776
+ DELETE FROM memory_dek_wraps WHERE memory_id = p_memory_id AND recipient = 'provider';
1777
+ -- Fragment parity (migration 043): fragments hold plaintext + live embeddings.
1778
+ DELETE FROM memory_fragments WHERE memory_id = p_memory_id;
1779
+ END;
1780
+ $rev$;
1781
+
1782
+ -- Atomic re-delegate (encryption \xA77) \u2014 inverse of revoke_memory. Restores plaintext
1783
+ -- summary/embedding, rebuilds content_tokens via the canonical builder, clears ciphertext
1784
+ -- cols, sets provider_delegated=true, (re-)inserts the provider wrap.
1785
+ CREATE OR REPLACE FUNCTION redelegate_memory(
1786
+ p_memory_id bigint,
1787
+ p_summary text,
1788
+ p_embedding text,
1789
+ p_content text,
1790
+ p_wrapped_dek text,
1791
+ p_wrap_pubkey text
1792
+ )
1793
+ RETURNS void LANGUAGE plpgsql AS $redel$
1794
+ BEGIN
1795
+ UPDATE memories SET
1796
+ summary = p_summary,
1797
+ summary_ciphertext = NULL,
1798
+ embedding = NULLIF(p_embedding, '')::vector,
1799
+ embedding_ciphertext = NULL,
1800
+ provider_delegated = true
1801
+ WHERE id = p_memory_id;
1802
+ PERFORM set_memory_content_tokens(p_memory_id, p_content);
1803
+ INSERT INTO memory_dek_wraps (memory_id, recipient, wrapped_dek, wrap_pubkey)
1804
+ VALUES (p_memory_id, 'provider', p_wrapped_dek, p_wrap_pubkey)
1805
+ ON CONFLICT (memory_id, recipient) DO UPDATE
1806
+ SET wrapped_dek = EXCLUDED.wrapped_dek, wrap_pubkey = EXCLUDED.wrap_pubkey;
1807
+ END;
1808
+ $redel$;
1809
+
1810
+ -- BM25-ranked full-text search RPC (Exp 8) \u2014 dual-column (ts_summary + content_tokens)
1317
1811
  CREATE OR REPLACE FUNCTION bm25_search_memories(
1318
1812
  search_query text,
1319
1813
  match_count int DEFAULT 20,
@@ -1332,11 +1826,17 @@ async function initDatabase2() {
1332
1826
  RETURN;
1333
1827
  END IF;
1334
1828
  RETURN QUERY
1335
- SELECT m.id, ts_rank_cd(m.ts_summary, tsquery_val, 32)::float AS rank
1829
+ SELECT m.id,
1830
+ (ts_rank_cd(COALESCE(m.ts_summary, ''::tsvector), tsquery_val, 32)
1831
+ + ts_rank_cd(COALESCE(m.content_tokens, ''::tsvector), tsquery_val, 32))::float AS rank
1336
1832
  FROM memories m
1337
- WHERE m.ts_summary @@ tsquery_val
1833
+ WHERE (m.ts_summary @@ tsquery_val OR m.content_tokens @@ tsquery_val)
1338
1834
  AND m.decay_factor >= min_decay
1339
- AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
1835
+ AND (
1836
+ filter_owner IS NULL
1837
+ OR (filter_owner = '__BOT_OWN__' AND m.owner_wallet IS NULL)
1838
+ OR m.owner_wallet = filter_owner
1839
+ )
1340
1840
  AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
1341
1841
  AND (filter_tags IS NULL OR m.tags && filter_tags)
1342
1842
  ORDER BY rank DESC
@@ -1365,9 +1865,31 @@ async function initDatabase2() {
1365
1865
  tokens_prompt INTEGER,
1366
1866
  tokens_completion INTEGER,
1367
1867
  memory_ids INTEGER[],
1868
+ frontier_tokens INTEGER,
1869
+ memories_used INTEGER,
1870
+ tokens_saved INTEGER,
1368
1871
  created_at TIMESTAMPTZ DEFAULT NOW()
1369
1872
  );
1370
1873
  CREATE INDEX IF NOT EXISTS idx_chat_msg_conv ON chat_messages(conversation_id, created_at);
1874
+ CREATE INDEX IF NOT EXISTS idx_chat_msg_saved ON chat_messages(created_at) WHERE tokens_saved IS NOT NULL;
1875
+ -- Proof counter totals. "today" is UTC-based (resets 00:00 UTC). Full-table aggregate:
1876
+ -- callers MUST use a server-side TTL cache (see proof.routes.ts). Mirror of migration 026.
1877
+ CREATE OR REPLACE FUNCTION proof_tokens_saved_totals()
1878
+ RETURNS TABLE(
1879
+ measured_saved bigint,
1880
+ measured_today bigint,
1881
+ measured_frontier bigint,
1882
+ historical_prompt_sum bigint,
1883
+ n bigint
1884
+ ) LANGUAGE sql STABLE AS $$
1885
+ SELECT
1886
+ COALESCE(SUM(tokens_saved) FILTER (WHERE tokens_saved IS NOT NULL), 0)::bigint,
1887
+ COALESCE(SUM(tokens_saved) FILTER (WHERE tokens_saved IS NOT NULL AND created_at >= date_trunc('day', now())), 0)::bigint,
1888
+ COALESCE(SUM(frontier_tokens) FILTER (WHERE tokens_saved IS NOT NULL), 0)::bigint,
1889
+ COALESCE(SUM(tokens_prompt) FILTER (WHERE tokens_saved IS NULL), 0)::bigint,
1890
+ COUNT(*) FILTER (WHERE tokens_saved IS NOT NULL)::bigint
1891
+ FROM chat_messages;
1892
+ $$;
1371
1893
 
1372
1894
  -- Chat billing: balances, top-ups, and per-message usage
1373
1895
  CREATE TABLE IF NOT EXISTS chat_balances (
@@ -1422,6 +1944,50 @@ async function initDatabase2() {
1422
1944
  );
1423
1945
  CREATE INDEX IF NOT EXISTS idx_wiki_pack_installations_owner
1424
1946
  ON wiki_pack_installations(owner_wallet);
1947
+
1948
+ -- Migration 019: corrected access-boost RPC (no importance mutation on read).
1949
+ -- Re-asserted on every boot so the read->rank->read feedback loop can't return.
1950
+ -- importance_boosts is kept for call-site signature compatibility and ignored.
1951
+ -- Drop the stale single-arg overload (migration 003) so the two-arg version is
1952
+ -- unambiguous and the lockdown below can't be bypassed via an unrevoked overload.
1953
+ DROP FUNCTION IF EXISTS batch_boost_memory_access(bigint[]);
1954
+ CREATE OR REPLACE FUNCTION batch_boost_memory_access(
1955
+ memory_ids BIGINT[],
1956
+ importance_boosts DOUBLE PRECISION[] DEFAULT NULL
1957
+ ) RETURNS void LANGUAGE plpgsql AS $fn$
1958
+ BEGIN
1959
+ UPDATE memories
1960
+ SET access_count = access_count + 1,
1961
+ last_accessed = NOW(),
1962
+ decay_factor = LEAST(1.0, decay_factor + 0.1)
1963
+ WHERE id = ANY(memory_ids);
1964
+ END;
1965
+ $fn$;
1966
+
1967
+ -- Migration 020: lock down dangerous SECURITY DEFINER RPCs to service_role only.
1968
+ -- exec_sql/boost RPCs must never be callable by anon/authenticated (the publishable
1969
+ -- key ships in client apps). Wrapped so a not-yet-created function never aborts boot.
1970
+ DO $do$ BEGIN
1971
+ REVOKE EXECUTE ON FUNCTION exec_sql(text) FROM PUBLIC, anon, authenticated;
1972
+ GRANT EXECUTE ON FUNCTION exec_sql(text) TO service_role;
1973
+ EXCEPTION WHEN undefined_function THEN NULL; END $do$;
1974
+
1975
+ DO $do$ BEGIN
1976
+ REVOKE EXECUTE ON FUNCTION batch_boost_memory_access(bigint[], double precision[]) FROM PUBLIC, anon, authenticated;
1977
+ GRANT EXECUTE ON FUNCTION batch_boost_memory_access(bigint[], double precision[]) TO service_role;
1978
+ EXCEPTION WHEN undefined_function THEN NULL; END $do$;
1979
+
1980
+ DO $do$ BEGIN
1981
+ REVOKE EXECUTE ON FUNCTION boost_memory_importance(bigint, double precision, double precision) FROM PUBLIC, anon, authenticated;
1982
+ GRANT EXECUTE ON FUNCTION boost_memory_importance(bigint, double precision, double precision) TO service_role;
1983
+ EXCEPTION WHEN undefined_function THEN NULL; END $do$;
1984
+
1985
+ -- Migration 022 (guard portion): reject empty content on NEW writes in every env.
1986
+ -- NOT VALID enforces on new INSERT/UPDATE without scanning existing rows; the
1987
+ -- one-time cleanup of legacy blank rows + VALIDATE lives in the manual 022 file.
1988
+ DO $do$ BEGIN
1989
+ ALTER TABLE memories ADD CONSTRAINT memories_content_nonempty CHECK (length(btrim(content)) > 0) NOT VALID;
1990
+ EXCEPTION WHEN duplicate_object THEN NULL; END $do$;
1425
1991
  `
1426
1992
  });
1427
1993
  if (error) {
@@ -1430,7 +1996,75 @@ async function initDatabase2() {
1430
1996
  } catch {
1431
1997
  log.warn("rpc exec_sql not available. Create tables via Supabase SQL editor.");
1432
1998
  }
1433
- log.info("Database initialized");
1999
+ }
2000
+ function getSchemaDriftReport() {
2001
+ return lastSchemaReport;
2002
+ }
2003
+ async function verifyCoreSchema(db) {
2004
+ const missingTables = [];
2005
+ const probeErrors = [];
2006
+ for (const table of CORE_TABLES) {
2007
+ try {
2008
+ const { error } = await db.from(table).select("id", { head: true, count: "exact" }).limit(1);
2009
+ if (error) {
2010
+ if (MISSING_RE.test(error.message ?? "")) missingTables.push(table);
2011
+ else probeErrors.push(`${table}: ${error.message}`);
2012
+ }
2013
+ } catch (err) {
2014
+ probeErrors.push(`${table}: ${err instanceof Error ? err.message : String(err)}`);
2015
+ }
2016
+ }
2017
+ const brokenRpcs = [];
2018
+ for (const probe of CORE_RPC_PROBES) {
2019
+ try {
2020
+ const { error } = await db.rpc(probe.name, probe.args);
2021
+ if (error && MISSING_RE.test(error.message ?? "")) brokenRpcs.push(probe.name);
2022
+ } catch (err) {
2023
+ probeErrors.push(`${probe.name}: ${err instanceof Error ? err.message : String(err)}`);
2024
+ }
2025
+ }
2026
+ return { missingTables, brokenRpcs, probeErrors };
2027
+ }
2028
+ async function initDatabase2(dbOverride) {
2029
+ const db = dbOverride ?? getDb();
2030
+ const mode = process.env.INIT_DB_MODE ?? "auto";
2031
+ if (mode === "replay") {
2032
+ await runBootDdl(db);
2033
+ lastSchemaReport = { at: (/* @__PURE__ */ new Date()).toISOString(), status: "replayed", missingTables: [], brokenRpcs: [] };
2034
+ log.info("Database initialized (legacy replay mode)");
2035
+ return;
2036
+ }
2037
+ let { missingTables, brokenRpcs, probeErrors } = await verifyCoreSchema(db);
2038
+ if (mode === "auto" && missingTables.includes("memories")) {
2039
+ log.info("Fresh database detected (no memories table) \u2014 running boot DDL");
2040
+ await runBootDdl(db);
2041
+ ({ missingTables, brokenRpcs, probeErrors } = await verifyCoreSchema(db));
2042
+ const healthy2 = missingTables.length === 0 && brokenRpcs.length === 0;
2043
+ lastSchemaReport = {
2044
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2045
+ status: healthy2 ? "fresh-bootstrapped" : "drift",
2046
+ missingTables,
2047
+ brokenRpcs
2048
+ };
2049
+ if (healthy2) log.info("Database bootstrapped from boot DDL");
2050
+ else log.error({ missingTables, brokenRpcs }, "SCHEMA DRIFT after fresh bootstrap \u2014 check exec_sql availability");
2051
+ return;
2052
+ }
2053
+ if (probeErrors.length > 0 && missingTables.length === 0 && brokenRpcs.length === 0) {
2054
+ lastSchemaReport = { at: (/* @__PURE__ */ new Date()).toISOString(), status: "unknown", missingTables, brokenRpcs };
2055
+ log.warn({ probeErrors: probeErrors.slice(0, 3) }, "Schema verification inconclusive (probe errors); skipping");
2056
+ return;
2057
+ }
2058
+ const healthy = missingTables.length === 0 && brokenRpcs.length === 0;
2059
+ lastSchemaReport = { at: (/* @__PURE__ */ new Date()).toISOString(), status: healthy ? "ok" : "drift", missingTables, brokenRpcs };
2060
+ if (healthy) {
2061
+ log.info("Database schema verified (boot blob frozen; migrations are the single schema writer)");
2062
+ } else {
2063
+ log.error(
2064
+ { missingTables, brokenRpcs },
2065
+ "SCHEMA DRIFT \u2014 core objects missing on an established database; apply the corresponding migration by hand (the boot blob no longer auto-repairs)"
2066
+ );
2067
+ }
1434
2068
  }
1435
2069
  async function isAlreadyProcessed(tweetId) {
1436
2070
  const db = getDb();
@@ -1463,27 +2097,81 @@ async function markProcessed(tweetId, feature, responseTweetId, extra) {
1463
2097
  processed_at: (/* @__PURE__ */ new Date()).toISOString()
1464
2098
  });
1465
2099
  }
1466
- var import_supabase_js, log, supabase;
2100
+ var import_supabase_js, log, supabase, CORE_TABLES, CORE_RPC_PROBES, lastSchemaReport, MISSING_RE;
1467
2101
  var init_database = __esm({
1468
2102
  "packages/shared/src/core/database.ts"() {
1469
2103
  "use strict";
1470
2104
  import_supabase_js = require("@supabase/supabase-js");
1471
2105
  init_config();
1472
2106
  init_logger();
2107
+ init_migration_profile();
1473
2108
  log = createChildLogger("database");
2109
+ CORE_TABLES = [
2110
+ "memories",
2111
+ "memory_fragments",
2112
+ "memory_links",
2113
+ "agent_keys",
2114
+ "dream_logs",
2115
+ "rate_limits",
2116
+ "chat_conversations",
2117
+ "chat_messages"
2118
+ ];
2119
+ CORE_RPC_PROBES = [
2120
+ { name: "get_linked_memories", args: { seed_ids: [], min_strength: 0.1, max_results: 1, filter_owner: null } },
2121
+ { name: "bm25_search_memories", args: { search_query: "", match_count: 1, min_decay: 0.1, filter_owner: null } },
2122
+ // Vector recall lanes (memory.ts recall). match_memories is the always-on memory-level lane;
2123
+ // match_memory_fragments is the fragment lane (non-skipExpansion path). Both are silently absent
2124
+ // from a stale boot blob, so probe them so the gap surfaces as drift instead of a dead lane.
2125
+ // Vector-safe args (null embedding + match_count 0) make each a no-op read. filter_tags pins the
2126
+ // migration-028 8-arg match_memories overload recall calls unconditionally (catches a pre-028 box
2127
+ // where recall would break); the fragment probe stays the 4/6-arg common subset (min_decay +
2128
+ // filter_types are opt-in via MEMORY_FRAGMENT_FILTERS) so it never false-drifts a pre-043 box
2129
+ // still serving the migration-009 4-arg form.
2130
+ { name: "match_memories", args: { query_embedding: null, match_count: 0, filter_owner: null, filter_tags: null } },
2131
+ { name: "match_memory_fragments", args: { query_embedding: null, match_count: 0, filter_owner: null } },
2132
+ // Memory 3.0 C2: probe the outbox claim RPC so a table-without-RPC state (the §6 hazard) is
2133
+ // caught at boot as drift rather than silently never draining.
2134
+ { name: "claim_memory_write_jobs", args: { p_limit: 0 } }
2135
+ ];
2136
+ lastSchemaReport = null;
2137
+ MISSING_RE = /does not exist|could not find|schema cache/i;
1474
2138
  }
1475
2139
  });
1476
2140
 
1477
- // packages/shared/src/utils/format.ts
1478
- function timeAgo(dateStr) {
1479
- const ms = Date.now() - new Date(dateStr).getTime();
1480
- const hours = Math.floor(ms / (1e3 * 60 * 60));
1481
- if (hours < 1) return "just now";
1482
- if (hours < 24) return `${hours}h ago`;
1483
- const days = Math.floor(hours / 24);
1484
- if (days < 7) return `${days}d ago`;
1485
- return `${Math.floor(days / 7)}w ago`;
2141
+ // packages/shared/src/core/memory-grounding.ts
2142
+ function byMemoryDateAsc(a, b) {
2143
+ const ta = a.created_at ? Date.parse(a.created_at) : NaN;
2144
+ const tb = b.created_at ? Date.parse(b.created_at) : NaN;
2145
+ if (Number.isNaN(ta) && Number.isNaN(tb)) return 0;
2146
+ if (Number.isNaN(ta)) return 1;
2147
+ if (Number.isNaN(tb)) return -1;
2148
+ return ta - tb;
2149
+ }
2150
+ function renderGroundedLine(m, suffix = "") {
2151
+ const date = m.created_at ? new Date(m.created_at).toISOString().slice(0, 10) : "";
2152
+ const stamp = date ? `[${date}] ` : "";
2153
+ const summary = (m.summary || "").trim();
2154
+ const content = (m.content || "").trim();
2155
+ let body;
2156
+ if (!content || content === summary) {
2157
+ body = summary || content;
2158
+ } else if (content.length <= GROUNDED_CONTENT_MAX) {
2159
+ body = summary ? `${summary} \u2014 ${content}` : content;
2160
+ } else {
2161
+ const slice = content.slice(0, GROUNDED_CONTENT_MAX);
2162
+ body = summary ? `${summary} \u2014 ${slice}\u2026` : `${slice}\u2026`;
2163
+ }
2164
+ return `- ${stamp}${body}${suffix}`;
1486
2165
  }
2166
+ var GROUNDED_CONTENT_MAX;
2167
+ var init_memory_grounding = __esm({
2168
+ "packages/shared/src/core/memory-grounding.ts"() {
2169
+ "use strict";
2170
+ GROUNDED_CONTENT_MAX = 600;
2171
+ }
2172
+ });
2173
+
2174
+ // packages/shared/src/utils/format.ts
1487
2175
  function clamp(val, min, max) {
1488
2176
  return Math.max(min, Math.min(max, val));
1489
2177
  }
@@ -1501,7 +2189,7 @@ var init_text = __esm({
1501
2189
  });
1502
2190
 
1503
2191
  // packages/shared/src/utils/constants.ts
1504
- var MEMO_PROGRAM_ID, BOT_X_HANDLE, MEMORY_MIN_DECAY, MEMORY_MAX_CONTENT_LENGTH, MEMORY_MAX_SUMMARY_LENGTH, DECAY_RATES, RECENCY_DECAY_BASE, RETRIEVAL_WEIGHT_RECENCY, RETRIEVAL_WEIGHT_RELEVANCE, RETRIEVAL_WEIGHT_IMPORTANCE, RETRIEVAL_WEIGHT_VECTOR, KNOWLEDGE_TYPE_BOOST, VECTOR_MATCH_THRESHOLD, BOND_TYPE_WEIGHTS, RETRIEVAL_WEIGHT_GRAPH, RETRIEVAL_WEIGHT_COOCCURRENCE, LINK_SIMILARITY_THRESHOLD, MAX_AUTO_LINKS, LINK_CO_RETRIEVAL_BOOST, INTERNAL_MEMORY_SOURCES, INTERNAL_IMPORTANCE_BOOST, REFLECTION_MIN_INTERVAL_MS, WHALE_SELL_COOLDOWN_MS, MEMO_MAX_LENGTH;
2192
+ var MEMO_PROGRAM_ID, BOT_X_HANDLE, MEMORY_MIN_DECAY, MEMORY_MAX_CONTENT_LENGTH, MEMORY_MAX_SUMMARY_LENGTH, DECAY_RATES, RECENCY_DECAY_BASE, RETRIEVAL_WEIGHT_RECENCY, RETRIEVAL_WEIGHT_RELEVANCE, RETRIEVAL_WEIGHT_IMPORTANCE, RETRIEVAL_WEIGHT_VECTOR, RETRIEVAL_WEIGHT_BM25, KNOWLEDGE_TYPE_BOOST, VECTOR_MATCH_THRESHOLD, BOND_TYPE_WEIGHTS, RETRIEVAL_WEIGHT_GRAPH, RETRIEVAL_WEIGHT_COOCCURRENCE, LINK_SIMILARITY_THRESHOLD, MAX_AUTO_LINKS, LINK_CO_RETRIEVAL_BOOST, INTERNAL_MEMORY_SOURCES, EMBEDDING_FRAGMENT_MAX_LENGTH, REFLECTION_MIN_INTERVAL_MS, WHALE_SELL_COOLDOWN_MS, MEMO_MAX_LENGTH;
1505
2193
  var init_constants = __esm({
1506
2194
  "packages/shared/src/utils/constants.ts"() {
1507
2195
  "use strict";
@@ -1523,10 +2211,11 @@ var init_constants = __esm({
1523
2211
  // Journal entries persist like knowledge
1524
2212
  };
1525
2213
  RECENCY_DECAY_BASE = 0.995;
1526
- RETRIEVAL_WEIGHT_RECENCY = 1;
1527
- RETRIEVAL_WEIGHT_RELEVANCE = 2;
1528
- RETRIEVAL_WEIGHT_IMPORTANCE = 2;
1529
- RETRIEVAL_WEIGHT_VECTOR = 4;
2214
+ RETRIEVAL_WEIGHT_RECENCY = Number(process.env.RETRIEVAL_WEIGHT_RECENCY ?? "1.0");
2215
+ RETRIEVAL_WEIGHT_RELEVANCE = Number(process.env.RETRIEVAL_WEIGHT_RELEVANCE ?? "2.0");
2216
+ RETRIEVAL_WEIGHT_IMPORTANCE = Number(process.env.RETRIEVAL_WEIGHT_IMPORTANCE ?? "2.0");
2217
+ RETRIEVAL_WEIGHT_VECTOR = Number(process.env.RETRIEVAL_WEIGHT_VECTOR ?? "4.0");
2218
+ RETRIEVAL_WEIGHT_BM25 = Number(process.env.RETRIEVAL_WEIGHT_BM25 ?? "0.15");
1530
2219
  KNOWLEDGE_TYPE_BOOST = {
1531
2220
  semantic: 0.15,
1532
2221
  // Distilled knowledge gets meaningful boost
@@ -1543,6 +2232,8 @@ var init_constants = __esm({
1543
2232
  BOND_TYPE_WEIGHTS = {
1544
2233
  causes: 1,
1545
2234
  supports: 0.9,
2235
+ supersedes: 0.85,
2236
+ // decisive replacement: new fact overrides an older one (Memory 3.0 C1)
1546
2237
  concurrent_with: 0.8,
1547
2238
  resolves: 0.8,
1548
2239
  happens_before: 0.7,
@@ -1567,7 +2258,7 @@ var init_constants = __esm({
1567
2258
  "dream-cycle",
1568
2259
  "meditation"
1569
2260
  ]);
1570
- INTERNAL_IMPORTANCE_BOOST = 5e-3;
2261
+ EMBEDDING_FRAGMENT_MAX_LENGTH = 2e3;
1571
2262
  REFLECTION_MIN_INTERVAL_MS = 30 * 60 * 1e3;
1572
2263
  WHALE_SELL_COOLDOWN_MS = 30 * 60 * 1e3;
1573
2264
  MEMO_MAX_LENGTH = 100;
@@ -1604,6 +2295,17 @@ var init_guardrails = __esm({
1604
2295
  }
1605
2296
  });
1606
2297
 
2298
+ // packages/shared/src/core/model-capabilities.ts
2299
+ function modelRejectsSamplingParams(model) {
2300
+ if (!model) return false;
2301
+ return /opus[-_.]?4[-_.]?[789]\b|opus[-_.]?[5-9]\b/i.test(model);
2302
+ }
2303
+ var init_model_capabilities = __esm({
2304
+ "packages/shared/src/core/model-capabilities.ts"() {
2305
+ "use strict";
2306
+ }
2307
+ });
2308
+
1607
2309
  // packages/shared/src/core/openrouter-client.ts
1608
2310
  function getModelForFunction(fn) {
1609
2311
  return COGNITIVE_MODEL_MAP[fn] || OPENROUTER_MODELS["llama-70b"];
@@ -1670,7 +2372,8 @@ async function generateOpenRouterResponse(opts) {
1670
2372
  model,
1671
2373
  messages,
1672
2374
  max_tokens: maxTokens,
1673
- temperature: opts.temperature ?? 0.7
2375
+ // Opus 4.7+ via OpenRouter rejects temperature/top_p strip for those models.
2376
+ ...modelRejectsSamplingParams(model) ? {} : { temperature: opts.temperature ?? 0.7 }
1674
2377
  })
1675
2378
  });
1676
2379
  if (!response.ok) {
@@ -1701,6 +2404,7 @@ var init_openrouter_client = __esm({
1701
2404
  "packages/shared/src/core/openrouter-client.ts"() {
1702
2405
  "use strict";
1703
2406
  init_logger();
2407
+ init_model_capabilities();
1704
2408
  log3 = createChildLogger("openrouter");
1705
2409
  OPENROUTER_API_URL = "https://openrouter.ai/api/v1";
1706
2410
  OPENROUTER_MODELS = {
@@ -1775,10 +2479,11 @@ function getModel() {
1775
2479
  }
1776
2480
  async function generateImportanceScore(description) {
1777
2481
  const importancePrompt = process.env.CLUDE_IMPORTANCE_PROMPT || "You rate the importance of events for an AI agent. Respond with ONLY a single integer from 1 to 10. 1 = purely mundane. 5 = moderately important. 10 = extremely significant.";
2482
+ const scoreModel = getModel();
1778
2483
  const response = await getClient().messages.create({
1779
- model: getModel(),
2484
+ model: scoreModel,
1780
2485
  max_tokens: 10,
1781
- temperature: 0,
2486
+ ...modelRejectsSamplingParams(scoreModel) ? {} : { temperature: 0 },
1782
2487
  system: importancePrompt,
1783
2488
  messages: [{
1784
2489
  role: "user",
@@ -1799,6 +2504,7 @@ var init_claude_client = __esm({
1799
2504
  init_guardrails();
1800
2505
  init_openrouter_client();
1801
2506
  init_constants();
2507
+ init_model_capabilities();
1802
2508
  log4 = createChildLogger("claude-client");
1803
2509
  anthropic = null;
1804
2510
  try {
@@ -1956,7 +2662,7 @@ function stableStringify(value) {
1956
2662
  }
1957
2663
  if (typeof value === "object") {
1958
2664
  const obj = value;
1959
- const keys = Object.keys(obj).sort();
2665
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
1960
2666
  const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k]));
1961
2667
  return "{" + pairs.join(",") + "}";
1962
2668
  }
@@ -1999,9 +2705,12 @@ var init_content_hash = __esm({
1999
2705
  });
2000
2706
 
2001
2707
  // packages/tokenization/src/pack-merkle.ts
2708
+ var LEAF_PREFIX, INNER_PREFIX;
2002
2709
  var init_pack_merkle = __esm({
2003
2710
  "packages/tokenization/src/pack-merkle.ts"() {
2004
2711
  "use strict";
2712
+ LEAF_PREFIX = Buffer.from([0]);
2713
+ INNER_PREFIX = Buffer.from([1]);
2005
2714
  }
2006
2715
  });
2007
2716
 
@@ -2197,7 +2906,73 @@ async function generateEmbeddings(texts) {
2197
2906
  return texts.map(() => null);
2198
2907
  }
2199
2908
  }
2200
- var log6, PROVIDERS, _enabled, _overrideConfig, EMBEDDING_CACHE_MAX, EMBEDDING_CACHE_TTL_MS, _embeddingCache;
2909
+ async function _fetchMetadataToken() {
2910
+ const res = await fetch(METADATA_TOKEN_URL, { headers: { "Metadata-Flavor": "Google" } });
2911
+ if (!res.ok) throw new Error(`Vertex metadata token fetch failed: ${res.status}`);
2912
+ const j = await res.json();
2913
+ return { token: j.access_token, expiresInSec: j.expires_in };
2914
+ }
2915
+ async function getVertexAccessToken() {
2916
+ const override = config.vertex.accessToken;
2917
+ if (override) return override;
2918
+ const now = Date.now();
2919
+ if (_vertexToken && _vertexToken.expiresAtMs > now + 6e4) return _vertexToken.token;
2920
+ const { token, expiresInSec } = await _fetchMetadataToken();
2921
+ _vertexToken = { token, expiresAtMs: now + expiresInSec * 1e3 };
2922
+ return token;
2923
+ }
2924
+ function vertexPredictUrl(model) {
2925
+ const { project, location } = config.vertex;
2926
+ return `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:predict`;
2927
+ }
2928
+ function isVertexConfigured() {
2929
+ return !!config.vertex.project;
2930
+ }
2931
+ async function generateVertexEmbeddings(texts, dimensions) {
2932
+ if (texts.length === 0) return [];
2933
+ const { project, model, dimensions: defaultDims } = config.vertex;
2934
+ if (!project) {
2935
+ log6.warn("Vertex embeddings requested but VERTEX_PROJECT is unset");
2936
+ return texts.map(() => null);
2937
+ }
2938
+ const outputDimensionality = dimensions ?? defaultDims;
2939
+ const startMs = Date.now();
2940
+ try {
2941
+ const token = await getVertexAccessToken();
2942
+ const res = await fetch(vertexPredictUrl(model), {
2943
+ method: "POST",
2944
+ headers: {
2945
+ "Content-Type": "application/json",
2946
+ "Authorization": `Bearer ${token}`
2947
+ },
2948
+ body: JSON.stringify({
2949
+ instances: texts.map((t) => ({ content: t.slice(0, 8e3) })),
2950
+ parameters: { outputDimensionality }
2951
+ })
2952
+ });
2953
+ if (!res.ok) {
2954
+ const errText = await res.text();
2955
+ log6.error({ status: res.status, body: errText.slice(0, 200), provider: "vertex" }, "Vertex embedding API error");
2956
+ return texts.map(() => null);
2957
+ }
2958
+ const data = await res.json();
2959
+ const preds = data.predictions ?? [];
2960
+ log6.debug({ provider: "vertex", model, count: texts.length, elapsed: Date.now() - startMs }, "Vertex embeddings generated");
2961
+ return texts.map((_, i) => preds[i]?.embeddings?.values ?? null);
2962
+ } catch (err) {
2963
+ log6.error({ err, provider: "vertex" }, "Vertex embedding generation failed");
2964
+ return texts.map(() => null);
2965
+ }
2966
+ }
2967
+ async function generateVertexEmbedding(text, dimensions) {
2968
+ const [vec] = await generateVertexEmbeddings([text], dimensions);
2969
+ return vec ?? null;
2970
+ }
2971
+ async function generateQueryEmbeddingForSpace(space, text) {
2972
+ if (space === "vertex") return generateVertexEmbedding(text);
2973
+ return generateQueryEmbedding(text);
2974
+ }
2975
+ var log6, PROVIDERS, _enabled, _overrideConfig, EMBEDDING_CACHE_MAX, EMBEDDING_CACHE_TTL_MS, _embeddingCache, _vertexToken, METADATA_TOKEN_URL;
2201
2976
  var init_embeddings = __esm({
2202
2977
  "packages/shared/src/core/embeddings.ts"() {
2203
2978
  "use strict";
@@ -2230,6 +3005,8 @@ var init_embeddings = __esm({
2230
3005
  EMBEDDING_CACHE_MAX = 200;
2231
3006
  EMBEDDING_CACHE_TTL_MS = 30 * 60 * 1e3;
2232
3007
  _embeddingCache = /* @__PURE__ */ new Map();
3008
+ _vertexToken = null;
3009
+ METADATA_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
2233
3010
  }
2234
3011
  });
2235
3012
 
@@ -2533,6 +3310,489 @@ var init_bm25_search = __esm({
2533
3310
  }
2534
3311
  });
2535
3312
 
3313
+ // packages/brain/src/memory/content-tokens.ts
3314
+ async function setContentTokens(db, memoryId, plaintext) {
3315
+ try {
3316
+ const { error } = await db.rpc("set_memory_content_tokens", {
3317
+ p_memory_id: memoryId,
3318
+ p_text: plaintext
3319
+ });
3320
+ if (error) {
3321
+ log8.warn({ memoryId, error: error.message }, "set_memory_content_tokens failed (keyword recall degraded)");
3322
+ }
3323
+ } catch (err) {
3324
+ log8.warn({ memoryId, err }, "set_memory_content_tokens threw (keyword recall degraded)");
3325
+ }
3326
+ }
3327
+ var log8;
3328
+ var init_content_tokens = __esm({
3329
+ "packages/brain/src/memory/content-tokens.ts"() {
3330
+ "use strict";
3331
+ init_logger();
3332
+ log8 = createChildLogger("content-tokens");
3333
+ }
3334
+ });
3335
+
3336
+ // packages/brain/src/memory/reconcile-router.ts
3337
+ function shortReason(x) {
3338
+ return typeof x === "string" ? x.slice(0, 200) : "";
3339
+ }
3340
+ function parseRouterReply(raw, candidateIds, model) {
3341
+ const fail = (reason) => ({ op: "add", targetId: null, reason, model });
3342
+ let obj;
3343
+ try {
3344
+ obj = JSON.parse(raw);
3345
+ } catch {
3346
+ const m = raw.match(/\{[\s\S]*\}/);
3347
+ if (!m) return fail("router_parse_error");
3348
+ try {
3349
+ obj = JSON.parse(m[0]);
3350
+ } catch {
3351
+ return fail("router_parse_error");
3352
+ }
3353
+ }
3354
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return fail("router_invalid_shape");
3355
+ const rec = obj;
3356
+ const op = rec.op;
3357
+ if (op !== "add" && op !== "update" && op !== "noop") return fail("router_invalid_op");
3358
+ if (op === "add") return { op: "add", targetId: null, reason: shortReason(rec.reason), model };
3359
+ const rawTarget = rec.targetId ?? rec.target_id;
3360
+ const targetId = typeof rawTarget === "number" ? rawTarget : Number(rawTarget);
3361
+ if (!Number.isInteger(targetId) || !candidateIds.has(targetId)) return fail("router_invalid_target");
3362
+ return { op, targetId, reason: shortReason(rec.reason), model };
3363
+ }
3364
+ function buildPayload(newFact, candidates) {
3365
+ const lines = candidates.map(
3366
+ (c) => `[id ${c.id}] date: ${c.eventDate ?? "unknown"} \u2014 ${c.summary}`
3367
+ );
3368
+ return `NEW fact:
3369
+ date: ${newFact.eventDate ?? "unknown"} \u2014 ${newFact.summary}
3370
+
3371
+ Candidate facts:
3372
+ ${lines.join("\n")}`;
3373
+ }
3374
+ function isRouterConfigured() {
3375
+ return config.memory.reconcileRouter && !!config.memory.reconcileModel && isOpenRouterEnabled();
3376
+ }
3377
+ function utcDay() {
3378
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3379
+ }
3380
+ function rollDay() {
3381
+ const d = utcDay();
3382
+ if (d !== budgetDay) {
3383
+ budgetDay = d;
3384
+ budgetByOwner.clear();
3385
+ }
3386
+ }
3387
+ function budgetKey(owner) {
3388
+ return owner ?? "__BOT_OWN__";
3389
+ }
3390
+ function routerBudgetAvailable(owner) {
3391
+ rollDay();
3392
+ return (budgetByOwner.get(budgetKey(owner)) ?? 0) < config.memory.reconcileBudget;
3393
+ }
3394
+ function noteRouterCall(owner) {
3395
+ rollDay();
3396
+ const k = budgetKey(owner);
3397
+ budgetByOwner.set(k, (budgetByOwner.get(k) ?? 0) + 1);
3398
+ }
3399
+ async function routeReconcile(newFact, candidates) {
3400
+ const model = config.memory.reconcileModel;
3401
+ const candidateIds = new Set(candidates.map((c) => c.id));
3402
+ try {
3403
+ const raw = await generateOpenRouterResponse({
3404
+ systemPrompt: ROUTER_SYSTEM,
3405
+ messages: [{ role: "user", content: buildPayload(newFact, candidates) }],
3406
+ model,
3407
+ // explicit — never cognitiveFunction (which overrides model with the fast llama slot)
3408
+ maxTokens: 150,
3409
+ temperature: 0
3410
+ });
3411
+ return parseRouterReply(raw, candidateIds, model);
3412
+ } catch (err) {
3413
+ log9.warn({ err: err?.message }, "reconcile router call failed \u2014 defaulting to add");
3414
+ return { op: "add", targetId: null, reason: "router_error", model };
3415
+ }
3416
+ }
3417
+ var log9, ROUTER_SYSTEM, budgetDay, budgetByOwner;
3418
+ var init_reconcile_router = __esm({
3419
+ "packages/brain/src/memory/reconcile-router.ts"() {
3420
+ "use strict";
3421
+ init_config();
3422
+ init_logger();
3423
+ init_openrouter_client();
3424
+ log9 = createChildLogger("reconcile-router");
3425
+ ROUTER_SYSTEM = 'You are a memory reconciliation classifier. Given a NEW fact and a list of EXISTING candidate facts that all belong to the SAME user, decide the relationship of the NEW fact to the candidates. Respond with ONLY a single JSON object, no prose, no markdown.\nFormat: {"op":"add"|"update"|"noop","targetId":<candidate id or null>,"reason":"<short>"}\n- "noop": NEW states the SAME thing as candidate <targetId> (a duplicate). Set targetId.\n- "update": NEW corrects or SUPERSEDES candidate <targetId> \u2014 same underlying fact, changed value or a newer date. Set targetId to that candidate id.\n- "add": NEW is distinct from every candidate. targetId must be null.\nWhen unsure, choose "add". NEVER invent a targetId that is not in the candidate list.';
3426
+ budgetDay = "";
3427
+ budgetByOwner = /* @__PURE__ */ new Map();
3428
+ }
3429
+ });
3430
+
3431
+ // packages/brain/src/memory/reconcile-shadow.ts
3432
+ function logReconcileDegradeOnce(err, memoryId) {
3433
+ if (reconcileDegradeLogged) return;
3434
+ reconcileDegradeLogged = true;
3435
+ const e = err;
3436
+ log10.warn(
3437
+ { code: e?.code, message: e?.message, memoryId },
3438
+ "Reconcile shadow degraded (migration 046 unapplied?) \u2014 skipping; write unaffected"
3439
+ );
3440
+ }
3441
+ function gateVersion() {
3442
+ const { reconcileFloor: f, reconcileLo: lo, reconcileHi: hi } = config.memory;
3443
+ return `c1-shadow-v1:floor=${f}:lo=${lo}:hi=${hi}`;
3444
+ }
3445
+ async function insertShadowRow(row) {
3446
+ try {
3447
+ const { error } = await getDb().from("memory_reconciliation_log").insert(row);
3448
+ if (error) logReconcileDegradeOnce(error, row.memory_id);
3449
+ } catch (err) {
3450
+ logReconcileDegradeOnce(err, row.memory_id);
3451
+ }
3452
+ }
3453
+ async function runReconcileShadow(memoryId, scope) {
3454
+ if (process.env.BENCH_MODE === "true") return;
3455
+ const ownerWalletForRow = scope === SCOPE_BOT_OWN ? null : scope;
3456
+ const gv = gateVersion();
3457
+ const db = getDb();
3458
+ const skip = (reason) => insertShadowRow({
3459
+ memory_id: memoryId,
3460
+ owner_wallet: ownerWalletForRow,
3461
+ mode: "shadow",
3462
+ proposed_op: "skip",
3463
+ target_memory_id: null,
3464
+ max_cosine: null,
3465
+ band: "none",
3466
+ router_used: false,
3467
+ fact_key: null,
3468
+ reason,
3469
+ gate_version: gv
3470
+ });
3471
+ const add = (reason, maxCosine2, band2) => insertShadowRow({
3472
+ memory_id: memoryId,
3473
+ owner_wallet: ownerWalletForRow,
3474
+ mode: "shadow",
3475
+ proposed_op: "add",
3476
+ target_memory_id: null,
3477
+ max_cosine: maxCosine2,
3478
+ band: band2,
3479
+ router_used: false,
3480
+ fact_key: null,
3481
+ reason,
3482
+ gate_version: gv
3483
+ });
3484
+ let embedding;
3485
+ let newSummary = "";
3486
+ let newEventDate = null;
3487
+ try {
3488
+ const { data, error } = await db.from("memories").select("embedding, summary, event_date, created_at").eq("id", memoryId).maybeSingle();
3489
+ if (error) {
3490
+ logReconcileDegradeOnce(error, memoryId);
3491
+ return;
3492
+ }
3493
+ const row = data;
3494
+ embedding = row?.embedding ?? null;
3495
+ newSummary = row?.summary ?? "";
3496
+ newEventDate = row?.event_date ?? row?.created_at ?? null;
3497
+ } catch (err) {
3498
+ logReconcileDegradeOnce(err, memoryId);
3499
+ return;
3500
+ }
3501
+ if (!embedding) {
3502
+ await skip("no_embedding");
3503
+ return;
3504
+ }
3505
+ const queryEmbedding = typeof embedding === "string" ? embedding : JSON.stringify(embedding);
3506
+ let candidates;
3507
+ try {
3508
+ const { data, error } = await db.rpc("match_memories_temporal", {
3509
+ query_embedding: queryEmbedding,
3510
+ match_threshold: config.memory.reconcileFloor,
3511
+ match_count: K_CANDIDATES,
3512
+ start_date: null,
3513
+ end_date: null,
3514
+ filter_types: null,
3515
+ filter_user: null,
3516
+ min_decay: 0,
3517
+ filter_owner: scope,
3518
+ filter_tags: null
3519
+ });
3520
+ if (error) {
3521
+ logReconcileDegradeOnce(error, memoryId);
3522
+ return;
3523
+ }
3524
+ candidates = data ?? [];
3525
+ } catch (err) {
3526
+ logReconcileDegradeOnce(err, memoryId);
3527
+ return;
3528
+ }
3529
+ candidates = candidates.filter((c) => c.id !== memoryId);
3530
+ if (candidates.length === 0) {
3531
+ await add("no_candidate", null, "none");
3532
+ return;
3533
+ }
3534
+ const ids = candidates.map((c) => c.id);
3535
+ let validIds;
3536
+ const hydratedById = /* @__PURE__ */ new Map();
3537
+ try {
3538
+ const base = db.from("memories").select("id, owner_wallet, invalid_at, summary, event_date, created_at").in("id", ids);
3539
+ const scoped = scope === SCOPE_BOT_OWN ? base.is("owner_wallet", null) : base.eq("owner_wallet", scope);
3540
+ const { data, error } = await scoped;
3541
+ if (error) {
3542
+ logReconcileDegradeOnce(error, memoryId);
3543
+ return;
3544
+ }
3545
+ const rows = data ?? [];
3546
+ const valid = rows.filter((r) => r.invalid_at == null).filter((r) => scope === SCOPE_BOT_OWN ? r.owner_wallet == null : r.owner_wallet === scope);
3547
+ validIds = new Set(valid.map((r) => r.id));
3548
+ for (const r of valid) hydratedById.set(r.id, { summary: r.summary ?? "", eventDate: r.event_date ?? r.created_at ?? null });
3549
+ } catch (err) {
3550
+ logReconcileDegradeOnce(err, memoryId);
3551
+ return;
3552
+ }
3553
+ const surviving = candidates.filter((c) => validIds.has(c.id));
3554
+ if (surviving.length === 0) {
3555
+ await add("no_valid_candidate", null, "none");
3556
+ return;
3557
+ }
3558
+ const maxCosine = surviving[0].similarity;
3559
+ const { reconcileLo: LO, reconcileHi: HI } = config.memory;
3560
+ if (maxCosine < LO) {
3561
+ await add("below_lo", maxCosine, "lo");
3562
+ return;
3563
+ }
3564
+ const band = maxCosine >= HI ? "hi" : "mid";
3565
+ const top = surviving[0];
3566
+ if (isRouterConfigured() && routerBudgetAvailable(ownerWalletForRow)) {
3567
+ noteRouterCall(ownerWalletForRow);
3568
+ const routerCandidates = surviving.map((c) => ({
3569
+ id: c.id,
3570
+ summary: hydratedById.get(c.id)?.summary ?? "",
3571
+ eventDate: hydratedById.get(c.id)?.eventDate ?? null
3572
+ }));
3573
+ const decision = await routeReconcile({ summary: newSummary, eventDate: newEventDate }, routerCandidates);
3574
+ await insertShadowRow({
3575
+ memory_id: memoryId,
3576
+ owner_wallet: ownerWalletForRow,
3577
+ mode: "shadow",
3578
+ proposed_op: decision.op,
3579
+ target_memory_id: decision.op === "add" ? null : decision.targetId,
3580
+ max_cosine: maxCosine,
3581
+ band,
3582
+ router_used: true,
3583
+ router_model: decision.model,
3584
+ fact_key: null,
3585
+ reason: decision.reason || "router",
3586
+ gate_version: gv
3587
+ });
3588
+ return;
3589
+ }
3590
+ await insertShadowRow({
3591
+ memory_id: memoryId,
3592
+ owner_wallet: ownerWalletForRow,
3593
+ mode: "shadow",
3594
+ proposed_op: "needs_router",
3595
+ target_memory_id: top.id,
3596
+ max_cosine: maxCosine,
3597
+ band,
3598
+ router_used: false,
3599
+ fact_key: null,
3600
+ reason: "similar_prior_fact",
3601
+ gate_version: gv
3602
+ });
3603
+ }
3604
+ var log10, SCOPE_BOT_OWN, K_CANDIDATES, reconcileDegradeLogged;
3605
+ var init_reconcile_shadow = __esm({
3606
+ "packages/brain/src/memory/reconcile-shadow.ts"() {
3607
+ "use strict";
3608
+ init_database();
3609
+ init_config();
3610
+ init_logger();
3611
+ init_reconcile_router();
3612
+ log10 = createChildLogger("reconcile-shadow");
3613
+ SCOPE_BOT_OWN = "__BOT_OWN__";
3614
+ K_CANDIDATES = 6;
3615
+ reconcileDegradeLogged = false;
3616
+ }
3617
+ });
3618
+
3619
+ // packages/shared/src/core/owner-key-constants.ts
3620
+ var HKDF_SALT, HKDF_INFO;
3621
+ var init_owner_key_constants = __esm({
3622
+ "packages/shared/src/core/owner-key-constants.ts"() {
3623
+ "use strict";
3624
+ HKDF_SALT = "clude-cortex-v1";
3625
+ HKDF_INFO = "memory-encryption-x25519-v2";
3626
+ }
3627
+ });
3628
+
3629
+ // packages/shared/src/core/memory-envelope.ts
3630
+ function generateDek() {
3631
+ return import_tweetnacl2.default.randomBytes(import_tweetnacl2.default.secretbox.keyLength);
3632
+ }
3633
+ function encryptField(plaintext, dek) {
3634
+ const nonce = import_tweetnacl2.default.randomBytes(NONCE_LENGTH);
3635
+ const message = new TextEncoder().encode(plaintext);
3636
+ const ct = import_tweetnacl2.default.secretbox(message, nonce, dek);
3637
+ const combined = new Uint8Array(NONCE_LENGTH + ct.length);
3638
+ combined.set(nonce, 0);
3639
+ combined.set(ct, NONCE_LENGTH);
3640
+ return Buffer.from(combined).toString("base64");
3641
+ }
3642
+ function decryptField(encrypted, dek) {
3643
+ try {
3644
+ const combined = Buffer.from(encrypted, "base64");
3645
+ if (combined.length < NONCE_LENGTH + 1) return null;
3646
+ const nonce = combined.subarray(0, NONCE_LENGTH);
3647
+ const ct = combined.subarray(NONCE_LENGTH);
3648
+ const pt = import_tweetnacl2.default.secretbox.open(ct, nonce, dek);
3649
+ if (!pt) return null;
3650
+ return new TextDecoder().decode(pt);
3651
+ } catch {
3652
+ return null;
3653
+ }
3654
+ }
3655
+ function wrapDek(dek, recipientPubkey) {
3656
+ const ephemeral = import_tweetnacl2.default.box.keyPair();
3657
+ const nonce = import_tweetnacl2.default.randomBytes(BOX_NONCE_LENGTH);
3658
+ const boxed = import_tweetnacl2.default.box(dek, nonce, recipientPubkey, ephemeral.secretKey);
3659
+ const combined = new Uint8Array(BOX_NONCE_LENGTH + boxed.length);
3660
+ combined.set(nonce, 0);
3661
+ combined.set(boxed, BOX_NONCE_LENGTH);
3662
+ return {
3663
+ wrapped: Buffer.from(combined).toString("base64"),
3664
+ wrapPubkey: Buffer.from(ephemeral.publicKey).toString("base64")
3665
+ };
3666
+ }
3667
+ function unwrapDek(wrapped, wrapPubkey, recipientSecretKey) {
3668
+ try {
3669
+ const combined = Buffer.from(wrapped, "base64");
3670
+ if (combined.length < BOX_NONCE_LENGTH + 1) return null;
3671
+ const nonce = combined.subarray(0, BOX_NONCE_LENGTH);
3672
+ const boxed = combined.subarray(BOX_NONCE_LENGTH);
3673
+ const ephemeralPub = Buffer.from(wrapPubkey, "base64");
3674
+ if (ephemeralPub.length !== import_tweetnacl2.default.box.publicKeyLength) return null;
3675
+ const out = import_tweetnacl2.default.box.open(boxed, nonce, ephemeralPub, recipientSecretKey);
3676
+ return out ?? null;
3677
+ } catch {
3678
+ return null;
3679
+ }
3680
+ }
3681
+ async function deriveOwnerEncryptionKeypair(signature) {
3682
+ const seed = await hkdfAsync("sha256", signature, HKDF_SALT, HKDF_INFO, 32);
3683
+ return import_tweetnacl2.default.box.keyPair.fromSecretKey(new Uint8Array(seed));
3684
+ }
3685
+ var import_tweetnacl2, import_crypto, import_util, hkdfAsync, NONCE_LENGTH, BOX_NONCE_LENGTH;
3686
+ var init_memory_envelope = __esm({
3687
+ "packages/shared/src/core/memory-envelope.ts"() {
3688
+ "use strict";
3689
+ import_tweetnacl2 = __toESM(require("tweetnacl"));
3690
+ import_crypto = require("crypto");
3691
+ import_util = require("util");
3692
+ init_owner_key_constants();
3693
+ hkdfAsync = (0, import_util.promisify)(import_crypto.hkdf);
3694
+ NONCE_LENGTH = import_tweetnacl2.default.secretbox.nonceLength;
3695
+ BOX_NONCE_LENGTH = import_tweetnacl2.default.box.nonceLength;
3696
+ }
3697
+ });
3698
+
3699
+ // packages/shared/src/core/encryption-keys.ts
3700
+ function loadProviderKeypair() {
3701
+ if (providerKeypair) return providerKeypair;
3702
+ const b64 = process.env.PMP_PROVIDER_ENC_SECRET;
3703
+ if (!b64) throw new Error("PMP_PROVIDER_ENC_SECRET is not set");
3704
+ const secret = Buffer.from(b64, "base64");
3705
+ if (secret.length !== import_tweetnacl3.default.box.secretKeyLength) {
3706
+ throw new Error(`PMP_PROVIDER_ENC_SECRET must decode to 32 bytes (got ${secret.length})`);
3707
+ }
3708
+ providerKeypair = import_tweetnacl3.default.box.keyPair.fromSecretKey(new Uint8Array(secret));
3709
+ log11.info("Provider encryption keypair loaded");
3710
+ return providerKeypair;
3711
+ }
3712
+ async function getOwnerEncryptionKey(ownerWallet) {
3713
+ const { data, error } = await getDb().from("encryption_keys").select("x25519_pubkey, verifier_ct").eq("owner_wallet", ownerWallet).maybeSingle();
3714
+ if (error) throw new Error(`getOwnerEncryptionKey failed: ${error.message}`);
3715
+ if (!data) return null;
3716
+ return { x25519_pubkey: data.x25519_pubkey, verifier_ct: data.verifier_ct };
3717
+ }
3718
+ var import_tweetnacl3, log11, providerKeypair;
3719
+ var init_encryption_keys = __esm({
3720
+ "packages/shared/src/core/encryption-keys.ts"() {
3721
+ "use strict";
3722
+ import_tweetnacl3 = __toESM(require("tweetnacl"));
3723
+ init_database();
3724
+ init_logger();
3725
+ log11 = createChildLogger("encryption-keys");
3726
+ providerKeypair = null;
3727
+ }
3728
+ });
3729
+
3730
+ // packages/brain/src/memory/memory-encryption.ts
3731
+ function isMemoryEncryptionEnabled() {
3732
+ if (process.env.PMP_ENCRYPTION_ENABLED !== "true") return false;
3733
+ try {
3734
+ loadProviderKeypair();
3735
+ return true;
3736
+ } catch {
3737
+ return false;
3738
+ }
3739
+ }
3740
+ async function getBotOwnerKeypair() {
3741
+ if (botKeypair) return botKeypair;
3742
+ const wallet = getBotWallet();
3743
+ if (!wallet) return null;
3744
+ const sig = import_tweetnacl4.default.sign.detached(new TextEncoder().encode(DERIVE_MESSAGE), wallet.secretKey);
3745
+ botKeypair = await deriveOwnerEncryptionKeypair(sig);
3746
+ return botKeypair;
3747
+ }
3748
+ async function resolveOwnerPublicKey(ownerWallet) {
3749
+ const botWallet2 = getBotWallet();
3750
+ const botAddr = botWallet2?.publicKey.toBase58();
3751
+ if (!ownerWallet || botAddr && ownerWallet === botAddr) {
3752
+ const kp = await getBotOwnerKeypair();
3753
+ return kp ? Buffer.from(kp.publicKey).toString("base64") : null;
3754
+ }
3755
+ const row = await getOwnerEncryptionKey(ownerWallet);
3756
+ return row?.x25519_pubkey ?? null;
3757
+ }
3758
+ async function encryptForStorage(plaintext, ownerWallet) {
3759
+ if (!isMemoryEncryptionEnabled()) return null;
3760
+ const ownerPubkey = await resolveOwnerPublicKey(ownerWallet);
3761
+ if (!ownerPubkey) {
3762
+ log12.warn("No owner encryption key resolvable \u2014 storing plaintext (encrypted:false)");
3763
+ return null;
3764
+ }
3765
+ const dek = generateDek();
3766
+ const ciphertext = encryptField(plaintext, dek);
3767
+ const ownerWrap = wrapDek(dek, Buffer.from(ownerPubkey, "base64"));
3768
+ const provWrap = wrapDek(dek, loadProviderKeypair().publicKey);
3769
+ return {
3770
+ ciphertext,
3771
+ ownerPubkey,
3772
+ wraps: [
3773
+ { recipient: "owner", wrapped_dek: ownerWrap.wrapped, wrap_pubkey: ownerWrap.wrapPubkey },
3774
+ { recipient: "provider", wrapped_dek: provWrap.wrapped, wrap_pubkey: provWrap.wrapPubkey }
3775
+ ]
3776
+ };
3777
+ }
3778
+ function delegationStateForWrite(hasEnvelope) {
3779
+ return hasEnvelope ? true : null;
3780
+ }
3781
+ var import_tweetnacl4, log12, DERIVE_MESSAGE, botKeypair;
3782
+ var init_memory_encryption = __esm({
3783
+ "packages/brain/src/memory/memory-encryption.ts"() {
3784
+ "use strict";
3785
+ import_tweetnacl4 = __toESM(require("tweetnacl"));
3786
+ init_memory_envelope();
3787
+ init_encryption_keys();
3788
+ init_solana_client();
3789
+ init_logger();
3790
+ log12 = createChildLogger("memory-encryption");
3791
+ DERIVE_MESSAGE = "clude-memory-encryption-v1";
3792
+ botKeypair = null;
3793
+ }
3794
+ });
3795
+
2536
3796
  // packages/shared/src/core/encryption.ts
2537
3797
  function isEncryptionEnabled() {
2538
3798
  return encryptionKey !== null;
@@ -2544,12 +3804,12 @@ function encryptContent(plaintext) {
2544
3804
  if (!encryptionKey) {
2545
3805
  throw new Error("Encryption not configured. Call configureEncryption() first.");
2546
3806
  }
2547
- const nonce = import_tweetnacl2.default.randomBytes(NONCE_LENGTH);
3807
+ const nonce = import_tweetnacl5.default.randomBytes(NONCE_LENGTH2);
2548
3808
  const messageBytes = new TextEncoder().encode(plaintext);
2549
- const ciphertext = import_tweetnacl2.default.secretbox(messageBytes, nonce, encryptionKey);
2550
- const combined = new Uint8Array(NONCE_LENGTH + ciphertext.length);
3809
+ const ciphertext = import_tweetnacl5.default.secretbox(messageBytes, nonce, encryptionKey);
3810
+ const combined = new Uint8Array(NONCE_LENGTH2 + ciphertext.length);
2551
3811
  combined.set(nonce, 0);
2552
- combined.set(ciphertext, NONCE_LENGTH);
3812
+ combined.set(ciphertext, NONCE_LENGTH2);
2553
3813
  return Buffer.from(combined).toString("base64");
2554
3814
  }
2555
3815
  function decryptContent(encrypted) {
@@ -2558,12 +3818,12 @@ function decryptContent(encrypted) {
2558
3818
  }
2559
3819
  try {
2560
3820
  const combined = Buffer.from(encrypted, "base64");
2561
- if (combined.length < NONCE_LENGTH + 1) {
3821
+ if (combined.length < NONCE_LENGTH2 + 1) {
2562
3822
  return null;
2563
3823
  }
2564
- const nonce = combined.subarray(0, NONCE_LENGTH);
2565
- const ciphertext = combined.subarray(NONCE_LENGTH);
2566
- const plaintext = import_tweetnacl2.default.secretbox.open(ciphertext, nonce, encryptionKey);
3824
+ const nonce = combined.subarray(0, NONCE_LENGTH2);
3825
+ const ciphertext = combined.subarray(NONCE_LENGTH2);
3826
+ const plaintext = import_tweetnacl5.default.secretbox.open(ciphertext, nonce, encryptionKey);
2567
3827
  if (!plaintext) {
2568
3828
  return null;
2569
3829
  }
@@ -2577,34 +3837,92 @@ function decryptMemoryBatch(memories) {
2577
3837
  for (const mem of memories) {
2578
3838
  if (!mem.encrypted) continue;
2579
3839
  if (mem.encryption_pubkey && mem.encryption_pubkey !== encryptionPubkey) {
2580
- log8.debug({ memPubkey: mem.encryption_pubkey?.slice(0, 12) }, "Skipping memory encrypted by different key");
3840
+ log13.debug({ memPubkey: mem.encryption_pubkey?.slice(0, 12) }, "Skipping memory encrypted by different key");
2581
3841
  continue;
2582
3842
  }
2583
3843
  const decrypted = decryptContent(mem.content);
2584
3844
  if (decrypted !== null) {
2585
3845
  mem.content = decrypted;
2586
3846
  } else {
2587
- log8.warn("Failed to decrypt memory content \u2014 wrong key or corrupted data");
3847
+ log13.warn("Failed to decrypt memory content \u2014 wrong key or corrupted data");
2588
3848
  }
2589
3849
  }
2590
3850
  return memories;
2591
3851
  }
2592
- var import_tweetnacl2, import_crypto, import_util, log8, hkdfAsync, NONCE_LENGTH, encryptionKey, encryptionPubkey;
3852
+ var import_tweetnacl5, import_crypto2, import_util2, log13, hkdfAsync2, NONCE_LENGTH2, encryptionKey, encryptionPubkey;
2593
3853
  var init_encryption = __esm({
2594
3854
  "packages/shared/src/core/encryption.ts"() {
2595
3855
  "use strict";
2596
- import_tweetnacl2 = __toESM(require("tweetnacl"));
2597
- import_crypto = require("crypto");
2598
- import_util = require("util");
3856
+ import_tweetnacl5 = __toESM(require("tweetnacl"));
3857
+ import_crypto2 = require("crypto");
3858
+ import_util2 = require("util");
2599
3859
  init_logger();
2600
- log8 = createChildLogger("encryption");
2601
- hkdfAsync = (0, import_util.promisify)(import_crypto.hkdf);
2602
- NONCE_LENGTH = import_tweetnacl2.default.secretbox.nonceLength;
3860
+ log13 = createChildLogger("encryption");
3861
+ hkdfAsync2 = (0, import_util2.promisify)(import_crypto2.hkdf);
3862
+ NONCE_LENGTH2 = import_tweetnacl5.default.secretbox.nonceLength;
2603
3863
  encryptionKey = null;
2604
3864
  encryptionPubkey = null;
2605
3865
  }
2606
3866
  });
2607
3867
 
3868
+ // packages/brain/src/memory/memory-decryption.ts
3869
+ async function fetchProviderWraps(ids) {
3870
+ const map = /* @__PURE__ */ new Map();
3871
+ if (ids.length === 0) return map;
3872
+ const { data, error } = await getDb().from("memory_dek_wraps").select("memory_id, wrapped_dek, wrap_pubkey").eq("recipient", "provider").in("memory_id", ids);
3873
+ if (error || !data) return map;
3874
+ for (const r of data) {
3875
+ map.set(r.memory_id, { wrapped_dek: r.wrapped_dek, wrap_pubkey: r.wrap_pubkey });
3876
+ }
3877
+ return map;
3878
+ }
3879
+ async function decryptMemories(memories) {
3880
+ if (!memories || memories.length === 0) return memories;
3881
+ const encryptedRows = memories.filter((m) => m.encrypted === true);
3882
+ if (encryptedRows.length === 0) return memories;
3883
+ let providerSecret = null;
3884
+ try {
3885
+ providerSecret = loadProviderKeypair().secretKey;
3886
+ } catch {
3887
+ providerSecret = null;
3888
+ }
3889
+ const wraps = providerSecret ? await fetchProviderWraps(encryptedRows.map((m) => m.id)) : /* @__PURE__ */ new Map();
3890
+ const legacyRows = [];
3891
+ for (const mem of encryptedRows) {
3892
+ const wrap = wraps.get(mem.id);
3893
+ if (providerSecret && wrap) {
3894
+ const dek = unwrapDek(wrap.wrapped_dek, wrap.wrap_pubkey, providerSecret);
3895
+ const plain = dek ? decryptField(mem.content, dek) : null;
3896
+ if (plain !== null) {
3897
+ mem.content = plain;
3898
+ continue;
3899
+ }
3900
+ log14.debug({ id: mem.id }, "Envelope decrypt failed \u2014 leaving ciphertext");
3901
+ continue;
3902
+ }
3903
+ legacyRows.push(mem);
3904
+ }
3905
+ if (legacyRows.length > 0) decryptMemoryBatch(legacyRows);
3906
+ return memories;
3907
+ }
3908
+ async function decryptOneContent(row) {
3909
+ if (row.encrypted !== true) return row.content;
3910
+ const [out] = await decryptMemories([{ ...row }]);
3911
+ return out.content !== row.content ? out.content : null;
3912
+ }
3913
+ var log14;
3914
+ var init_memory_decryption = __esm({
3915
+ "packages/brain/src/memory/memory-decryption.ts"() {
3916
+ "use strict";
3917
+ init_database();
3918
+ init_memory_envelope();
3919
+ init_encryption_keys();
3920
+ init_encryption();
3921
+ init_logger();
3922
+ log14 = createChildLogger("memory-decryption");
3923
+ }
3924
+ });
3925
+
2608
3926
  // packages/brain/src/events/event-bus.ts
2609
3927
  var import_events, TypedEventBus, eventBus;
2610
3928
  var init_event_bus = __esm({
@@ -2653,16 +3971,16 @@ async function findOrCreateEntity(name, entityType, opts) {
2653
3971
  mention_count: 1
2654
3972
  }).select().single();
2655
3973
  if (error) {
2656
- log9.error({ error: error.message, name }, "Failed to create entity");
3974
+ log15.error({ error: error.message, name }, "Failed to create entity");
2657
3975
  return null;
2658
3976
  }
2659
- log9.debug({ id: newEntity.id, name, type: entityType }, "Entity created");
3977
+ log15.debug({ id: newEntity.id, name, type: entityType }, "Entity created");
2660
3978
  embedEntity(newEntity.id, name, opts?.description).catch(
2661
- (err) => log9.debug({ err }, "Entity embedding failed")
3979
+ (err) => log15.debug({ err }, "Entity embedding failed")
2662
3980
  );
2663
3981
  return newEntity;
2664
3982
  } catch (err) {
2665
- log9.error({ err, name }, "Entity findOrCreate failed");
3983
+ log15.error({ err, name }, "Entity findOrCreate failed");
2666
3984
  return null;
2667
3985
  }
2668
3986
  }
@@ -2684,7 +4002,7 @@ async function createEntityMention(entityId, memoryId, context, salience = 0.5)
2684
4002
  salience: Math.max(0, Math.min(1, salience))
2685
4003
  }, { onConflict: "entity_id,memory_id" });
2686
4004
  if (error) {
2687
- log9.debug({ error: error.message, entityId, memoryId }, "Entity mention failed");
4005
+ log15.debug({ error: error.message, entityId, memoryId }, "Entity mention failed");
2688
4006
  }
2689
4007
  }
2690
4008
  async function getMemoriesByEntity(entityId, opts) {
@@ -2696,12 +4014,12 @@ async function getMemoriesByEntity(entityId, opts) {
2696
4014
  `).eq("entity_id", entityId).order("salience", { ascending: false }).limit(opts?.limit || 20);
2697
4015
  const { data, error } = await query;
2698
4016
  if (error) {
2699
- log9.error({ error: error.message, entityId }, "Failed to get memories by entity");
4017
+ log15.error({ error: error.message, entityId }, "Failed to get memories by entity");
2700
4018
  return [];
2701
4019
  }
2702
4020
  const { getOwnerWallet: getOwnerWallet3 } = (init_memory(), __toCommonJS(memory_exports));
2703
4021
  const ownerWallet = getOwnerWallet3();
2704
- const memories = (data || []).map((d) => d.memories).filter((m) => m !== null).filter((m) => !opts?.memoryTypes || opts.memoryTypes.includes(m.memory_type)).filter((m) => !ownerWallet || m.owner_wallet === ownerWallet);
4022
+ const memories = (data || []).map((d) => d.memories).filter((m) => m !== null).filter((m) => m.provider_delegated !== false).filter((m) => !opts?.memoryTypes || opts.memoryTypes.includes(m.memory_type)).filter((m) => !ownerWallet || m.owner_wallet === ownerWallet);
2705
4023
  return memories;
2706
4024
  }
2707
4025
  async function getEntityCooccurrences(entityId, opts) {
@@ -2713,33 +4031,27 @@ async function getEntityCooccurrences(entityId, opts) {
2713
4031
  max_results: opts?.maxResults ?? 10
2714
4032
  });
2715
4033
  if (error || !data) {
2716
- log9.debug({ error: error?.message, entityId }, "Entity co-occurrence lookup failed");
4034
+ log15.debug({ error: error?.message, entityId }, "Entity co-occurrence lookup failed");
2717
4035
  return [];
2718
4036
  }
2719
4037
  return data;
2720
4038
  } catch (err) {
2721
- log9.debug({ err, entityId }, "Entity co-occurrence skipped (RPC unavailable)");
4039
+ log15.debug({ err, entityId }, "Entity co-occurrence skipped (RPC unavailable)");
2722
4040
  return [];
2723
4041
  }
2724
4042
  }
2725
4043
  async function createEntityRelation(sourceEntityId, targetEntityId, relationType, evidenceMemoryId, strength = 0.5) {
2726
4044
  if (sourceEntityId === targetEntityId) return;
2727
4045
  const db = getDb();
2728
- const { data: existing } = await db.from("entity_relations").select("id, strength, evidence_memory_ids").eq("source_entity_id", sourceEntityId).eq("target_entity_id", targetEntityId).eq("relation_type", relationType).single();
2729
- if (existing) {
2730
- const newEvidence = evidenceMemoryId && !existing.evidence_memory_ids.includes(evidenceMemoryId) ? [...existing.evidence_memory_ids, evidenceMemoryId] : existing.evidence_memory_ids;
2731
- await db.from("entity_relations").update({
2732
- strength: Math.min(1, existing.strength + 0.1),
2733
- evidence_memory_ids: newEvidence
2734
- }).eq("id", existing.id);
2735
- } else {
2736
- await db.from("entity_relations").insert({
2737
- source_entity_id: sourceEntityId,
2738
- target_entity_id: targetEntityId,
2739
- relation_type: relationType,
2740
- strength,
2741
- evidence_memory_ids: evidenceMemoryId ? [evidenceMemoryId] : []
2742
- });
4046
+ const { error } = await db.rpc("upsert_entity_relation", {
4047
+ p_src: sourceEntityId,
4048
+ p_tgt: targetEntityId,
4049
+ p_type: relationType,
4050
+ p_evidence: evidenceMemoryId ?? null,
4051
+ p_strength: strength
4052
+ });
4053
+ if (error) {
4054
+ log15.warn({ err: error.message, sourceEntityId, targetEntityId, relationType }, "upsert_entity_relation failed");
2743
4055
  }
2744
4056
  }
2745
4057
  function extractEntitiesFromText(text) {
@@ -2819,11 +4131,11 @@ async function extractAndLinkEntities(memoryId, content, summary, relatedUser) {
2819
4131
  // base co-occurrence strength
2820
4132
  );
2821
4133
  } catch (err) {
2822
- log9.debug({ err, source: entityIds[i].name, target: entityIds[j].name }, "Failed to create entity relation");
4134
+ log15.debug({ err, source: entityIds[i].name, target: entityIds[j].name }, "Failed to create entity relation");
2823
4135
  }
2824
4136
  }
2825
4137
  }
2826
- log9.debug({ memoryId, entityCount: extracted.length, relations: Math.max(0, entityIds.length * (entityIds.length - 1) / 2) }, "Entities extracted, linked, and related");
4138
+ log15.debug({ memoryId, entityCount: extracted.length, relations: Math.max(0, entityIds.length * (entityIds.length - 1) / 2) }, "Entities extracted, linked, and related");
2827
4139
  }
2828
4140
  async function findSimilarEntities(query, opts) {
2829
4141
  if (!isEmbeddingEnabled()) return [];
@@ -2837,7 +4149,7 @@ async function findSimilarEntities(query, opts) {
2837
4149
  filter_types: opts?.entityTypes || null
2838
4150
  });
2839
4151
  if (error) {
2840
- log9.debug({ error: error.message }, "Entity vector search failed");
4152
+ log15.debug({ error: error.message }, "Entity vector search failed");
2841
4153
  return [];
2842
4154
  }
2843
4155
  const ids = (data || []).map((d) => d.id);
@@ -2845,14 +4157,14 @@ async function findSimilarEntities(query, opts) {
2845
4157
  const { data: entities } = await db.from("entities").select("*").in("id", ids);
2846
4158
  return entities || [];
2847
4159
  }
2848
- var log9;
4160
+ var log15;
2849
4161
  var init_graph = __esm({
2850
4162
  "packages/brain/src/memory/graph.ts"() {
2851
4163
  "use strict";
2852
4164
  init_database();
2853
4165
  init_logger();
2854
4166
  init_embeddings();
2855
- log9 = createChildLogger("memory-graph");
4167
+ log15 = createChildLogger("memory-graph");
2856
4168
  }
2857
4169
  });
2858
4170
 
@@ -2872,8 +4184,11 @@ var init_owner_context = __esm({
2872
4184
  // packages/brain/src/memory/memory.ts
2873
4185
  var memory_exports = {};
2874
4186
  __export(memory_exports, {
2875
- SCOPE_BOT_OWN: () => SCOPE_BOT_OWN,
4187
+ SCOPE_BOT_OWN: () => SCOPE_BOT_OWN2,
4188
+ _resetOutboxDegradeLog: () => _resetOutboxDegradeLog,
2876
4189
  _setOwnerWallet: () => _setOwnerWallet,
4190
+ applyOwnerPostGuard: () => applyOwnerPostGuard,
4191
+ buildFragmentRpcArgs: () => buildFragmentRpcArgs,
2877
4192
  calculateImportance: () => calculateImportance,
2878
4193
  createMemoryLink: () => createMemoryLink,
2879
4194
  createMemoryLinksBatch: () => createMemoryLinksBatch,
@@ -2884,12 +4199,15 @@ __export(memory_exports, {
2884
4199
  formatMemoryContext: () => formatMemoryContext,
2885
4200
  generateHashId: () => generateHashId,
2886
4201
  getMemoryStats: () => getMemoryStats,
4202
+ getOwnerScope: () => getOwnerScope,
2887
4203
  getOwnerWallet: () => getOwnerWallet,
2888
4204
  getRecentMemories: () => getRecentMemories,
2889
4205
  getSelfModel: () => getSelfModel,
2890
4206
  hydrateMemories: () => hydrateMemories,
2891
4207
  inferConcepts: () => inferConcepts,
2892
4208
  invalidateInstalledPacksCache: () => invalidateInstalledPacksCache,
4209
+ isBenchMode: () => isBenchMode,
4210
+ isOwnerScopeFailClosed: () => isOwnerScopeFailClosed,
2893
4211
  isValidHashId: () => isValidHashId,
2894
4212
  listMemories: () => listMemories,
2895
4213
  markJepaQueried: () => markJepaQueried,
@@ -2897,11 +4215,15 @@ __export(memory_exports, {
2897
4215
  moodToValence: () => moodToValence,
2898
4216
  recallMemories: () => recallMemories,
2899
4217
  recallMemorySummaries: () => recallMemorySummaries,
4218
+ runEnrichPipeline: () => runEnrichPipeline,
2900
4219
  scopeToOwner: () => scopeToOwner,
4220
+ scoreImportanceOnWrite: () => scoreImportanceOnWrite,
2901
4221
  scoreImportanceWithLLM: () => scoreImportanceWithLLM,
2902
4222
  scoreMemory: () => scoreMemory,
4223
+ shouldTrackAccess: () => shouldTrackAccess,
2903
4224
  storeDreamLog: () => storeDreamLog,
2904
4225
  storeMemory: () => storeMemory,
4226
+ storeMemoryWithOutbox: () => storeMemoryWithOutbox,
2905
4227
  updateMemory: () => updateMemory
2906
4228
  });
2907
4229
  function getCachedEmbedding2(query) {
@@ -2930,18 +4252,51 @@ function getOwnerWallet() {
2930
4252
  if (contextWallet !== void 0) return contextWallet;
2931
4253
  return _ownerWallet;
2932
4254
  }
2933
- function scopeToOwner(query) {
4255
+ function isOwnerScopeFailClosed() {
4256
+ return process.env.OWNER_SCOPE_FAILCLOSED === "true";
4257
+ }
4258
+ function getOwnerScope() {
2934
4259
  const wallet = getOwnerWallet();
2935
- if (wallet === SCOPE_BOT_OWN) {
4260
+ if (wallet) return wallet;
4261
+ return isOwnerScopeFailClosed() ? SCOPE_BOT_OWN2 : null;
4262
+ }
4263
+ function scopeToOwner(query) {
4264
+ const scope = getOwnerScope();
4265
+ if (scope === SCOPE_BOT_OWN2) {
2936
4266
  return query.is("owner_wallet", null);
2937
4267
  }
2938
- if (wallet) {
2939
- return query.eq("owner_wallet", wallet);
4268
+ if (scope) {
4269
+ return query.eq("owner_wallet", scope);
2940
4270
  }
2941
4271
  return query;
2942
4272
  }
4273
+ function applyOwnerPostGuard(results, scope) {
4274
+ if (!scope) return { kept: results, stripped: 0 };
4275
+ const kept = scope === SCOPE_BOT_OWN2 ? results.filter((m) => m.owner_wallet == null) : results.filter((m) => m.owner_wallet === scope);
4276
+ return { kept, stripped: results.length - kept.length };
4277
+ }
4278
+ function isBenchMode() {
4279
+ return process.env.BENCH_MODE === "true";
4280
+ }
4281
+ function shouldTrackAccess(opts) {
4282
+ if (isBenchMode()) return false;
4283
+ return opts.trackAccess !== false;
4284
+ }
4285
+ function buildFragmentRpcArgs(opts) {
4286
+ const args = {
4287
+ query_embedding: opts.embeddingJson,
4288
+ match_threshold: opts.matchThreshold,
4289
+ match_count: opts.matchCount,
4290
+ filter_owner: getOwnerScope()
4291
+ };
4292
+ if (process.env.MEMORY_FRAGMENT_FILTERS === "true") {
4293
+ args.min_decay = opts.minDecay;
4294
+ args.filter_types = opts.memoryTypes && opts.memoryTypes.length > 0 ? opts.memoryTypes : null;
4295
+ }
4296
+ return args;
4297
+ }
2943
4298
  function generateHashId() {
2944
- return `${HASH_ID_PREFIX}-${(0, import_crypto2.randomBytes)(4).toString("hex")}`;
4299
+ return `${HASH_ID_PREFIX}-${(0, import_crypto3.randomBytes)(4).toString("hex")}`;
2945
4300
  }
2946
4301
  function isValidHashId(id) {
2947
4302
  return /^clude-[a-f0-9]{8}$/.test(id);
@@ -2976,6 +4331,23 @@ function inferConcepts(summary, source, tags) {
2976
4331
  concepts.push("identity_evolution");
2977
4332
  return [...new Set(concepts)];
2978
4333
  }
4334
+ function scoreImportanceOnWrite(opts) {
4335
+ const base = {
4336
+ self_model: 0.75,
4337
+ semantic: 0.7,
4338
+ procedural: 0.65,
4339
+ introspective: 0.6,
4340
+ episodic: 0.45
4341
+ };
4342
+ let score = base[opts.type] ?? 0.5;
4343
+ const len = (opts.content ?? "").trim().length;
4344
+ score += Math.min(len / 1e3, 0.15);
4345
+ if (opts.tags && opts.tags.length) score += 0.05;
4346
+ const concepts = opts.concepts ?? inferConcepts(opts.summary, opts.source, opts.tags || []);
4347
+ if (concepts.length) score += 0.05;
4348
+ if (INTERNAL_MEMORY_SOURCES.has(opts.source)) score -= 0.05;
4349
+ return Math.max(0, Math.min(1, score));
4350
+ }
2979
4351
  function normalizeSummary(summary) {
2980
4352
  return summary.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<id>").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*/g, "<ts>").toLowerCase().trim();
2981
4353
  }
@@ -3007,7 +4379,7 @@ async function getInstalledPackIdsCached(ownerWallet) {
3007
4379
  ids = Array.from(/* @__PURE__ */ new Set([DEFAULT_PACK_ID, ...fromDb]));
3008
4380
  }
3009
4381
  } catch (err) {
3010
- log10.debug({ err }, "Failed to fetch installed packs (using default only)");
4382
+ log16.debug({ err }, "Failed to fetch installed packs (using default only)");
3011
4383
  }
3012
4384
  }
3013
4385
  installedPacksCache.set(key, { ids, expiresAt: now + INSTALLED_PACKS_TTL_MS });
@@ -3044,7 +4416,7 @@ async function ensureTopicEmbeddings(installedPackIds) {
3044
4416
  if (emb) topicEmbeddingCache.set(s.topicId, emb);
3045
4417
  });
3046
4418
  } catch (err) {
3047
- log10.warn({ err }, "Failed to populate topic embeddings cache");
4419
+ log16.warn({ err }, "Failed to populate topic embeddings cache");
3048
4420
  } finally {
3049
4421
  topicEmbeddingPopulationLock = null;
3050
4422
  }
@@ -3055,12 +4427,18 @@ async function ensureTopicEmbeddings(installedPackIds) {
3055
4427
  );
3056
4428
  }
3057
4429
  async function storeMemory(opts) {
4430
+ const trimmedContent = (opts.content ?? "").trim();
4431
+ if (!trimmedContent) {
4432
+ log16.warn({ source: opts.source }, "Rejected empty-content memory");
4433
+ return null;
4434
+ }
3058
4435
  if (opts.source.startsWith("shiro_") && isDuplicateWrite(opts.source, opts.summary)) {
3059
- log10.debug({ source: opts.source, summary: opts.summary.slice(0, 60) }, "Skipping duplicate memory write");
4436
+ log16.debug({ source: opts.source, summary: opts.summary.slice(0, 60) }, "Skipping duplicate memory write");
3060
4437
  return null;
3061
4438
  }
3062
4439
  const db = getDb();
3063
4440
  const ownerWallet = getOwnerWallet();
4441
+ const importance = clamp(opts.importance ?? scoreImportanceOnWrite(opts), 0, 1);
3064
4442
  const concepts = opts.concepts || inferConcepts(opts.summary, opts.source, opts.tags || []);
3065
4443
  const installedPackIds = await getInstalledPackIdsCached(ownerWallet);
3066
4444
  const taggedTags = autoCategorizeTags({
@@ -3071,9 +4449,10 @@ async function storeMemory(opts) {
3071
4449
  });
3072
4450
  const hashId = generateHashId();
3073
4451
  try {
3074
- const plaintextContent = opts.content.slice(0, MEMORY_MAX_CONTENT_LENGTH);
3075
- const shouldEncrypt = isEncryptionEnabled();
3076
- const storedContent = shouldEncrypt ? encryptContent(plaintextContent) : plaintextContent;
4452
+ const plaintextContent = trimmedContent.slice(0, MEMORY_MAX_CONTENT_LENGTH);
4453
+ const envelope = await encryptForStorage(plaintextContent, ownerWallet || null);
4454
+ const legacyEncrypt = !envelope && isEncryptionEnabled();
4455
+ const storedContent = envelope ? envelope.ciphertext : legacyEncrypt ? encryptContent(plaintextContent) : plaintextContent;
3077
4456
  const { data, error } = await db.from("memories").insert({
3078
4457
  hash_id: hashId,
3079
4458
  memory_type: opts.type,
@@ -3082,7 +4461,7 @@ async function storeMemory(opts) {
3082
4461
  tags: taggedTags,
3083
4462
  concepts,
3084
4463
  emotional_valence: clamp(opts.emotionalValence ?? 0, -1, 1),
3085
- importance: clamp(opts.importance ?? 0.5, 0, 1),
4464
+ importance,
3086
4465
  source: opts.source,
3087
4466
  source_id: opts.sourceId || null,
3088
4467
  related_user: opts.relatedUser || null,
@@ -3090,43 +4469,72 @@ async function storeMemory(opts) {
3090
4469
  metadata: opts.metadata || {},
3091
4470
  evidence_ids: opts.evidenceIds || [],
3092
4471
  compacted: false,
3093
- encrypted: shouldEncrypt,
3094
- encryption_pubkey: shouldEncrypt ? getEncryptionPubkey() : null,
4472
+ encrypted: envelope !== null || legacyEncrypt,
4473
+ encryption_pubkey: envelope ? envelope.ownerPubkey : legacyEncrypt ? getEncryptionPubkey() : null,
4474
+ provider_delegated: delegationStateForWrite(envelope !== null),
4475
+ // true|null at write; false is revoke-only (§7)
3095
4476
  owner_wallet: ownerWallet || null
3096
4477
  }).select("id, hash_id, content, memory_type, owner_wallet, created_at, tags, source, related_user, related_wallet").single();
3097
4478
  if (error) {
3098
- log10.error({ error: error.message }, "Failed to store memory");
4479
+ log16.error({ error: error.message }, "Failed to store memory");
3099
4480
  return null;
3100
4481
  }
3101
- log10.debug({
4482
+ log16.debug({
3102
4483
  id: data.id,
3103
4484
  hashId: data.hash_id,
3104
4485
  type: opts.type,
3105
4486
  summary: opts.summary.slice(0, 60),
3106
- importance: opts.importance,
4487
+ importance,
3107
4488
  concepts
3108
4489
  }, "Memory stored");
4490
+ await setContentTokens(db, data.id, plaintextContent);
4491
+ if (envelope) {
4492
+ const { error: wrapErr } = await db.from("memory_dek_wraps").insert(
4493
+ envelope.wraps.map((w) => ({ memory_id: data.id, ...w }))
4494
+ );
4495
+ if (wrapErr) {
4496
+ log16.error({ id: data.id, error: wrapErr.message }, "DEK wrap write failed \u2014 reverting memory to plaintext");
4497
+ await db.from("memories").update({
4498
+ content: plaintextContent,
4499
+ encrypted: false,
4500
+ encryption_pubkey: null,
4501
+ provider_delegated: null
4502
+ }).eq("id", data.id);
4503
+ }
4504
+ }
3109
4505
  eventBus.emit("memory:stored", {
3110
- importance: clamp(opts.importance ?? 0.5, 0, 1),
4506
+ importance,
3111
4507
  memoryType: opts.type,
3112
4508
  source: opts.source
3113
4509
  });
3114
- commitMemoryToChain(data.id, opts, data).catch(
3115
- (err) => log10.warn({ err }, "On-chain memory commit failed")
4510
+ commitMemoryToChain(data.id, opts, data, plaintextContent, envelope !== null || legacyEncrypt).catch(
4511
+ (err) => log16.warn({ err }, "On-chain memory commit failed")
3116
4512
  );
3117
- embedMemory(data.id, opts).catch((err) => log10.warn({ err }, "Embedding generation failed"));
3118
- autoLinkMemory(data.id, opts).catch((err) => log10.warn({ err }, "Auto-linking failed"));
3119
- extractAndLinkEntitiesForMemory(data.id, opts).catch((err) => log10.debug({ err }, "Entity extraction failed"));
4513
+ let enqueued = false;
4514
+ if (config.memory.outboxEnabled) {
4515
+ enqueued = await enqueueEnrichJob(db, data.id);
4516
+ }
4517
+ if (!enqueued) {
4518
+ const embedP = embedMemory(data.id, opts);
4519
+ embedP.catch((err) => log16.warn({ err }, "Embedding generation failed"));
4520
+ autoLinkMemory(data.id, opts).catch((err) => log16.warn({ err }, "Auto-linking failed"));
4521
+ extractAndLinkEntitiesForMemory(data.id, opts).catch((err) => log16.debug({ err }, "Entity extraction failed"));
4522
+ if (config.memory.reconcileEnabled) {
4523
+ const reconcileScope = ownerWallet || SCOPE_BOT_OWN2;
4524
+ embedP.then(() => runReconcileShadow(data.id, reconcileScope)).catch(() => {
4525
+ });
4526
+ }
4527
+ }
3120
4528
  return data.id;
3121
4529
  } catch (err) {
3122
- log10.error({ err }, "Memory store failed");
4530
+ log16.error({ err }, "Memory store failed");
3123
4531
  return null;
3124
4532
  }
3125
4533
  }
3126
- async function commitMemoryToChain(memoryId, opts, row) {
4534
+ async function commitMemoryToChain(memoryId, opts, row, plaintextContent, encrypted) {
3127
4535
  if (TOKENISATION_SKIP_SOURCES.has(opts.source)) return;
3128
4536
  const canonicalHash = memoryContentHash({
3129
- content: row.content,
4537
+ content: plaintextContent,
3130
4538
  memory_type: row.memory_type,
3131
4539
  owner_wallet: row.owner_wallet,
3132
4540
  created_at: row.created_at,
@@ -3138,7 +4546,6 @@ async function commitMemoryToChain(memoryId, opts, row) {
3138
4546
  const contentHashBuf = Buffer.from(canonicalHash, "hex");
3139
4547
  let signature = null;
3140
4548
  if (isRegistryEnabled()) {
3141
- const encrypted = isEncryptionEnabled();
3142
4549
  signature = await registerMemoryOnChain(
3143
4550
  contentHashBuf,
3144
4551
  opts.type,
@@ -3167,18 +4574,101 @@ async function commitMemoryToChain(memoryId, opts, row) {
3167
4574
  tokenization_status: "minted",
3168
4575
  tokenized_at: (/* @__PURE__ */ new Date()).toISOString()
3169
4576
  }).eq("id", memoryId);
3170
- log10.debug({ memoryId, signature: signature.slice(0, 16) }, "Memory committed on-chain (PMP + legacy)");
4577
+ log16.debug({ memoryId, signature: signature.slice(0, 16) }, "Memory committed on-chain (PMP + legacy)");
4578
+ }
4579
+ function logOutboxDegradeOnce(err, memoryId) {
4580
+ if (outboxDegradeLogged) return;
4581
+ outboxDegradeLogged = true;
4582
+ const e = err;
4583
+ log16.warn(
4584
+ { code: e?.code, message: e?.message, memoryId },
4585
+ "Outbox enqueue degraded \u2014 falling back to fire-and-forget (migration 044 unapplied?)"
4586
+ );
4587
+ }
4588
+ async function enqueueEnrichJob(db, memoryId) {
4589
+ try {
4590
+ const { error } = await db.from("memory_write_jobs").insert({ memory_id: memoryId, job_type: "enrich" });
4591
+ if (error) {
4592
+ logOutboxDegradeOnce(error, memoryId);
4593
+ return false;
4594
+ }
4595
+ return true;
4596
+ } catch (err) {
4597
+ logOutboxDegradeOnce(err, memoryId);
4598
+ return false;
4599
+ }
4600
+ }
4601
+ function _resetOutboxDegradeLog() {
4602
+ outboxDegradeLogged = false;
4603
+ }
4604
+ async function runEnrichPipeline(memoryId, opts) {
4605
+ await embedMemory(memoryId, opts, { replaceFragments: true });
4606
+ await autoLinkMemory(memoryId, opts);
4607
+ await extractAndLinkEntitiesForMemory(memoryId, opts);
4608
+ if (config.memory.reconcileEnabled) {
4609
+ await runReconcileShadow(memoryId, getOwnerWallet() || SCOPE_BOT_OWN2);
4610
+ }
4611
+ }
4612
+ async function storeMemoryWithOutbox(opts) {
4613
+ const id = await storeMemory(opts);
4614
+ if (id === null) return null;
4615
+ const db = getDb();
4616
+ const { data: row } = await db.from("memories").select("hash_id").eq("id", id).maybeSingle();
4617
+ const { count } = await db.from("memory_write_jobs").select("id", { count: "exact", head: true }).eq("memory_id", id);
4618
+ return { id, hash_id: row?.hash_id ?? "", jobs_queued: count ?? 0 };
3171
4619
  }
3172
- async function embedMemory(memoryId, opts) {
4620
+ async function embedMemory(memoryId, opts, embedOpts = {}) {
3173
4621
  if (!isEmbeddingEnabled()) return;
3174
4622
  const db = getDb();
3175
- const embeddings = await generateEmbeddings([opts.summary]);
4623
+ const fragments = [];
4624
+ fragments.push({ type: "summary", text: opts.summary });
4625
+ const content = opts.content.slice(0, MEMORY_MAX_CONTENT_LENGTH);
4626
+ if (content.length > EMBEDDING_FRAGMENT_MAX_LENGTH) {
4627
+ const sentences = content.match(/[^.!?\n]+[.!?\n]+/g) || [content];
4628
+ let chunk = "";
4629
+ for (const sentence of sentences) {
4630
+ if (chunk.length + sentence.length > EMBEDDING_FRAGMENT_MAX_LENGTH && chunk.length > 0) {
4631
+ fragments.push({ type: "content_chunk", text: chunk.trim() });
4632
+ chunk = "";
4633
+ }
4634
+ chunk += sentence;
4635
+ }
4636
+ if (chunk.trim()) fragments.push({ type: "content_chunk", text: chunk.trim() });
4637
+ } else {
4638
+ fragments.push({ type: "content_chunk", text: content });
4639
+ }
4640
+ const allLabels = [...opts.tags || [], ...opts.concepts || inferConcepts(opts.summary, opts.source, opts.tags || [])];
4641
+ if (allLabels.length > 0) {
4642
+ fragments.push({ type: "tag_context", text: `Context: ${allLabels.join(", ")}. ${opts.summary}` });
4643
+ }
4644
+ const embeddings = await generateEmbeddings(fragments.map((f) => f.text));
3176
4645
  const summaryEmbedding = embeddings[0];
3177
4646
  if (!summaryEmbedding) {
3178
- log10.debug({ memoryId }, "No embedding generated; skipping semantic tagging");
4647
+ log16.debug({ memoryId }, "No embedding generated; skipping semantic tagging");
3179
4648
  return;
3180
4649
  }
3181
- await db.from("memories").update({ embedding: JSON.stringify(summaryEmbedding) }).eq("id", memoryId);
4650
+ const vertexEmbeddings = isVertexConfigured() ? await generateVertexEmbeddings(fragments.map((f) => f.text)) : [];
4651
+ const vertexSummary = vertexEmbeddings[0];
4652
+ await db.from("memories").update({
4653
+ embedding: JSON.stringify(summaryEmbedding),
4654
+ ...vertexSummary ? { embedding_vertex: JSON.stringify(vertexSummary) } : {}
4655
+ }).eq("id", memoryId);
4656
+ const fragmentRows = fragments.map((f, i) => ({
4657
+ memory_id: memoryId,
4658
+ fragment_type: f.type,
4659
+ content: f.text.slice(0, EMBEDDING_FRAGMENT_MAX_LENGTH),
4660
+ embedding: embeddings[i] ? JSON.stringify(embeddings[i]) : null,
4661
+ ...vertexEmbeddings[i] ? { embedding_vertex: JSON.stringify(vertexEmbeddings[i]) } : {}
4662
+ })).filter((r) => r.embedding !== null);
4663
+ if (fragmentRows.length > 0) {
4664
+ if (embedOpts.replaceFragments) {
4665
+ await db.from("memory_fragments").delete().eq("memory_id", memoryId);
4666
+ }
4667
+ const { error } = await db.from("memory_fragments").insert(fragmentRows);
4668
+ if (error) {
4669
+ log16.warn({ err: error.message, memoryId }, "Failed to store memory fragments (summary embedding intact)");
4670
+ }
4671
+ }
3182
4672
  try {
3183
4673
  const ownerWallet = getOwnerWallet();
3184
4674
  const installedPacks = await getInstalledPackIdsCached(ownerWallet);
@@ -3194,7 +4684,7 @@ async function embedMemory(memoryId, opts) {
3194
4684
  const merged = Array.from(/* @__PURE__ */ new Set([...existing, ...semantic]));
3195
4685
  if (merged.length !== existing.length) {
3196
4686
  await db.from("memories").update({ tags: merged }).eq("id", memoryId);
3197
- log10.debug({
4687
+ log16.debug({
3198
4688
  memoryId,
3199
4689
  added: semantic.filter((t) => !existing.includes(t))
3200
4690
  }, "Semantic pack tags appended");
@@ -3202,9 +4692,9 @@ async function embedMemory(memoryId, opts) {
3202
4692
  }
3203
4693
  }
3204
4694
  } catch (err) {
3205
- log10.debug({ err, memoryId }, "Semantic tagging skipped");
4695
+ log16.debug({ err, memoryId }, "Semantic tagging skipped");
3206
4696
  }
3207
- log10.debug({ memoryId }, "Memory embedded");
4697
+ log16.debug({ memoryId }, "Memory embedded");
3208
4698
  }
3209
4699
  async function expandQuery(query) {
3210
4700
  if (!isOpenRouterEnabled()) return [query];
@@ -3222,10 +4712,10 @@ async function expandQuery(query) {
3222
4712
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
3223
4713
  ]);
3224
4714
  const expansions = response.split("\n").map((l) => l.trim()).filter((l) => l.length > 5 && l.length < 200).slice(0, 3);
3225
- log10.debug({ original: query, expansions: expansions.length }, "Query expanded");
4715
+ log16.debug({ original: query, expansions: expansions.length }, "Query expanded");
3226
4716
  return [query, ...expansions];
3227
4717
  } catch (err) {
3228
- log10.debug({ err }, "Query expansion failed, using original");
4718
+ log16.debug({ err }, "Query expansion failed, using original");
3229
4719
  return [query];
3230
4720
  }
3231
4721
  }
@@ -3238,11 +4728,12 @@ async function recallMemories(opts) {
3238
4728
  let vectorScores = opts._vectorScores || /* @__PURE__ */ new Map();
3239
4729
  let primaryQueryEmbedding = null;
3240
4730
  const vectorSearchPromise = queries.length > 0 && isEmbeddingEnabled() && !opts._vectorScores ? (async () => {
4731
+ const space = activeEmbeddingSpace();
3241
4732
  const queryEmbeddings = await Promise.all(
3242
4733
  queries.map(async (q) => {
3243
4734
  const cached = getCachedEmbedding2(q);
3244
4735
  if (cached) return cached;
3245
- const emb = await generateQueryEmbedding(q);
4736
+ const emb = await generateQueryEmbeddingForSpace(space, q);
3246
4737
  if (emb) setCachedEmbedding2(q, emb);
3247
4738
  return emb;
3248
4739
  })
@@ -3250,39 +4741,69 @@ async function recallMemories(opts) {
3250
4741
  const validEmbeddings = queryEmbeddings.filter((e) => e !== null);
3251
4742
  if (validEmbeddings.length > 0) primaryQueryEmbedding = validEmbeddings[0];
3252
4743
  if (validEmbeddings.length === 0) {
3253
- log10.debug("All query embeddings returned null, using keyword-only retrieval");
4744
+ log16.debug("All query embeddings returned null, using keyword-only retrieval");
3254
4745
  return;
3255
4746
  }
3256
4747
  try {
3257
- const allSearches = validEmbeddings.map(
3258
- (emb) => Promise.resolve(db.rpc("match_memories", {
3259
- query_embedding: JSON.stringify(emb),
3260
- match_threshold: VECTOR_MATCH_THRESHOLD,
3261
- match_count: limit * (opts.skipExpansion ? 12 : 4),
3262
- filter_types: opts.memoryTypes || null,
3263
- filter_user: opts.relatedUser || null,
3264
- min_decay: minDecay,
3265
- filter_owner: getOwnerWallet() || null,
3266
- filter_tags: opts.tags && opts.tags.length > 0 ? opts.tags : null
3267
- })).then((r) => r.data || [])
3268
- );
4748
+ const allSearches = validEmbeddings.flatMap((emb) => {
4749
+ const searches = [
4750
+ Promise.resolve(db.rpc(vectorRpcName("match_memories"), {
4751
+ query_embedding: JSON.stringify(emb),
4752
+ match_threshold: VECTOR_MATCH_THRESHOLD,
4753
+ match_count: limit * (opts.skipExpansion ? 12 : 4),
4754
+ filter_types: opts.memoryTypes || null,
4755
+ filter_user: opts.relatedUser || null,
4756
+ min_decay: minDecay,
4757
+ filter_owner: getOwnerScope(),
4758
+ filter_tags: opts.tags && opts.tags.length > 0 ? opts.tags : null
4759
+ })).then((r) => r.data || [])
4760
+ ];
4761
+ if (!opts.skipExpansion) {
4762
+ searches.push(
4763
+ Promise.resolve(db.rpc(vectorRpcName("match_memory_fragments"), buildFragmentRpcArgs({
4764
+ embeddingJson: JSON.stringify(emb),
4765
+ matchThreshold: VECTOR_MATCH_THRESHOLD,
4766
+ matchCount: limit * 2,
4767
+ minDecay,
4768
+ memoryTypes: opts.memoryTypes
4769
+ }))).then((r) => r.data || [])
4770
+ );
4771
+ }
4772
+ return searches;
4773
+ });
3269
4774
  const results2 = await Promise.all(allSearches);
3270
- for (const batch of results2) {
3271
- for (const m of batch) {
3272
- const current = vectorScores.get(m.id) || 0;
3273
- vectorScores.set(m.id, Math.max(current, m.similarity));
4775
+ let fragmentHits = 0;
4776
+ if (opts.skipExpansion) {
4777
+ for (const batch of results2) {
4778
+ for (const m of batch) {
4779
+ const current = vectorScores.get(m.id) || 0;
4780
+ vectorScores.set(m.id, Math.max(current, m.similarity));
4781
+ }
4782
+ }
4783
+ } else {
4784
+ for (let i = 0; i < results2.length; i++) {
4785
+ const isFragmentBatch = i % 2 === 1;
4786
+ for (const r of results2[i]) {
4787
+ const id = isFragmentBatch ? r.memory_id : r.id;
4788
+ const sim = isFragmentBatch ? r.max_similarity : r.similarity;
4789
+ if (!id) continue;
4790
+ const current = vectorScores.get(id) || 0;
4791
+ vectorScores.set(id, Math.max(current, sim));
4792
+ if (isFragmentBatch) fragmentHits++;
4793
+ }
3274
4794
  }
3275
4795
  }
3276
- log10.debug({
4796
+ log16.debug({
3277
4797
  queryVariants: validEmbeddings.length,
3278
4798
  uniqueMemories: vectorScores.size,
4799
+ fragmentHits,
3279
4800
  fastMode: !!opts.skipExpansion
3280
4801
  }, "Vector search completed");
3281
4802
  } catch (err) {
3282
- log10.warn({ err }, "Vector search RPC failed, falling back to keyword retrieval");
4803
+ log16.warn({ err }, "Vector search RPC failed, falling back to keyword retrieval");
3283
4804
  }
3284
4805
  })() : Promise.resolve();
3285
- let importanceQuery = db.from("memories").select("*").gte("decay_factor", minDecay).not("source", "in", '("demo","demo-maas")').order("importance", { ascending: false }).order("created_at", { ascending: false }).limit(limit * 3);
4806
+ let importanceQuery = db.from("memories").select("*").gte("decay_factor", minDecay).not("source", "in", '("demo","demo-maas")').not("provider_delegated", "is", false).order("importance", { ascending: false }).order("created_at", { ascending: false }).limit(limit * 3);
3286
4807
  importanceQuery = scopeToOwner(importanceQuery);
3287
4808
  if (opts.memoryTypes && opts.memoryTypes.length > 0) {
3288
4809
  importanceQuery = importanceQuery.in("memory_type", opts.memoryTypes);
@@ -3305,7 +4826,12 @@ async function recallMemories(opts) {
3305
4826
  return await bm25SearchMemories(opts.query, {
3306
4827
  limit: limit * 2,
3307
4828
  minDecay,
3308
- filterOwner: _ownerWallet || void 0,
4829
+ // Use getOwnerWallet() (AsyncLocalStorage-aware), NOT the bare module-level
4830
+ // _ownerWallet. withOwnerWallet() sets the async context, not the module var,
4831
+ // so the bare var is null under hosted/wrapped calls and BM25 would search the
4832
+ // ENTIRE table unscoped — burying the owner's exact-match facts. This is why
4833
+ // BM25 silently never surfaced bench facts even after the RPC was fixed.
4834
+ filterOwner: getOwnerScope() || void 0,
3309
4835
  filterTypes: opts.memoryTypes || void 0,
3310
4836
  filterTags: opts.tags || void 0
3311
4837
  });
@@ -3359,7 +4885,7 @@ async function recallMemories(opts) {
3359
4885
  }
3360
4886
  }
3361
4887
  }
3362
- if (seedsAdded > 0) log10.debug({ seedsAdded, seedsTotal: knowledgeSeeds.length }, "Knowledge seeds added to candidates");
4888
+ if (seedsAdded > 0) log16.debug({ seedsAdded, seedsTotal: knowledgeSeeds.length }, "Knowledge seeds added to candidates");
3363
4889
  }
3364
4890
  }
3365
4891
  const bm25Scores = /* @__PURE__ */ new Map();
@@ -3380,18 +4906,18 @@ async function recallMemories(opts) {
3380
4906
  }
3381
4907
  }
3382
4908
  }
3383
- log10.debug({ bm25Hits: bm25Results.length, bm25New: bm25MissingIds.length }, "BM25 search added candidates");
4909
+ log16.debug({ bm25Hits: bm25Results.length, bm25New: bm25MissingIds.length }, "BM25 search added candidates");
3384
4910
  }
3385
4911
  if (error) {
3386
- log10.error({ error: error.message }, "Memory recall query failed");
4912
+ log16.error({ error: error.message }, "Memory recall query failed");
3387
4913
  return [];
3388
4914
  }
3389
- let candidates = decryptMemoryBatch(data || []);
4915
+ let candidates = await decryptMemories(data || []);
3390
4916
  if (vectorScores.size > 0) {
3391
4917
  const metadataIds = new Set(candidates.map((m) => m.id));
3392
4918
  const missingIds = [...vectorScores.keys()].filter((id) => !metadataIds.has(id));
3393
4919
  if (missingIds.length > 0) {
3394
- let vectorQuery = db.from("memories").select("*").in("id", missingIds);
4920
+ let vectorQuery = db.from("memories").select("*").in("id", missingIds).gte("decay_factor", minDecay).not("embedding", "is", null);
3395
4921
  vectorQuery = scopeToOwner(vectorQuery);
3396
4922
  if (opts.memoryTypes && opts.memoryTypes.length > 0) {
3397
4923
  vectorQuery = vectorQuery.in("memory_type", opts.memoryTypes);
@@ -3400,7 +4926,7 @@ async function recallMemories(opts) {
3400
4926
  vectorQuery = vectorQuery.overlaps("tags", opts.tags);
3401
4927
  }
3402
4928
  const { data: vectorOnly } = await vectorQuery;
3403
- if (vectorOnly) candidates = [...candidates, ...decryptMemoryBatch(vectorOnly)];
4929
+ if (vectorOnly) candidates = [...candidates, ...await decryptMemories(vectorOnly)];
3404
4930
  }
3405
4931
  }
3406
4932
  if (candidates.length === 0) return [];
@@ -3424,10 +4950,10 @@ async function recallMemories(opts) {
3424
4950
  if (entities.length > 0) {
3425
4951
  const resultIdSet = new Set(results.map((m) => m.id));
3426
4952
  for (const entity of entities) {
3427
- const entityMemories = await getMemoriesByEntity(entity.id, {
4953
+ const entityMemories = await decryptMemories(await getMemoriesByEntity(entity.id, {
3428
4954
  limit: Math.ceil(limit / 2),
3429
4955
  memoryTypes: opts.memoryTypes
3430
- });
4956
+ }));
3431
4957
  for (const mem of entityMemories) {
3432
4958
  addLinkPath(mem.id, "entity");
3433
4959
  if (!resultIdSet.has(mem.id)) {
@@ -3439,17 +4965,17 @@ async function recallMemories(opts) {
3439
4965
  }
3440
4966
  }
3441
4967
  }
3442
- log10.debug({ entities: entities.map((e) => e.name) }, "Entity-aware recall applied");
4968
+ log16.debug({ entities: entities.map((e) => e.name) }, "Entity-aware recall applied");
3443
4969
  let cooccurrenceAdded = 0;
3444
4970
  const cooccurrenceNames = [];
3445
4971
  for (const entity of entities) {
3446
4972
  const cooccurrences = await getEntityCooccurrences(entity.id, { minCooccurrence: 2, maxResults: 3 });
3447
4973
  for (const cooc of cooccurrences) {
3448
4974
  if (cooccurrenceAdded >= limit) break;
3449
- const coMems = await getMemoriesByEntity(cooc.related_entity_id, {
4975
+ const coMems = await decryptMemories(await getMemoriesByEntity(cooc.related_entity_id, {
3450
4976
  limit: 3,
3451
4977
  memoryTypes: opts.memoryTypes
3452
- });
4978
+ }));
3453
4979
  for (const mem of coMems) {
3454
4980
  if (cooccurrenceAdded >= limit) break;
3455
4981
  addLinkPath(mem.id, "entity");
@@ -3469,11 +4995,11 @@ async function recallMemories(opts) {
3469
4995
  }
3470
4996
  }
3471
4997
  if (cooccurrenceAdded > 0) {
3472
- log10.debug({ cooccurrenceAdded, cooccurrenceEntities: cooccurrenceNames.length }, "Entity co-occurrence recall applied");
4998
+ log16.debug({ cooccurrenceAdded, cooccurrenceEntities: cooccurrenceNames.length }, "Entity co-occurrence recall applied");
3473
4999
  }
3474
5000
  }
3475
5001
  } catch (err) {
3476
- log10.debug({ err }, "Entity-aware recall skipped");
5002
+ log16.debug({ err }, "Entity-aware recall skipped");
3477
5003
  }
3478
5004
  }
3479
5005
  if (results.length > 0) {
@@ -3483,17 +5009,17 @@ async function recallMemories(opts) {
3483
5009
  seed_ids: seedIds,
3484
5010
  min_strength: 0.2,
3485
5011
  max_results: limit,
3486
- filter_owner: getOwnerWallet() || null
5012
+ filter_owner: getOwnerScope()
3487
5013
  });
3488
5014
  if (linked && linked.length > 0) {
3489
5015
  const resultIdSet = new Set(seedIds);
3490
5016
  const graphCandidateIds = linked.filter((l) => !resultIdSet.has(l.memory_id)).map((l) => l.memory_id);
3491
5017
  if (graphCandidateIds.length > 0) {
3492
- let graphQuery = db.from("memories").select("*").in("id", graphCandidateIds);
5018
+ let graphQuery = db.from("memories").select("*").in("id", graphCandidateIds).not("provider_delegated", "is", false);
3493
5019
  graphQuery = scopeToOwner(graphQuery);
3494
5020
  const { data: graphMemories } = await graphQuery;
3495
5021
  if (graphMemories && graphMemories.length > 0) {
3496
- decryptMemoryBatch(graphMemories);
5022
+ await decryptMemories(graphMemories);
3497
5023
  const linkBoostMap = /* @__PURE__ */ new Map();
3498
5024
  for (const l of linked) {
3499
5025
  const bondWeight = BOND_TYPE_WEIGHTS[l.link_type] ?? 0.4;
@@ -3507,7 +5033,7 @@ async function recallMemories(opts) {
3507
5033
  _score: scoreMemory(mem, scoredOpts) + RETRIEVAL_WEIGHT_GRAPH * (linkBoostMap.get(mem.id) || 0)
3508
5034
  }));
3509
5035
  results = [...results, ...graphScored].sort((a, b) => b._score - a._score).slice(0, limit);
3510
- log10.debug({
5036
+ log16.debug({
3511
5037
  graphExpanded: graphMemories.length,
3512
5038
  linkedTotal: linked.length,
3513
5039
  bondTypes: [...new Set(linked.map((l) => l.link_type))]
@@ -3516,7 +5042,7 @@ async function recallMemories(opts) {
3516
5042
  }
3517
5043
  }
3518
5044
  } catch (err) {
3519
- log10.debug({ err }, "Graph expansion skipped (RPC unavailable)");
5045
+ log16.debug({ err }, "Graph expansion skipped (RPC unavailable)");
3520
5046
  }
3521
5047
  }
3522
5048
  if (results.length >= 3) {
@@ -3532,23 +5058,23 @@ async function recallMemories(opts) {
3532
5058
  ...results.slice(0, results.length - replaceCount),
3533
5059
  ...diverseCandidates.slice(0, replaceCount)
3534
5060
  ].sort((a, b) => b._score - a._score).slice(0, limit);
3535
- log10.debug({ injectedTypes: diverseCandidates.map((m) => m.memory_type) }, "Type diversity applied");
5061
+ log16.debug({ injectedTypes: diverseCandidates.map((m) => m.memory_type) }, "Type diversity applied");
3536
5062
  }
3537
5063
  }
3538
5064
  }
3539
- const finalOwner = getOwnerWallet();
3540
- if (finalOwner) {
3541
- const beforeCount = results.length;
3542
- results = results.filter((m) => m.owner_wallet === finalOwner);
3543
- if (results.length < beforeCount) {
3544
- log10.warn({ stripped: beforeCount - results.length, owner: finalOwner }, "Owner guard stripped foreign memories from recall results");
5065
+ const finalScope = getOwnerScope();
5066
+ {
5067
+ const { kept, stripped } = applyOwnerPostGuard(results, finalScope);
5068
+ if (stripped > 0) {
5069
+ log16.warn({ stripped, botOwn: finalScope === SCOPE_BOT_OWN2 }, "Owner guard stripped foreign memories from recall results");
3545
5070
  }
5071
+ results = kept;
3546
5072
  }
3547
- if (opts.trackAccess !== false) {
5073
+ if (shouldTrackAccess(opts)) {
3548
5074
  const ids = results.map((m) => m.id);
3549
5075
  const sources = results.map((m) => m.source || "");
3550
- updateMemoryAccess(ids, sources).catch((err) => log10.warn({ err }, "Memory access tracking failed"));
3551
- reinforceCoRetrievedLinks(ids).catch((err) => log10.debug({ err }, "Link reinforcement failed"));
5076
+ updateMemoryAccess(ids, sources).catch((err) => log16.warn({ err }, "Memory access tracking failed"));
5077
+ reinforceCoRetrievedLinks(ids).catch((err) => log16.debug({ err }, "Link reinforcement failed"));
3552
5078
  }
3553
5079
  for (const m of results) {
3554
5080
  const paths = linkPathMap.get(m.id);
@@ -3556,7 +5082,7 @@ async function recallMemories(opts) {
3556
5082
  m.link_path = [...paths];
3557
5083
  }
3558
5084
  }
3559
- log10.debug({
5085
+ log16.debug({
3560
5086
  recalled: results.length,
3561
5087
  topScore: results[0]?._score?.toFixed(3),
3562
5088
  query: opts.query?.slice(0, 40),
@@ -3566,7 +5092,7 @@ async function recallMemories(opts) {
3566
5092
  }, "Memories recalled");
3567
5093
  return results;
3568
5094
  } catch (err) {
3569
- log10.error({ err }, "Memory recall failed");
5095
+ log16.error({ err }, "Memory recall failed");
3570
5096
  return [];
3571
5097
  }
3572
5098
  }
@@ -3580,7 +5106,7 @@ function extractQueryTerms(query) {
3580
5106
  }
3581
5107
  function scoreMemory(mem, opts) {
3582
5108
  const now = Date.now();
3583
- const hoursSinceAccess = (now - new Date(mem.last_accessed).getTime()) / (1e3 * 60 * 60);
5109
+ const hoursSinceAccess = Math.max(0, (now - new Date(mem.last_accessed).getTime()) / (1e3 * 60 * 60));
3584
5110
  const recency = Math.pow(RECENCY_DECAY_BASE, hoursSinceAccess);
3585
5111
  let textScore = 0.5;
3586
5112
  if (opts.query) {
@@ -3616,7 +5142,7 @@ function scoreMemory(mem, opts) {
3616
5142
  }
3617
5143
  const bm25Rank = opts._bm25Scores?.get(mem.id) || 0;
3618
5144
  if (bm25Rank > 0) {
3619
- rawScore += 0.15 * Math.min(bm25Rank, 1);
5145
+ rawScore += RETRIEVAL_WEIGHT_BM25 * Math.min(bm25Rank, 1);
3620
5146
  }
3621
5147
  const typeBoost = KNOWLEDGE_TYPE_BOOST[mem.memory_type] || 0;
3622
5148
  rawScore += typeBoost;
@@ -3637,7 +5163,7 @@ async function recallMemorySummaries(opts) {
3637
5163
  const limit = opts.limit || 10;
3638
5164
  const minDecay = opts.minDecay ?? 0.1;
3639
5165
  try {
3640
- let query = db.from("memories").select("id, memory_type, summary, tags, concepts, importance, decay_factor, created_at, source").gte("decay_factor", minDecay).not("source", "in", '("demo","demo-maas")').order("importance", { ascending: false }).order("created_at", { ascending: false }).limit(limit);
5166
+ let query = db.from("memories").select("id, memory_type, summary, tags, concepts, importance, decay_factor, created_at, source").gte("decay_factor", minDecay).not("source", "in", '("demo","demo-maas")').not("provider_delegated", "is", false).order("importance", { ascending: false }).order("created_at", { ascending: false }).limit(limit);
3641
5167
  query = scopeToOwner(query);
3642
5168
  if (opts.memoryTypes && opts.memoryTypes.length > 0) {
3643
5169
  query = query.in("memory_type", opts.memoryTypes);
@@ -3656,12 +5182,12 @@ async function recallMemorySummaries(opts) {
3656
5182
  }
3657
5183
  const { data, error } = await query;
3658
5184
  if (error) {
3659
- log10.error({ error: error.message }, "Memory summary recall failed");
5185
+ log16.error({ error: error.message }, "Memory summary recall failed");
3660
5186
  return [];
3661
5187
  }
3662
5188
  return data || [];
3663
5189
  } catch (err) {
3664
- log10.error({ err }, "Memory summary recall failed");
5190
+ log16.error({ err }, "Memory summary recall failed");
3665
5191
  return [];
3666
5192
  }
3667
5193
  }
@@ -3673,28 +5199,23 @@ async function hydrateMemories(ids) {
3673
5199
  query = scopeToOwner(query);
3674
5200
  const { data, error } = await query;
3675
5201
  if (error) {
3676
- log10.error({ error: error.message }, "Memory hydration failed");
5202
+ log16.error({ error: error.message }, "Memory hydration failed");
3677
5203
  return [];
3678
5204
  }
3679
- return decryptMemoryBatch(data || []);
5205
+ return await decryptMemories(data || []);
3680
5206
  } catch (err) {
3681
- log10.error({ err }, "Memory hydration failed");
5207
+ log16.error({ err }, "Memory hydration failed");
3682
5208
  return [];
3683
5209
  }
3684
5210
  }
3685
- async function updateMemoryAccess(ids, sources = []) {
5211
+ async function updateMemoryAccess(ids, _sources = []) {
3686
5212
  if (ids.length === 0) return;
3687
5213
  const db = getDb();
3688
- const importanceBoosts = ids.map((_, i) => {
3689
- const source = sources[i] || "";
3690
- return INTERNAL_MEMORY_SOURCES.has(source) ? INTERNAL_IMPORTANCE_BOOST : 0.02;
3691
- });
3692
5214
  const { error } = await db.rpc("batch_boost_memory_access", {
3693
- memory_ids: ids,
3694
- importance_boosts: importanceBoosts
5215
+ memory_ids: ids
3695
5216
  });
3696
5217
  if (error) {
3697
- log10.warn({ error: error.message, ids }, "Batch memory access update failed");
5218
+ log16.warn({ error: error.message, ids }, "Batch memory access update failed");
3698
5219
  }
3699
5220
  }
3700
5221
  async function createMemoryLink(sourceId, targetId, linkType, strength = 0.5) {
@@ -3707,7 +5228,7 @@ async function createMemoryLink(sourceId, targetId, linkType, strength = 0.5) {
3707
5228
  strength: clamp(strength, 0, 1)
3708
5229
  }, { onConflict: "source_id,target_id,link_type" });
3709
5230
  if (error) {
3710
- log10.debug({ error: error.message, sourceId, targetId, linkType }, "Link creation failed");
5231
+ log16.debug({ error: error.message, sourceId, targetId, linkType }, "Link creation failed");
3711
5232
  }
3712
5233
  }
3713
5234
  async function createMemoryLinksBatch(links) {
@@ -3729,7 +5250,7 @@ async function createMemoryLinksBatch(links) {
3729
5250
  const db = getDb();
3730
5251
  const { error } = await db.from("memory_links").upsert(rows, { onConflict: "source_id,target_id,link_type" });
3731
5252
  if (error) {
3732
- log10.debug({ error: error.message, count: rows.length }, "Batch link creation failed");
5253
+ log16.debug({ error: error.message, count: rows.length }, "Batch link creation failed");
3733
5254
  }
3734
5255
  }
3735
5256
  async function autoLinkMemory(memoryId, opts) {
@@ -3748,7 +5269,7 @@ async function autoLinkMemory(memoryId, opts) {
3748
5269
  query_embedding: JSON.stringify(embedding),
3749
5270
  match_threshold: LINK_SIMILARITY_THRESHOLD,
3750
5271
  match_count: MAX_AUTO_LINKS * 2,
3751
- filter_owner: getOwnerWallet() || null
5272
+ filter_owner: getOwnerScope()
3752
5273
  });
3753
5274
  if (similar) {
3754
5275
  const similarIds = similar.map((s) => s.id).filter((id) => id !== memoryId);
@@ -3797,7 +5318,7 @@ async function autoLinkMemory(memoryId, opts) {
3797
5318
  }
3798
5319
  if (pendingLinks.length > 0) {
3799
5320
  await createMemoryLinksBatch(pendingLinks);
3800
- log10.debug({ memoryId, linksCreated: pendingLinks.length }, "Auto-linked memory");
5321
+ log16.debug({ memoryId, linksCreated: pendingLinks.length }, "Auto-linked memory");
3801
5322
  }
3802
5323
  }
3803
5324
  function classifyLinkType(newMem, candidate, newConcepts) {
@@ -3819,16 +5340,16 @@ async function reinforceCoRetrievedLinks(ids) {
3819
5340
  boost_amount: LINK_CO_RETRIEVAL_BOOST
3820
5341
  });
3821
5342
  if (error) {
3822
- log10.debug({ error: error.message }, "Link reinforcement RPC failed");
5343
+ log16.debug({ error: error.message }, "Link reinforcement RPC failed");
3823
5344
  } else if (data && data > 0) {
3824
- log10.debug({ boosted: data }, "Co-retrieval link reinforcement applied");
5345
+ log16.debug({ boosted: data }, "Co-retrieval link reinforcement applied");
3825
5346
  }
3826
5347
  }
3827
5348
  async function extractAndLinkEntitiesForMemory(memoryId, opts) {
3828
5349
  try {
3829
5350
  await extractAndLinkEntities(memoryId, opts.content, opts.summary, opts.relatedUser);
3830
5351
  } catch (err) {
3831
- log10.debug({ err, memoryId }, "Entity extraction failed");
5352
+ log16.debug({ err, memoryId }, "Entity extraction failed");
3832
5353
  }
3833
5354
  }
3834
5355
  async function decayMemories() {
@@ -3844,17 +5365,17 @@ async function decayMemories() {
3844
5365
  cutoff
3845
5366
  });
3846
5367
  if (error) {
3847
- log10.warn({ error: error.message, memType }, "Batch decay failed for type");
5368
+ log16.warn({ error: error.message, memType }, "Batch decay failed for type");
3848
5369
  continue;
3849
5370
  }
3850
5371
  totalDecayed += data || 0;
3851
5372
  }
3852
5373
  if (totalDecayed > 0) {
3853
- log10.info({ decayed: totalDecayed }, "Type-specific memory decay applied");
5374
+ log16.info({ decayed: totalDecayed }, "Type-specific memory decay applied");
3854
5375
  }
3855
5376
  return totalDecayed;
3856
5377
  } catch (err) {
3857
- log10.error({ err }, "Memory decay failed");
5378
+ log16.error({ err }, "Memory decay failed");
3858
5379
  return 0;
3859
5380
  }
3860
5381
  }
@@ -3864,7 +5385,7 @@ async function deleteMemory(id) {
3864
5385
  query = scopeToOwner(query);
3865
5386
  const { error } = await query;
3866
5387
  if (error) {
3867
- log10.error({ error: error.message, id }, "Failed to delete memory");
5388
+ log16.error({ error: error.message, id }, "Failed to delete memory");
3868
5389
  return false;
3869
5390
  }
3870
5391
  return true;
@@ -3882,7 +5403,7 @@ async function updateMemory(id, patches) {
3882
5403
  query = scopeToOwner(query);
3883
5404
  const { error } = await query;
3884
5405
  if (error) {
3885
- log10.error({ error: error.message, id }, "Failed to update memory");
5406
+ log16.error({ error: error.message, id }, "Failed to update memory");
3886
5407
  return false;
3887
5408
  }
3888
5409
  return true;
@@ -3903,10 +5424,10 @@ async function listMemories(opts) {
3903
5424
  if (opts.min_importance !== void 0) dataQ = dataQ.gte("importance", opts.min_importance);
3904
5425
  const { data, error } = await dataQ;
3905
5426
  if (error) {
3906
- log10.error({ error: error.message }, "Failed to list memories");
5427
+ log16.error({ error: error.message }, "Failed to list memories");
3907
5428
  return { memories: [], total: 0 };
3908
5429
  }
3909
- return { memories: decryptMemoryBatch(data || []), total: count ?? 0 };
5430
+ return { memories: await decryptMemories(data || []), total: count ?? 0 };
3910
5431
  }
3911
5432
  async function getMemoryStats() {
3912
5433
  const db = getDb();
@@ -3943,7 +5464,7 @@ async function getMemoryStats() {
3943
5464
  const MAX_PAGES = 20;
3944
5465
  let allMemories = [];
3945
5466
  for (let page = 0; page < MAX_PAGES; page++) {
3946
- let pageQuery = db.from("memories").select("importance, decay_factor, created_at, related_user, tags, concepts").order("id", { ascending: true }).range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
5467
+ let pageQuery = db.from("memories").select("importance, decay_factor, created_at, related_user, owner_wallet, tags, concepts").order("id", { ascending: true }).range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
3947
5468
  pageQuery = scopeToOwner(pageQuery);
3948
5469
  const { data: pageData } = await pageQuery;
3949
5470
  if (!pageData || pageData.length === 0) break;
@@ -3960,6 +5481,7 @@ async function getMemoryStats() {
3960
5481
  impSum += m.importance;
3961
5482
  decaySum += m.decay_factor;
3962
5483
  if (m.related_user) users.add(m.related_user);
5484
+ if (m.owner_wallet) users.add(m.owner_wallet);
3963
5485
  if (m.tags) {
3964
5486
  for (const tag of m.tags) {
3965
5487
  tagCounts[tag] = (tagCounts[tag] || 0) + 1;
@@ -3980,13 +5502,15 @@ async function getMemoryStats() {
3980
5502
  stats.oldestMemory = sorted[0] || null;
3981
5503
  stats.newestMemory = sorted[sorted.length - 1] || null;
3982
5504
  }
3983
- const { count, error: dreamError } = await db.from("dream_logs").select("id", { count: "exact", head: true });
5505
+ let dreamQuery = db.from("dream_logs").select("id", { count: "exact", head: true });
5506
+ dreamQuery = scopeToOwner(dreamQuery);
5507
+ const { count, error: dreamError } = await dreamQuery;
3984
5508
  if (dreamError) {
3985
- log10.warn({ error: dreamError.message }, "Failed to count dream logs");
5509
+ log16.warn({ error: dreamError.message }, "Failed to count dream logs");
3986
5510
  }
3987
5511
  stats.totalDreamSessions = count || 0;
3988
5512
  } catch (err) {
3989
- log10.error({ err }, "Failed to get memory stats");
5513
+ log16.error({ err }, "Failed to get memory stats");
3990
5514
  }
3991
5515
  return stats;
3992
5516
  }
@@ -4000,10 +5524,10 @@ async function getRecentMemories(hours, types, limit) {
4000
5524
  }
4001
5525
  const { data, error } = await query;
4002
5526
  if (error) {
4003
- log10.error({ error: error.message }, "Failed to get recent memories");
5527
+ log16.error({ error: error.message }, "Failed to get recent memories");
4004
5528
  return [];
4005
5529
  }
4006
- return decryptMemoryBatch(data || []);
5530
+ return await decryptMemories(data || []);
4007
5531
  }
4008
5532
  async function getSelfModel() {
4009
5533
  const db = getDb();
@@ -4011,10 +5535,10 @@ async function getSelfModel() {
4011
5535
  query = scopeToOwner(query);
4012
5536
  const { data, error } = await query;
4013
5537
  if (error) {
4014
- log10.error({ error: error.message }, "Failed to get self model");
5538
+ log16.error({ error: error.message }, "Failed to get self model");
4015
5539
  return [];
4016
5540
  }
4017
- return decryptMemoryBatch(data || []);
5541
+ return await decryptMemories(data || []);
4018
5542
  }
4019
5543
  async function storeDreamLog(sessionType, inputMemoryIds, output, newMemoryIds) {
4020
5544
  const db = getDb();
@@ -4022,54 +5546,48 @@ async function storeDreamLog(sessionType, inputMemoryIds, output, newMemoryIds)
4022
5546
  session_type: sessionType,
4023
5547
  input_memory_ids: inputMemoryIds,
4024
5548
  output: output.slice(0, MEMORY_MAX_CONTENT_LENGTH),
4025
- new_memories_created: newMemoryIds
5549
+ new_memories_created: newMemoryIds,
5550
+ // Stamp owner so getMemoryStats can scope the dream count (migration 021).
5551
+ owner_wallet: getOwnerWallet() === SCOPE_BOT_OWN2 ? null : getOwnerWallet()
4026
5552
  });
4027
5553
  if (error) {
4028
- log10.error({ error: error.message }, "Failed to store dream log");
5554
+ log16.error({ error: error.message }, "Failed to store dream log");
4029
5555
  }
4030
5556
  }
4031
5557
  function formatMemoryContext(memories) {
4032
5558
  if (memories.length === 0) return "";
4033
5559
  const lines = ["## Memory Recall"];
4034
- const episodic = memories.filter((m) => m.memory_type === "episodic");
4035
- const semantic = memories.filter((m) => m.memory_type === "semantic");
4036
- const procedural = memories.filter((m) => m.memory_type === "procedural");
4037
- const selfModel = memories.filter((m) => m.memory_type === "self_model");
4038
- const introspective = memories.filter((m) => m.memory_type === "introspective");
5560
+ const episodic = memories.filter((m) => m.memory_type === "episodic").sort(byMemoryDateAsc);
5561
+ const semantic = memories.filter((m) => m.memory_type === "semantic").sort(byMemoryDateAsc);
5562
+ const procedural = memories.filter((m) => m.memory_type === "procedural").sort(byMemoryDateAsc);
5563
+ const selfModel = memories.filter((m) => m.memory_type === "self_model").sort(byMemoryDateAsc);
5564
+ const introspective = memories.filter((m) => m.memory_type === "introspective").sort(byMemoryDateAsc);
4039
5565
  if (episodic.length > 0) {
4040
5566
  lines.push("### Past Interactions");
4041
- for (const m of episodic) {
4042
- lines.push(`- [${timeAgo(m.created_at)}] ${m.summary}`);
4043
- }
5567
+ for (const m of episodic) lines.push(renderGroundedLine(m));
4044
5568
  }
4045
5569
  if (semantic.length > 0) {
4046
5570
  lines.push("### Things You Know");
4047
- for (const m of semantic) {
4048
- lines.push(`- ${m.summary}`);
4049
- }
5571
+ for (const m of semantic) lines.push(renderGroundedLine(m));
4050
5572
  }
4051
5573
  if (procedural.length > 0) {
4052
5574
  lines.push("### Learned Strategies (from past outcomes)");
4053
5575
  for (const m of procedural) {
4054
5576
  const meta = m.metadata;
4055
5577
  const confidence = meta?.positiveRate != null ? ` [${Math.round(meta.positiveRate * 100)}% success rate, based on ${meta.basedOn || "?"} interactions]` : "";
4056
- lines.push(`- ${m.summary}${confidence}`);
5578
+ lines.push(renderGroundedLine(m, confidence));
4057
5579
  }
4058
5580
  }
4059
5581
  if (introspective.length > 0) {
4060
5582
  lines.push("### Your Own Reflections");
4061
- for (const m of introspective) {
4062
- lines.push(`- [${timeAgo(m.created_at)}] ${m.summary}`);
4063
- }
5583
+ for (const m of introspective) lines.push(renderGroundedLine(m));
4064
5584
  }
4065
5585
  if (selfModel.length > 0) {
4066
5586
  lines.push("### Self-Observations");
4067
- for (const m of selfModel) {
4068
- lines.push(`- ${m.summary}`);
4069
- }
5587
+ for (const m of selfModel) lines.push(renderGroundedLine(m));
4070
5588
  }
4071
5589
  lines.push("");
4072
- lines.push("You REMEMBER these interactions and facts. Reference them naturally if relevant.");
5590
+ lines.push(CONTEXT_GROUNDING_RULES);
4073
5591
  if (procedural.length > 0) {
4074
5592
  lines.push("");
4075
5593
  lines.push("IMPORTANT: You MUST follow the Learned Strategies above. They are behavioral rules you derived from analyzing your own past successes and failures. Apply them to this response.");
@@ -4094,10 +5612,10 @@ async function scoreImportanceWithLLM(description, fallbackOpts) {
4094
5612
  if (!isNaN(parsed) && parsed >= 1 && parsed <= 10) {
4095
5613
  return parsed / 10;
4096
5614
  }
4097
- log10.warn({ response }, "LLM importance score unparseable, using fallback");
5615
+ log16.warn({ response }, "LLM importance score unparseable, using fallback");
4098
5616
  return calculateImportance(fallbackOpts || {});
4099
5617
  } catch (err) {
4100
- log10.warn({ err }, "LLM importance scoring failed, using fallback");
5618
+ log16.warn({ err }, "LLM importance scoring failed, using fallback");
4101
5619
  return calculateImportance(fallbackOpts || {});
4102
5620
  }
4103
5621
  }
@@ -4121,7 +5639,8 @@ async function matchByEmbedding(opts) {
4121
5639
  query_embedding: JSON.stringify(opts.embedding),
4122
5640
  match_threshold: opts.threshold,
4123
5641
  match_count: opts.limit,
4124
- filter_owner: opts.ownerWallet ?? null
5642
+ // Explicit ownerWallet wins; otherwise the ambient scope (sentinel-aware, C0).
5643
+ filter_owner: opts.ownerWallet ?? getOwnerScope()
4125
5644
  });
4126
5645
  return (data ?? []).map((r) => ({
4127
5646
  id: r.id,
@@ -4144,32 +5663,39 @@ function moodToValence(mood) {
4144
5663
  return 0;
4145
5664
  }
4146
5665
  }
4147
- var import_crypto2, EMBED_CACHE_MAX, embeddingCache, _ownerWallet, SCOPE_BOT_OWN, HASH_ID_PREFIX, log10, DEDUP_TTL_MS, dedupCache, installedPacksCache, INSTALLED_PACKS_TTL_MS, topicEmbeddingCache, topicEmbeddingPopulationLock, TOKENISATION_SKIP_SOURCES, STOPWORDS;
5666
+ var import_crypto3, EMBED_CACHE_MAX, embeddingCache, _ownerWallet, SCOPE_BOT_OWN2, HASH_ID_PREFIX, log16, DEDUP_TTL_MS, dedupCache, installedPacksCache, INSTALLED_PACKS_TTL_MS, topicEmbeddingCache, topicEmbeddingPopulationLock, TOKENISATION_SKIP_SOURCES, outboxDegradeLogged, STOPWORDS, CONTEXT_GROUNDING_RULES;
4148
5667
  var init_memory = __esm({
4149
5668
  "packages/brain/src/memory/memory.ts"() {
4150
5669
  "use strict";
4151
5670
  init_database();
5671
+ init_config();
4152
5672
  init_logger();
5673
+ init_memory_grounding();
4153
5674
  init_utils();
4154
5675
  init_claude_client();
4155
5676
  init_solana_client();
4156
5677
  init_src();
4157
5678
  init_embeddings();
5679
+ init_migration_profile();
4158
5680
  init_wiki_packs();
4159
5681
  init_config2();
4160
5682
  init_bm25_search();
5683
+ init_content_tokens();
5684
+ init_reconcile_shadow();
5685
+ init_memory_encryption();
4161
5686
  init_openrouter_client();
4162
5687
  init_encryption();
5688
+ init_memory_decryption();
4163
5689
  init_event_bus();
4164
- import_crypto2 = require("crypto");
5690
+ import_crypto3 = require("crypto");
4165
5691
  init_graph();
4166
5692
  init_owner_context();
4167
5693
  EMBED_CACHE_MAX = 200;
4168
5694
  embeddingCache = /* @__PURE__ */ new Map();
4169
5695
  _ownerWallet = null;
4170
- SCOPE_BOT_OWN = "__BOT_OWN__";
5696
+ SCOPE_BOT_OWN2 = "__BOT_OWN__";
4171
5697
  HASH_ID_PREFIX = "clude";
4172
- log10 = createChildLogger("memory");
5698
+ log16 = createChildLogger("memory");
4173
5699
  DEDUP_TTL_MS = 10 * 60 * 1e3;
4174
5700
  dedupCache = /* @__PURE__ */ new Map();
4175
5701
  installedPacksCache = /* @__PURE__ */ new Map();
@@ -4182,6 +5708,7 @@ var init_memory = __esm({
4182
5708
  "locomo-benchmark",
4183
5709
  "longmemeval-benchmark"
4184
5710
  ]);
5711
+ outboxDegradeLogged = false;
4185
5712
  STOPWORDS = /* @__PURE__ */ new Set([
4186
5713
  "the",
4187
5714
  "a",
@@ -4266,18 +5793,92 @@ var init_memory = __esm({
4266
5793
  "our",
4267
5794
  "their"
4268
5795
  ]);
5796
+ CONTEXT_GROUNDING_RULES = `Ground specific claims \u2014 facts, preferences, history, dates, numbers, names \u2014 in the memories above; if a detail isn't written here, say you don't remember it rather than guessing. Each line is dated: when two memories disagree, read them as a timeline \u2014 use the most recent value for a question about now, and the value that was in effect at the time for a question about a specific past date ("as of \u2026"); never blend conflicting values. Don't invent memories or details that aren't above. General knowledge is unaffected \u2014 answer those normally.`;
5797
+ }
5798
+ });
5799
+
5800
+ // packages/brain/src/memory/memory-revoke.ts
5801
+ async function revokeMemory(db, memoryId) {
5802
+ const { data: mem } = await db.from("memories").select("summary, embedding, encrypted, provider_delegated").eq("id", memoryId).maybeSingle();
5803
+ if (!mem || mem.encrypted !== true || mem.provider_delegated === false) {
5804
+ return { revoked: false, reason: "not_delegated" };
5805
+ }
5806
+ const { data: wrap } = await db.from("memory_dek_wraps").select("wrapped_dek, wrap_pubkey").eq("memory_id", memoryId).eq("recipient", "provider").maybeSingle();
5807
+ if (!wrap) return { revoked: false, reason: "no_provider_wrap" };
5808
+ let providerSecret;
5809
+ try {
5810
+ providerSecret = loadProviderKeypair().secretKey;
5811
+ } catch {
5812
+ return { revoked: false, reason: "no_provider_key" };
5813
+ }
5814
+ const dek = unwrapDek(wrap.wrapped_dek, wrap.wrap_pubkey, providerSecret);
5815
+ if (!dek) return { revoked: false, reason: "unwrap_failed" };
5816
+ const summaryCt = encryptField(String(mem.summary ?? ""), dek);
5817
+ const embeddingCt = mem.embedding != null ? encryptField(String(mem.embedding), dek) : "";
5818
+ const { error } = await db.rpc("revoke_memory", {
5819
+ p_memory_id: memoryId,
5820
+ p_summary_ct: summaryCt,
5821
+ p_embedding_ct: embeddingCt
5822
+ });
5823
+ if (error) {
5824
+ log17.error({ memoryId, error: error.message }, "revoke_memory RPC failed");
5825
+ return { revoked: false, reason: "rpc_failed" };
5826
+ }
5827
+ log17.info({ memoryId }, "Memory revoked \u2014 provider access destroyed");
5828
+ return { revoked: true };
5829
+ }
5830
+ async function redelegateMemory(db, memoryId, providerWrap) {
5831
+ const { data: mem } = await db.from("memories").select("content, summary_ciphertext, embedding_ciphertext, encrypted").eq("id", memoryId).maybeSingle();
5832
+ if (!mem || mem.encrypted !== true) return { redelegated: false, reason: "not_encrypted" };
5833
+ let providerSecret;
5834
+ try {
5835
+ providerSecret = loadProviderKeypair().secretKey;
5836
+ } catch {
5837
+ return { redelegated: false, reason: "no_provider_key" };
5838
+ }
5839
+ const dek = unwrapDek(providerWrap.wrapped_dek, providerWrap.wrap_pubkey, providerSecret);
5840
+ if (!dek) return { redelegated: false, reason: "invalid_wrap" };
5841
+ const content = decryptField(String(mem.content ?? ""), dek);
5842
+ if (content === null) return { redelegated: false, reason: "invalid_wrap" };
5843
+ const summary = mem.summary_ciphertext ? decryptField(mem.summary_ciphertext, dek) : "";
5844
+ const embedding = mem.embedding_ciphertext ? decryptField(mem.embedding_ciphertext, dek) : "";
5845
+ const { error } = await db.rpc("redelegate_memory", {
5846
+ p_memory_id: memoryId,
5847
+ p_summary: summary ?? "",
5848
+ p_embedding: embedding ?? "",
5849
+ p_content: content,
5850
+ p_wrapped_dek: providerWrap.wrapped_dek,
5851
+ p_wrap_pubkey: providerWrap.wrap_pubkey
5852
+ });
5853
+ if (error) {
5854
+ log17.error({ memoryId, error: error.message }, "redelegate_memory RPC failed");
5855
+ return { redelegated: false, reason: "rpc_failed" };
5856
+ }
5857
+ log17.info({ memoryId }, "Memory re-delegated \u2014 provider access restored");
5858
+ return { redelegated: true };
5859
+ }
5860
+ var log17;
5861
+ var init_memory_revoke = __esm({
5862
+ "packages/brain/src/memory/memory-revoke.ts"() {
5863
+ "use strict";
5864
+ init_memory_envelope();
5865
+ init_encryption_keys();
5866
+ init_logger();
5867
+ log17 = createChildLogger("memory-revoke");
4269
5868
  }
4270
5869
  });
4271
5870
 
4272
5871
  // packages/brain/src/memory/index.ts
4273
5872
  var memory_exports2 = {};
4274
5873
  __export(memory_exports2, {
4275
- SCOPE_BOT_OWN: () => SCOPE_BOT_OWN,
5874
+ SCOPE_BOT_OWN: () => SCOPE_BOT_OWN2,
4276
5875
  _setOwnerWallet: () => _setOwnerWallet,
4277
5876
  calculateImportance: () => calculateImportance,
4278
5877
  createMemoryLink: () => createMemoryLink,
4279
5878
  createMemoryLinksBatch: () => createMemoryLinksBatch,
4280
5879
  decayMemories: () => decayMemories,
5880
+ decryptMemories: () => decryptMemories,
5881
+ decryptOneContent: () => decryptOneContent,
4281
5882
  deleteMemory: () => deleteMemory,
4282
5883
  formatMemoryContext: () => formatMemoryContext,
4283
5884
  generateHashId: () => generateHashId,
@@ -4293,6 +5894,8 @@ __export(memory_exports2, {
4293
5894
  moodToValence: () => moodToValence,
4294
5895
  recallMemories: () => recallMemories,
4295
5896
  recallMemorySummaries: () => recallMemorySummaries,
5897
+ redelegateMemory: () => redelegateMemory,
5898
+ revokeMemory: () => revokeMemory,
4296
5899
  scopeToOwner: () => scopeToOwner,
4297
5900
  scoreImportanceWithLLM: () => scoreImportanceWithLLM,
4298
5901
  scoreMemory: () => scoreMemory,
@@ -4304,6 +5907,8 @@ var init_memory2 = __esm({
4304
5907
  "packages/brain/src/memory/index.ts"() {
4305
5908
  "use strict";
4306
5909
  init_memory();
5910
+ init_memory_decryption();
5911
+ init_memory_revoke();
4307
5912
  }
4308
5913
  });
4309
5914
 
@@ -4343,7 +5948,7 @@ function evaluateConfidence(memories, opts = {}) {
4343
5948
  };
4344
5949
  if (!sufficient) {
4345
5950
  result.hedgingInstruction = score < 0.15 ? HEDGING_INSTRUCTIONS.noEvidence : HEDGING_INSTRUCTIONS.weakEvidence;
4346
- log11.info({
5951
+ log18.info({
4347
5952
  score: score.toFixed(3),
4348
5953
  threshold,
4349
5954
  components: {
@@ -4360,7 +5965,7 @@ function evaluateConfidence(memories, opts = {}) {
4360
5965
  function filterLowConfidenceMemories(memories, minScore = 0.15) {
4361
5966
  const filtered = memories.filter((m) => m._score >= minScore);
4362
5967
  if (filtered.length < memories.length) {
4363
- log11.debug({
5968
+ log18.debug({
4364
5969
  before: memories.length,
4365
5970
  after: filtered.length,
4366
5971
  minScore
@@ -4368,12 +5973,12 @@ function filterLowConfidenceMemories(memories, minScore = 0.15) {
4368
5973
  }
4369
5974
  return filtered;
4370
5975
  }
4371
- var log11, HEDGING_INSTRUCTIONS;
5976
+ var log18, HEDGING_INSTRUCTIONS;
4372
5977
  var init_confidence_gate = __esm({
4373
5978
  "packages/brain/src/experimental/confidence-gate.ts"() {
4374
5979
  "use strict";
4375
5980
  init_logger();
4376
- log11 = createChildLogger("exp-confidence");
5981
+ log18 = createChildLogger("exp-confidence");
4377
5982
  HEDGING_INSTRUCTIONS = {
4378
5983
  noEvidence: `IMPORTANT: Your memory retrieval returned no strong matches for this query.
4379
5984
  You MUST NOT fabricate or guess information. Instead:
@@ -4391,6 +5996,87 @@ may not directly answer the query. When responding:
4391
5996
  }
4392
5997
  });
4393
5998
 
5999
+ // packages/brain/package.json
6000
+ var require_package = __commonJS({
6001
+ "packages/brain/package.json"(exports2, module2) {
6002
+ module2.exports = {
6003
+ name: "@clude/brain",
6004
+ version: "3.3.0",
6005
+ private: true,
6006
+ description: "Clude brain \u2014 memory, dreams, agents, personality, SDK",
6007
+ main: "dist/index.js",
6008
+ types: "dist/index.d.ts",
6009
+ exports: {
6010
+ ".": {
6011
+ types: "./dist/index.d.ts",
6012
+ default: "./dist/index.js"
6013
+ },
6014
+ "./*": {
6015
+ types: "./dist/*.d.ts",
6016
+ default: "./dist/*.js"
6017
+ },
6018
+ "./memory": {
6019
+ types: "./dist/memory/index.d.ts",
6020
+ default: "./dist/memory/index.js"
6021
+ },
6022
+ "./agents": {
6023
+ types: "./dist/agents/index.d.ts",
6024
+ default: "./dist/agents/index.js"
6025
+ },
6026
+ "./features/compound": {
6027
+ types: "./dist/features/compound/index.d.ts",
6028
+ default: "./dist/features/compound/index.js"
6029
+ },
6030
+ "./sdk": {
6031
+ types: "./dist/sdk/index.d.ts",
6032
+ default: "./dist/sdk/index.js"
6033
+ }
6034
+ },
6035
+ scripts: {
6036
+ build: "tsc -p tsconfig.build.json",
6037
+ typecheck: "tsc --noEmit",
6038
+ test: "vitest run",
6039
+ "test:watch": "vitest"
6040
+ },
6041
+ dependencies: {
6042
+ "@ai-sdk/anthropic": "^3.0.64",
6043
+ "@ai-sdk/google": "^3.0.55",
6044
+ "@ai-sdk/openai": "^3.0.49",
6045
+ "@ai-sdk/xai": "^3.0.75",
6046
+ "@anthropic-ai/sdk": "^0.39.0",
6047
+ "@clude/memorypack": "workspace:*",
6048
+ "@clude/shared": "workspace:*",
6049
+ "@clude/tokenization": "workspace:*",
6050
+ "@huggingface/transformers": "^4.1.0",
6051
+ "@modelcontextprotocol/sdk": "^1.26.0",
6052
+ "@openrouter/ai-sdk-provider": "^2.3.3",
6053
+ "@privy-io/node": "^0.12.0",
6054
+ "@solana/web3.js": "^1.98.4",
6055
+ "@supabase/supabase-js": "^2.95.3",
6056
+ ai: "^6.0.142",
6057
+ "better-sqlite3": "^12.9.0",
6058
+ bs58: "^6.0.0",
6059
+ express: "^4.21.0",
6060
+ jose: "^6.1.3",
6061
+ "node-cron": "^3.0.3",
6062
+ "sqlite-vec": "^0.1.9",
6063
+ "twitter-api-v2": "^1.29.0",
6064
+ unpdf: "^1.4.0",
6065
+ "vercel-minimax-ai-provider": "^0.0.2",
6066
+ zod: "^4.3.6"
6067
+ },
6068
+ devDependencies: {
6069
+ "@types/better-sqlite3": "^7.6.13",
6070
+ "@types/express": "^4.17.25",
6071
+ "@types/node": "^22.19.11",
6072
+ "@types/node-cron": "^3.0.11",
6073
+ typescript: "^5.6.0",
6074
+ vitest: "^4.1.0"
6075
+ }
6076
+ };
6077
+ }
6078
+ });
6079
+
4394
6080
  // packages/brain/src/mcp/local-store.ts
4395
6081
  var local_store_exports = {};
4396
6082
  __export(local_store_exports, {
@@ -4646,7 +6332,7 @@ async function findClinamen(opts) {
4646
6332
  const limit = opts.limit || DEFAULT_LIMIT;
4647
6333
  const minImportance = opts.minImportance ?? MIN_IMPORTANCE;
4648
6334
  const maxRelevance = opts.maxRelevance ?? MAX_RELEVANCE_SIM;
4649
- log12.debug({ context: opts.context.slice(0, 60), limit }, "Searching for clinamen");
6335
+ log19.debug({ context: opts.context.slice(0, 60), limit }, "Searching for clinamen");
4650
6336
  let contextEmbedding = null;
4651
6337
  if (isEmbeddingEnabled()) {
4652
6338
  const cached = getCachedEmbedding(opts.context);
@@ -4658,7 +6344,7 @@ async function findClinamen(opts) {
4658
6344
  }
4659
6345
  }
4660
6346
  if (!contextEmbedding) {
4661
- log12.debug("No embedding available \u2014 falling back to importance-only clinamen");
6347
+ log19.debug("No embedding available \u2014 falling back to importance-only clinamen");
4662
6348
  return importanceOnlyClinamen(db, limit, minImportance, opts.memoryTypes);
4663
6349
  }
4664
6350
  const cutoff = new Date(Date.now() - MIN_AGE_HOURS * 60 * 60 * 1e3).toISOString();
@@ -4669,7 +6355,7 @@ async function findClinamen(opts) {
4669
6355
  }
4670
6356
  const { data: candidates, error } = await query;
4671
6357
  if (error || !candidates || candidates.length === 0) {
4672
- log12.debug({ error: error?.message }, "No clinamen candidates found");
6358
+ log19.debug({ error: error?.message }, "No clinamen candidates found");
4673
6359
  return [];
4674
6360
  }
4675
6361
  const scored = [];
@@ -4695,7 +6381,7 @@ async function findClinamen(opts) {
4695
6381
  }
4696
6382
  scored.sort((a, b) => b._divergence - a._divergence);
4697
6383
  const results = scored.slice(0, limit);
4698
- log12.info({
6384
+ log19.info({
4699
6385
  candidates: candidates.length,
4700
6386
  afterFilter: scored.length,
4701
6387
  returned: results.length,
@@ -4721,7 +6407,7 @@ async function importanceOnlyClinamen(db, limit, minImportance, memoryTypes) {
4721
6407
  _relevanceSim: 0
4722
6408
  }));
4723
6409
  }
4724
- var log12, MIN_IMPORTANCE, MAX_RELEVANCE_SIM, MIN_AGE_HOURS, CANDIDATE_POOL_SIZE, DEFAULT_LIMIT;
6410
+ var log19, MIN_IMPORTANCE, MAX_RELEVANCE_SIM, MIN_AGE_HOURS, CANDIDATE_POOL_SIZE, DEFAULT_LIMIT;
4725
6411
  var init_clinamen = __esm({
4726
6412
  "packages/brain/src/memory/clinamen.ts"() {
4727
6413
  "use strict";
@@ -4729,7 +6415,7 @@ var init_clinamen = __esm({
4729
6415
  init_memory();
4730
6416
  init_embeddings();
4731
6417
  init_logger();
4732
- log12 = createChildLogger("clinamen");
6418
+ log19 = createChildLogger("clinamen");
4733
6419
  MIN_IMPORTANCE = 0.6;
4734
6420
  MAX_RELEVANCE_SIM = 0.35;
4735
6421
  MIN_AGE_HOURS = 24;
@@ -4831,7 +6517,9 @@ function loadSelfHosted() {
4831
6517
  }
4832
6518
  var server = new import_mcp.McpServer({
4833
6519
  name: "clude-memory",
4834
- version: "2.7.0"
6520
+ // Single-sourced from the package manifest (inlined at bundle time) so the
6521
+ // MCP handshake, `clude --version`, and npm always agree.
6522
+ version: require_package().version
4835
6523
  });
4836
6524
  var MEMORY_TYPES = ["episodic", "semantic", "procedural", "self_model", "introspective"];
4837
6525
  server.tool(
@@ -4960,7 +6648,7 @@ server.tool(
4960
6648
  "Store a new memory. Memories persist across conversations and decay over time if not accessed.",
4961
6649
  {
4962
6650
  type: import_zod.z.enum(MEMORY_TYPES).describe("Memory type: episodic (events), semantic (knowledge), procedural (behaviors), self_model (self-awareness), introspective (journal entries)"),
4963
- content: import_zod.z.string().max(5e3, "Content cannot exceed 5000 characters").describe("Full memory content"),
6651
+ content: import_zod.z.string().trim().min(1, "content cannot be empty").max(5e3, "Content cannot exceed 5000 characters").describe("Full memory content"),
4964
6652
  summary: import_zod.z.string().max(500, "Summary cannot exceed 500 characters").describe("Short summary for recall matching"),
4965
6653
  tags: import_zod.z.array(import_zod.z.string()).optional().describe("Tags for filtering"),
4966
6654
  concepts: import_zod.z.array(import_zod.z.string()).optional().describe("Structured concept labels (auto-inferred if omitted)"),
@@ -5356,7 +7044,7 @@ server.tool(
5356
7044
  {
5357
7045
  memories: import_zod.z.array(import_zod.z.object({
5358
7046
  type: import_zod.z.enum(MEMORY_TYPES).describe("Memory type"),
5359
- content: import_zod.z.string().max(5e3).describe("Full memory content"),
7047
+ content: import_zod.z.string().trim().min(1, "content cannot be empty").max(5e3).describe("Full memory content"),
5360
7048
  summary: import_zod.z.string().max(500).describe("Short summary"),
5361
7049
  tags: import_zod.z.array(import_zod.z.string()).optional(),
5362
7050
  concepts: import_zod.z.array(import_zod.z.string()).optional(),