@cerefox/memory 1.15.2 → 1.16.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.
@@ -1103,6 +1103,82 @@ AS $$
1103
1103
  ORDER BY created_at DESC;
1104
1104
  $$;
1105
1105
 
1106
+ -- ── cerefox_purge_expired_trash (#251, 0.17.0) ───────────────────────────────
1107
+ -- Permanently purges documents that have been in the trash longer than
1108
+ -- `trash_retention_days`, when `trash_auto_purge_enabled` is exactly 'true'.
1109
+ -- Called by cerefox_delete_document after every real soft delete: the write
1110
+ -- that adds to the trash also sweeps it. There is no scheduler, by decision
1111
+ -- (docs/specs/trash-auto-purge.md): the trash only grows through deletes, so
1112
+ -- this keeps it bounded on every deployment without pg_cron or a daemon.
1113
+ --
1114
+ -- Safeguards, each load-bearing:
1115
+ -- * at most p_max documents per call, oldest first (partial index on
1116
+ -- deleted_at), so a large first backlog drains over several deletes;
1117
+ -- * FOR UPDATE SKIP LOCKED, so concurrent deletes sweep disjoint rows;
1118
+ -- * an unparsable or < 1 retention purges NOTHING (never "purge everything");
1119
+ -- * one audit entry per purge, authored 'trash-retention' / 'user': the
1120
+ -- authority is the operator's setting, not whoever's delete triggered it.
1121
+ -- Not exposed on MCP, the Edge Functions or the CLI: there is still no
1122
+ -- agent-callable purge (access-paths.md → trust model).
1123
+ CREATE OR REPLACE FUNCTION cerefox_purge_expired_trash(
1124
+ p_max INT DEFAULT 100
1125
+ )
1126
+ RETURNS INT
1127
+ LANGUAGE plpgsql
1128
+ SECURITY DEFINER
1129
+ SET search_path = public, pg_catalog
1130
+ AS $$
1131
+ DECLARE
1132
+ v_enabled TEXT;
1133
+ v_days_txt TEXT;
1134
+ v_days INT;
1135
+ v_cutoff TIMESTAMPTZ;
1136
+ v_count INT := 0;
1137
+ r RECORD;
1138
+ BEGIN
1139
+ SELECT value INTO v_enabled FROM cerefox_config WHERE key = 'trash_auto_purge_enabled';
1140
+ IF v_enabled IS DISTINCT FROM 'true' THEN
1141
+ RETURN 0;
1142
+ END IF;
1143
+
1144
+ SELECT value INTO v_days_txt FROM cerefox_config WHERE key = 'trash_retention_days';
1145
+ IF v_days_txt IS NULL OR BTRIM(v_days_txt) !~ '^[0-9]{1,6}$' THEN
1146
+ RETURN 0;
1147
+ END IF;
1148
+ v_days := BTRIM(v_days_txt)::INT;
1149
+ IF v_days < 1 THEN
1150
+ RETURN 0;
1151
+ END IF;
1152
+ v_cutoff := NOW() - make_interval(days => v_days);
1153
+
1154
+ FOR r IN
1155
+ SELECT id, title, total_chars, deleted_at
1156
+ FROM cerefox_documents
1157
+ WHERE deleted_at IS NOT NULL AND deleted_at < v_cutoff
1158
+ ORDER BY deleted_at
1159
+ LIMIT GREATEST(COALESCE(p_max, 0), 0)
1160
+ FOR UPDATE SKIP LOCKED
1161
+ LOOP
1162
+ PERFORM cerefox_create_audit_entry(
1163
+ p_document_id := r.id,
1164
+ p_operation := 'delete',
1165
+ p_author := 'trash-retention',
1166
+ p_author_type := 'user',
1167
+ p_size_before := r.total_chars,
1168
+ p_size_after := 0,
1169
+ p_description := 'Auto-purged from the trash (trash_retention_days = ' || v_days ||
1170
+ '; trashed ' || to_char(r.deleted_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') ||
1171
+ '): ' || COALESCE(r.title, '(untitled)') ||
1172
+ ' (' || COALESCE(r.total_chars, 0) || ' chars)'
1173
+ );
1174
+ DELETE FROM cerefox_documents WHERE id = r.id;
1175
+ v_count := v_count + 1;
1176
+ END LOOP;
1177
+
1178
+ RETURN v_count;
1179
+ END;
1180
+ $$;
1181
+
1106
1182
  -- ── cerefox_delete_document (soft delete) ────────────────────────────────────
1107
1183
  -- Soft-deletes a document by setting deleted_at = NOW(). The document, its
1108
1184
  -- chunks, and versions remain in the database but are excluded from search.
@@ -1123,6 +1199,12 @@ $$;
1123
1199
  --
1124
1200
  -- Deleting an already-deleted document is a reported no-op: the original
1125
1201
  -- deleted_at is preserved and no duplicate audit entry is written.
1202
+ --
1203
+ -- 0.17.0 (#251): a real soft delete then sweeps the trash
1204
+ -- (cerefox_purge_expired_trash) and reports how many documents it purged as
1205
+ -- `auto_purged`. The sweep runs in its own sub-block: if it fails, only its
1206
+ -- savepoint rolls back, a WARNING is raised, and the delete commits exactly as
1207
+ -- it would with auto-purge off. A sweep can never fail a delete.
1126
1208
 
1127
1209
  DROP FUNCTION IF EXISTS cerefox_delete_document(UUID, TEXT, TEXT, TEXT, TEXT);
1128
1210
  DROP FUNCTION IF EXISTS cerefox_delete_document(UUID, TEXT, TEXT);
@@ -1144,6 +1226,7 @@ DECLARE
1144
1226
  v_total_chars INT;
1145
1227
  v_current_hash TEXT;
1146
1228
  v_deleted_at TIMESTAMPTZ;
1229
+ v_auto_purged INT := 0;
1147
1230
  BEGIN
1148
1231
  -- FOR UPDATE: makes the hash check atomic with the delete — a concurrent
1149
1232
  -- content update serializes here, and a stale deleter sees its hash.
@@ -1202,12 +1285,21 @@ BEGIN
1202
1285
  COALESCE('; reason: ' || NULLIF(BTRIM(p_reason), ''), '')
1203
1286
  );
1204
1287
 
1288
+ BEGIN
1289
+ v_auto_purged := cerefox_purge_expired_trash(100);
1290
+ EXCEPTION WHEN OTHERS THEN
1291
+ v_auto_purged := 0;
1292
+ RAISE WARNING 'cerefox: trash auto-purge skipped after deleting %: % (%)',
1293
+ p_document_id, SQLERRM, SQLSTATE;
1294
+ END;
1295
+
1205
1296
  RETURN jsonb_build_object(
1206
1297
  'document_id', p_document_id,
1207
1298
  'title', v_title,
1208
1299
  'total_chars', v_total_chars,
1209
1300
  'deleted_at', v_deleted_at,
1210
- 'already_deleted', FALSE
1301
+ 'already_deleted', FALSE,
1302
+ 'auto_purged', v_auto_purged
1211
1303
  );
1212
1304
  END;
1213
1305
  $$;
@@ -2742,7 +2834,10 @@ DECLARE
2742
2834
  -- (0 = off). Partial edits make writes cheap, so an insert-only agent
2743
2835
  -- never assembles the document and never sees it grow past its split
2744
2836
  -- point. A signal in the write's response, never a refusal.
2745
- 'document_size_warning_chars'
2837
+ 'document_size_warning_chars',
2838
+ -- #251: trash auto-purge, off by default; read by
2839
+ -- cerefox_purge_expired_trash on every soft delete.
2840
+ 'trash_auto_purge_enabled', 'trash_retention_days'
2746
2841
  ];
2747
2842
  v_old TEXT;
2748
2843
  BEGIN
@@ -3091,6 +3186,9 @@ SET search_path = public, pg_catalog
3091
3186
  AS $$
3092
3187
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
3093
3188
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
3189
+ -- 0.17.0 (#251): trash auto-purge. New cerefox_purge_expired_trash, called
3190
+ -- by cerefox_delete_document after a real soft delete (which now returns
3191
+ -- `auto_purged`); two config keys, off by default. Migration 0033.
3094
3192
  -- 0.16.2 (#26 follow-up): the Data API grant list is DERIVED from pg_class
3095
3193
  -- rather than a hand-written array that has to match the set of tables. The
3096
3194
  -- array had drifted (cerefox_document_relations), which only a FRESH cloud
@@ -3129,7 +3227,7 @@ AS $$
3129
3227
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
3130
3228
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
3131
3229
  -- sequence, so a store deploying this gets everything from both lines.
3132
- SELECT '0.16.2'::TEXT;
3230
+ SELECT '0.17.0'::TEXT;
3133
3231
  $$;
3134
3232
 
3135
3233
  -- ── cerefox_find_dead_links ──────────────────────────────────────────────────
@@ -5,7 +5,7 @@
5
5
  -- Requires extensions: vector (pgvector), uuid-ossp
6
6
  -- These are enabled at the top of db_deploy.py before this file is applied.
7
7
  --
8
- -- @version: 0.16.2
8
+ -- @version: 0.17.0
9
9
  -- The `@version` marker above is read by the schema-version-mismatch banner
10
10
  -- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
11
11
  -- changes in a way that requires `cerefox server deploy` to be re-run —
@@ -413,6 +413,15 @@ ON CONFLICT (key) DO NOTHING;
413
413
  INSERT INTO cerefox_config (key, value)
414
414
  VALUES ('review_workflow_enabled', 'false')
415
415
  ON CONFLICT (key) DO NOTHING;
416
+ -- Trash auto-purge (#251): OFF by default. When on, the soft delete that adds
417
+ -- to the trash also purges documents trashed more than trash_retention_days
418
+ -- ago (cerefox_purge_expired_trash). Design: docs/specs/trash-auto-purge.md.
419
+ INSERT INTO cerefox_config (key, value)
420
+ VALUES ('trash_auto_purge_enabled', 'false')
421
+ ON CONFLICT (key) DO NOTHING;
422
+ INSERT INTO cerefox_config (key, value)
423
+ VALUES ('trash_retention_days', '60')
424
+ ON CONFLICT (key) DO NOTHING;
416
425
 
417
426
 
418
427
  -- ── Usage log ────────────────────────────────────────────────────────────────
@@ -374,7 +374,7 @@ Deno.serve(async (req: Request) => {
374
374
 
375
375
  // Content unchanged -- skip re-indexing
376
376
  if (existingDoc.content_hash === contentHash) {
377
- const note = update_if_exists ? undefined : "update_if_exists flag was overridden by document_id";
377
+ const note = body.update_if_exists === false ? "update_if_exists flag was overridden by document_id" : undefined;
378
378
  return new Response(
379
379
  JSON.stringify({
380
380
  document_id: existingDoc.id,
@@ -462,7 +462,7 @@ Deno.serve(async (req: Request) => {
462
462
  await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
463
463
  }
464
464
 
465
- const note = update_if_exists ? undefined : "update_if_exists flag was overridden by document_id";
465
+ const note = body.update_if_exists === false ? "update_if_exists flag was overridden by document_id" : undefined;
466
466
  return new Response(
467
467
  JSON.stringify({
468
468
  document_id: existingDoc.id,