@cerefox/memory 1.17.4 → 1.19.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.
Files changed (48) hide show
  1. package/AGENT_GUIDE.md +8 -3
  2. package/AGENT_QUICK_REFERENCE.md +3 -3
  3. package/README.md +1 -1
  4. package/dist/bin/cerefox.js +1542 -1123
  5. package/dist/frontend/assets/index-01k5C3sG.css +1 -0
  6. package/dist/frontend/assets/index-HNs8rgY1.js +121 -0
  7. package/dist/frontend/assets/index-HNs8rgY1.js.map +1 -0
  8. package/dist/frontend/index.html +2 -2
  9. package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
  10. package/dist/server-assets/_shared/embeddings/onnx-embedder.ts +25 -9
  11. package/dist/server-assets/_shared/mcp-tools/_document-meta.ts +24 -2
  12. package/dist/server-assets/_shared/mcp-tools/_utils.ts +92 -0
  13. package/dist/server-assets/_shared/mcp-tools/audit-log.ts +6 -2
  14. package/dist/server-assets/_shared/mcp-tools/audit-ops.ts +27 -0
  15. package/dist/server-assets/_shared/mcp-tools/get-document.ts +11 -1
  16. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
  17. package/dist/server-assets/_shared/mcp-tools/ingest.ts +55 -17
  18. package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +3 -1
  19. package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +8 -13
  20. package/dist/server-assets/_shared/mcp-tools/search.ts +11 -2
  21. package/dist/server-assets/_shared/mcp-tools/set-document-metadata.ts +31 -5
  22. package/dist/server-assets/db/migrations/0012_content_format.sql +1 -1
  23. package/dist/server-assets/db/migrations/0014_document_relations.sql +1 -1
  24. package/dist/server-assets/db/migrations/0020_ingest_preserves_source.sql +1 -1
  25. package/dist/server-assets/db/migrations/0022_rls_on_document_relations.sql +1 -1
  26. package/dist/server-assets/db/migrations/0032_derive_grants.sql +1 -1
  27. package/dist/server-assets/db/migrations/0034_drop_vector_indexes.sql +22 -0
  28. package/dist/server-assets/db/migrations/0035_drop_content_hash_unique.sql +21 -0
  29. package/dist/server-assets/db/migrations/0036_metadata_revision.sql +43 -0
  30. package/dist/server-assets/db/rpcs.sql +260 -58
  31. package/dist/server-assets/db/schema.sql +49 -18
  32. package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +8 -1
  33. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +42 -6
  34. package/docs/api/openapi.json +77 -3
  35. package/docs/guides/access-paths.md +7 -3
  36. package/docs/guides/api.md +1 -1
  37. package/docs/guides/cli.md +5 -3
  38. package/docs/guides/configuration.md +23 -2
  39. package/docs/guides/connect-agents.md +60 -25
  40. package/docs/guides/ops-scripts.md +1 -1
  41. package/docs/guides/quickstart.md +7 -3
  42. package/docs/guides/setup-local.md +30 -6
  43. package/docs/guides/setup-supabase.md +5 -0
  44. package/docs/guides/upgrading.md +16 -1
  45. package/package.json +7 -7
  46. package/dist/frontend/assets/index-B0H_N4if.css +0 -1
  47. package/dist/frontend/assets/index-D1kdLLr8.js +0 -121
  48. package/dist/frontend/assets/index-D1kdLLr8.js.map +0 -1
@@ -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.18.2
8
+ -- @version: 0.21.1
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 —
@@ -40,7 +40,9 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
40
40
  source TEXT NOT NULL DEFAULT 'manual',
41
41
  -- source values: 'file' | 'paste' | 'agent' | 'url' | 'manual'
42
42
  source_path TEXT,
43
- -- SHA-256 of raw markdown content; used for deduplication
43
+ -- SHA-256 of raw markdown content: the optimistic-concurrency token, and
44
+ -- the create-time duplicate check (live documents only, in
45
+ -- cerefox_ingest_document). Deliberately NOT unique (0.20.0, #332).
44
46
  content_hash TEXT NOT NULL,
45
47
  -- #212 (0.12.2): must be a JSON OBJECT — jsonb accepts any JSON value,
46
48
  -- but every reader assumes an object, and non-object values were silently
@@ -51,6 +53,12 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
51
53
  metadata JSONB NOT NULL DEFAULT '{}'::jsonb
52
54
  CONSTRAINT cerefox_documents_metadata_object
53
55
  CHECK (jsonb_typeof(metadata) = 'object'),
56
+ -- #333 (0.21.0): the metadata's optimistic-concurrency token, independent
57
+ -- of content_hash (which covers the body only). Starts at 1 and goes up by
58
+ -- one whenever `metadata` actually changes; maintained by the
59
+ -- trig_cerefox_documents_metadata_revision trigger below, so every writer
60
+ -- is covered and no caller can set it.
61
+ metadata_revision INT NOT NULL DEFAULT 1,
54
62
  chunk_count INT NOT NULL DEFAULT 0,
55
63
  total_chars INT NOT NULL DEFAULT 0,
56
64
  -- review_status: human governance flag. 'approved' = validated by human,
@@ -71,7 +79,6 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
71
79
  -- Search indexes exclude soft-deleted docs. Purge does the real CASCADE DELETE.
72
80
  deleted_at TIMESTAMPTZ DEFAULT NULL,
73
81
 
74
- CONSTRAINT cerefox_documents_hash_unique UNIQUE (content_hash),
75
82
  CONSTRAINT cerefox_documents_review_status_check CHECK (review_status IN ('approved', 'pending_review'))
76
83
  );
77
84
 
@@ -127,6 +134,11 @@ CREATE TABLE IF NOT EXISTS cerefox_audit_log (
127
134
  -- author_type: 'user' (human via web UI/CLI) or 'agent' (AI via MCP/Edge Function)
128
135
  size_before INT,
129
136
  size_after INT,
137
+ -- #333 (0.21.0): on 'update-metadata' entries, the document's metadata
138
+ -- before and after the change, so every metadata change is recoverable
139
+ -- (content versions never captured metadata). NULL on other operations.
140
+ metadata_before JSONB,
141
+ metadata_after JSONB,
130
142
  description TEXT NOT NULL DEFAULT '',
131
143
  -- description: free-text explaining what changed and why.
132
144
  -- Auto-generated for system actions (approval, archival, retention cleanup).
@@ -166,7 +178,7 @@ CREATE TABLE IF NOT EXISTS cerefox_document_projects (
166
178
  -- ── Document relations (iteration 29) ─────────────────────────────────────────
167
179
  -- Typed, directed edges between documents. rel_type is free text by design so
168
180
  -- agents can define new types without a migration; the type dictionary lives in
169
- -- the RPCs and gives KNOWN types behaviour (symmetry, lifecycle side effects).
181
+ -- the RPCs and gives KNOWN types behavior (symmetry, lifecycle side effects).
170
182
  -- Design: docs/research/document-relations-and-semantic-graph.md §2.2.
171
183
 
172
184
  CREATE TABLE IF NOT EXISTS cerefox_document_relations (
@@ -267,17 +279,11 @@ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_fts
267
279
  ON cerefox_chunks USING GIN(fts)
268
280
  WHERE version_id IS NULL;
269
281
 
270
- -- Vector similarity — primary embedding, current chunks only
271
- CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_emb_primary
272
- ON cerefox_chunks USING hnsw (embedding_primary vector_cosine_ops)
273
- WITH (m = 16, ef_construction = 64)
274
- WHERE version_id IS NULL;
275
-
276
- -- Vector similarity — upgrade embedding, current chunks only
277
- CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_emb_upgrade
278
- ON cerefox_chunks USING hnsw (embedding_upgrade vector_cosine_ops)
279
- WITH (m = 16, ef_construction = 64)
280
- WHERE version_id IS NULL;
282
+ -- Vector similarity: deliberately NO index (0.19.0, migration 0034). Search
283
+ -- ranks vector candidates by an exact scan, which is both correct and fast at
284
+ -- knowledge-base scale; the HNSW indexes it replaced returned too few, mostly
285
+ -- wrong candidates on stores with version churn. Do not add one back without
286
+ -- the measurements in docs/specs/search-calibration.md (scale section).
281
287
 
282
288
  -- Partial unique index: enforces (document_id, chunk_index) uniqueness for
283
289
  -- current chunks. Archived chunks are excluded and may share chunk_index values
@@ -295,7 +301,7 @@ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_version
295
301
  CREATE INDEX IF NOT EXISTS idx_cerefox_docs_metadata
296
302
  ON cerefox_documents USING GIN(metadata);
297
303
 
298
- -- Documents by content_hash (unique constraint covers equality; this covers range/prefix)
304
+ -- Documents by content_hash (the create-time duplicate check and hash lookups)
299
305
  CREATE INDEX IF NOT EXISTS idx_cerefox_docs_hash
300
306
  ON cerefox_documents(content_hash);
301
307
 
@@ -350,6 +356,31 @@ CREATE OR REPLACE TRIGGER trig_cerefox_chunks_updated
350
356
  BEFORE UPDATE ON cerefox_chunks
351
357
  FOR EACH ROW EXECUTE FUNCTION cerefox_set_updated_at();
352
358
 
359
+ -- ── metadata_revision trigger (#333, 0.21.0) ─────────────────────────────────
360
+ -- Bumps metadata_revision when, and only when, metadata actually changes. A
361
+ -- trigger rather than per-RPC code because metadata has several writers (the
362
+ -- metadata RPC, the ingest RPC, direct table updates from older clients), and
363
+ -- a counter that one of them forgets to bump is a token that lies. It also
364
+ -- pins the column on every other UPDATE, so no caller can set it directly.
365
+ CREATE OR REPLACE FUNCTION cerefox_bump_metadata_revision()
366
+ RETURNS TRIGGER
367
+ LANGUAGE plpgsql
368
+ SET search_path = public, pg_catalog
369
+ AS $$
370
+ BEGIN
371
+ IF NEW.metadata IS DISTINCT FROM OLD.metadata THEN
372
+ NEW.metadata_revision := OLD.metadata_revision + 1;
373
+ ELSE
374
+ NEW.metadata_revision := OLD.metadata_revision;
375
+ END IF;
376
+ RETURN NEW;
377
+ END;
378
+ $$;
379
+
380
+ CREATE OR REPLACE TRIGGER trig_cerefox_documents_metadata_revision
381
+ BEFORE UPDATE ON cerefox_documents
382
+ FOR EACH ROW EXECUTE FUNCTION cerefox_bump_metadata_revision();
383
+
353
384
 
354
385
  -- ── Live-database migration ───────────────────────────────────────────────────
355
386
  -- When applying this schema to an existing database that still has the old
@@ -408,7 +439,7 @@ ON CONFLICT (key) DO NOTHING;
408
439
  -- The review workflow (agent writes land pending_review, a person approves)
409
440
  -- is OFF on a fresh install (#241): most stores have no reviewer, and a queue
410
441
  -- nobody drains is noise. Migration 0031 seeds TRUE on stores that predate the
411
- -- flag, so an upgrade never changes behaviour. Only this seed and that
442
+ -- flag, so an upgrade never changes behavior. Only this seed and that
412
443
  -- migration ever write the value; the toggle itself is `cerefox config set`.
413
444
  INSERT INTO cerefox_config (key, value)
414
445
  VALUES ('review_workflow_enabled', 'false')
@@ -491,7 +522,7 @@ ALTER TABLE cerefox_document_relations ENABLE ROW LEVEL SECURITY;
491
522
  -- Guarded: on the local (World B) stack this file deploys BEFORE roles.sql
492
523
  -- creates service_role, so missing-role must be a no-op (roles.sql re-runs the
493
524
  -- grants implicitly via its own PostgREST wiring; the next deploy picks them up).
494
- -- DERIVED from the catalogue, never listed. The list used to be a hand-written
525
+ -- DERIVED from the catalog, never listed. The list used to be a hand-written
495
526
  -- ARRAY and it drifted: cerefox_document_relations was added above and never
496
527
  -- added to the array, so a FRESH cloud deploy left that table with no Data API
497
528
  -- grant. Upgrades hid it (migration 0014 grants it inline) and the self-hosted
@@ -19,7 +19,8 @@ import { callerIdentity } from "../../../_shared/mcp-tools/identity.ts";
19
19
  * { document_id: string, version_id?: string | null }
20
20
  *
21
21
  * Response (200):
22
- * { document_id, doc_title, full_content, chunk_count, total_chars, is_archived, version_id, content_hash }
22
+ * { document_id, doc_title, full_content, chunk_count, total_chars, is_archived, version_id, content_hash,
23
+ * metadata, metadata_revision }
23
24
  * Response (404):
24
25
  * { error: "Document not found" }
25
26
  * Response (400):
@@ -108,6 +109,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
108
109
  chunk_count?: number;
109
110
  total_chars?: number;
110
111
  content_hash?: string;
112
+ doc_metadata?: Record<string, unknown> | null;
113
+ metadata_revision?: number | null;
111
114
  } | undefined;
112
115
 
113
116
  if (!row) {
@@ -138,6 +141,10 @@ Deno.serve(async (req: Request): Promise<Response> => {
138
141
  // Optimistic-concurrency token (iter-32): always the CURRENT hash —
139
142
  // pass back as expected_content_hash when updating via ingest.
140
143
  content_hash: row.content_hash ?? null,
144
+ // #333: the metadata and its own token — pass metadata_revision back as
145
+ // expected_metadata_revision when changing metadata via ingest.
146
+ metadata: row.doc_metadata ?? {},
147
+ metadata_revision: version_id === null ? (row.metadata_revision ?? null) : null,
141
148
  }),
142
149
  { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
143
150
  );
@@ -3,6 +3,7 @@ import { createClient } from "jsr:@supabase/supabase-js@2";
3
3
  import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
4
  import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
5
5
  import { callerIdentity } from "../../../_shared/mcp-tools/identity.ts";
6
+ import { duplicateContentDocumentId, legacyContentUniqueError } from "../../../_shared/mcp-tools/_utils.ts";
6
7
  import { capEmbeddingInput } from "../../../_shared/embeddings/index.ts";
7
8
  import {
8
9
  ensureDocumentInProject,
@@ -61,24 +62,26 @@ interface IngestRequest {
61
62
  expected_content_hash?: string;
62
63
  // Explicitly skip the concurrency check (external source of truth).
63
64
  last_write_wins?: boolean;
65
+ // #333: the metadata's token, checked when `metadata` is passed on an update
66
+ // (metadata merges; a null value removes a key).
67
+ expected_metadata_revision?: number;
64
68
  }
65
69
 
66
70
  // Map the RPC's expected/validation errors to proper HTTP responses:
67
71
  // CEREFOX_CONFLICT → 409 (stale optimistic-concurrency token)
68
72
  // CEREFOX_TOKEN_REQUIRED → 400 (missing expected_content_hash)
69
- // cerefox_documents_hash_unique → 409 (content de-dup: another doc already
70
- // holds identical content)
73
+ // CEREFOX_METADATA_CONFLICT → 409 (stale expected_metadata_revision, #333)
71
74
  // Returns null for genuinely unexpected errors (→ 500 at the call site).
72
75
  function concurrencyErrorResponse(
73
76
  message: string,
74
77
  headers: Record<string, string>,
75
78
  ): Response | null {
76
- if (message.includes("cerefox_documents_hash_unique")) {
79
+ if (message.includes("CEREFOX_METADATA_CONFLICT")) {
77
80
  return new Response(
78
81
  JSON.stringify({
79
- error: "duplicate_content",
82
+ error: "metadata_conflict",
80
83
  message:
81
- "Another document already has identical content. Cerefox de-duplicates by content hash — update that document instead of creating or editing a second copy to match it.",
84
+ "The document's metadata changed since it was read. Re-read it (getDocument), reapply your change, and retry with the new expected_metadata_revision.",
82
85
  detail: message,
83
86
  }),
84
87
  { status: 409, headers },
@@ -278,7 +281,7 @@ Deno.serve(async (req: Request) => {
278
281
  // metadata: null = "not provided" — the RPC keeps existing metadata on
279
282
  // update and uses {} on create (v0.11.1; a `= {}` default here used to wipe
280
283
  // a document's tags on every content update that didn't re-pass them).
281
- const { title, content, document_id = null, project_name, source = "agent", metadata = null, update_if_exists = false, author_type = "agent", expected_content_hash = null, last_write_wins = false } = body;
284
+ const { title, content, document_id = null, project_name, source = "agent", metadata = null, update_if_exists = false, author_type = "agent", expected_content_hash = null, last_write_wins = false, expected_metadata_revision = null } = body;
282
285
  const callerName = callerIdentity(body as unknown as Record<string, unknown>);
283
286
  const author = callerName ?? "agent";
284
287
 
@@ -436,6 +439,9 @@ Deno.serve(async (req: Request) => {
436
439
  p_source_label: source,
437
440
  p_expected_content_hash: expected_content_hash,
438
441
  p_last_write_wins: last_write_wins,
442
+ // Only when set (#333 review): a null would name a signature a pre-0.21.0
443
+ // schema lacks, failing the whole update.
444
+ ...(expected_metadata_revision != null ? { p_expected_metadata_revision: expected_metadata_revision } : {}),
439
445
  p_content_format: CONTENT_FORMAT_BLIND_STITCH,
440
446
  });
441
447
 
@@ -554,6 +560,9 @@ Deno.serve(async (req: Request) => {
554
560
  p_source_label: source,
555
561
  p_expected_content_hash: expected_content_hash,
556
562
  p_last_write_wins: last_write_wins,
563
+ // Only when set (#333 review): a null would name a signature a pre-0.21.0
564
+ // schema lacks, failing the whole update.
565
+ ...(expected_metadata_revision != null ? { p_expected_metadata_revision: expected_metadata_revision } : {}),
557
566
  p_content_format: CONTENT_FORMAT_BLIND_STITCH,
558
567
  });
559
568
 
@@ -600,10 +609,14 @@ Deno.serve(async (req: Request) => {
600
609
  }
601
610
 
602
611
  // ── Hash deduplication (normal create path) ────────────────────────────────
612
+ // Creates only, against LIVE documents (#332). Saves the embedding spend;
613
+ // the RPC repeats the check authoritatively.
603
614
  const { data: hashMatch } = await supabase
604
615
  .from("cerefox_documents")
605
616
  .select("id, title")
606
617
  .eq("content_hash", contentHash)
618
+ .is("deleted_at", null)
619
+ .order("created_at", { ascending: true })
607
620
  .limit(1);
608
621
 
609
622
  if (hashMatch?.length) {
@@ -665,7 +678,30 @@ Deno.serve(async (req: Request) => {
665
678
  p_content_format: CONTENT_FORMAT_BLIND_STITCH,
666
679
  });
667
680
 
681
+ // The RPC's duplicate check caught an identical create that landed after
682
+ // the pre-check above. Nothing was written; answer as the pre-check does.
683
+ const duplicateId = duplicateContentDocumentId(ingestErr?.message ?? "");
684
+ if (duplicateId) {
685
+ const { data: existing } = await supabase
686
+ .from("cerefox_documents")
687
+ .select("title")
688
+ .eq("id", duplicateId)
689
+ .maybeSingle();
690
+ return new Response(
691
+ JSON.stringify({
692
+ document_id: duplicateId,
693
+ title: existing?.title ?? title.trim(),
694
+ skipped: true,
695
+ message: "Document already exists (content hash match)",
696
+ }),
697
+ { headers },
698
+ );
699
+ }
668
700
  if (ingestErr || !ingestResult?.length) {
701
+ // Functions deployed ahead of the schema still meet the pre-0.20.0
702
+ // store-wide content constraint.
703
+ const legacy = legacyContentUniqueError(ingestErr?.message ?? "");
704
+ if (legacy) return new Response(JSON.stringify({ error: legacy.message }), { status: 409, headers });
669
705
  return new Response(
670
706
  JSON.stringify({ error: `Ingest RPC failed: ${ingestErr?.message ?? "no data returned"}` }),
671
707
  { status: 500, headers },
@@ -206,6 +206,34 @@
206
206
  "created_at": {
207
207
  "default": "",
208
208
  "type": "string"
209
+ },
210
+ "metadata_before": {
211
+ "anyOf": [
212
+ {
213
+ "type": "object",
214
+ "propertyNames": {
215
+ "type": "string"
216
+ },
217
+ "additionalProperties": {}
218
+ },
219
+ {
220
+ "type": "null"
221
+ }
222
+ ]
223
+ },
224
+ "metadata_after": {
225
+ "anyOf": [
226
+ {
227
+ "type": "object",
228
+ "propertyNames": {
229
+ "type": "string"
230
+ },
231
+ "additionalProperties": {}
232
+ },
233
+ {
234
+ "type": "null"
235
+ }
236
+ ]
209
237
  }
210
238
  },
211
239
  "required": [
@@ -1498,6 +1526,18 @@
1498
1526
  "null"
1499
1527
  ]
1500
1528
  },
1529
+ "metadata_revision": {
1530
+ "anyOf": [
1531
+ {
1532
+ "type": "integer",
1533
+ "minimum": -9007199254740991,
1534
+ "maximum": 9007199254740991
1535
+ },
1536
+ {
1537
+ "type": "null"
1538
+ }
1539
+ ]
1540
+ },
1501
1541
  "versions": {
1502
1542
  "default": [],
1503
1543
  "type": "array",
@@ -2007,7 +2047,7 @@
2007
2047
  }
2008
2048
  },
2009
2049
  "metadata": {
2010
- "description": "Replaces the metadata. `{}` clears every key; omit to leave it unchanged.",
2050
+ "description": "Replaces the metadata (this route edits it as a whole form). `{}` clears every key; omit to leave it unchanged. Send expected_metadata_revision with it.",
2011
2051
  "type": "object",
2012
2052
  "propertyNames": {
2013
2053
  "type": "string"
@@ -2021,6 +2061,19 @@
2021
2061
  "null"
2022
2062
  ]
2023
2063
  },
2064
+ "expected_metadata_revision": {
2065
+ "description": "The metadata_revision you read (GET /documents/{id}). Required with `metadata` for an identified caller; a stale one is a 409.",
2066
+ "anyOf": [
2067
+ {
2068
+ "type": "integer",
2069
+ "minimum": -9007199254740991,
2070
+ "maximum": 9007199254740991
2071
+ },
2072
+ {
2073
+ "type": "null"
2074
+ }
2075
+ ]
2076
+ },
2024
2077
  "last_write_wins": {
2025
2078
  "description": "Skip the concurrency check. Only when an external source of truth makes a conflict meaningless.",
2026
2079
  "type": "boolean"
@@ -3471,12 +3524,19 @@
3471
3524
  "type": "string"
3472
3525
  },
3473
3526
  "metadata": {
3527
+ "description": "On an update it MERGES into the stored metadata: a null value removes a key, omitted keys are kept.",
3474
3528
  "type": "object",
3475
3529
  "propertyNames": {
3476
3530
  "type": "string"
3477
3531
  },
3478
3532
  "additionalProperties": {}
3479
3533
  },
3534
+ "expected_metadata_revision": {
3535
+ "description": "Optional, with metadata on an update: the metadata_revision you read. A stale one is a 409.",
3536
+ "type": "integer",
3537
+ "minimum": -9007199254740991,
3538
+ "maximum": 9007199254740991
3539
+ },
3480
3540
  "source": {
3481
3541
  "description": "Origin label. Defaults to \"paste\".",
3482
3542
  "type": "string"
@@ -5731,7 +5791,7 @@
5731
5791
  },
5732
5792
  "/api/v1/version": {
5733
5793
  "get": {
5734
- "summary": "Server version and environment label.",
5794
+ "summary": "Server version, environment label, and the newest release on npm as the server last checked it (`latest`, plus `update_command` when it is newer).",
5735
5795
  "operationId": "get_version",
5736
5796
  "responses": {
5737
5797
  "200": {
@@ -5761,13 +5821,27 @@
5761
5821
  "string",
5762
5822
  "null"
5763
5823
  ]
5824
+ },
5825
+ "latest": {
5826
+ "type": [
5827
+ "string",
5828
+ "null"
5829
+ ]
5830
+ },
5831
+ "update_command": {
5832
+ "type": [
5833
+ "string",
5834
+ "null"
5835
+ ]
5764
5836
  }
5765
5837
  },
5766
5838
  "required": [
5767
5839
  "version",
5768
5840
  "git_commit_short",
5769
5841
  "build_date",
5770
- "env_label"
5842
+ "env_label",
5843
+ "latest",
5844
+ "update_command"
5771
5845
  ],
5772
5846
  "additionalProperties": false
5773
5847
  }
@@ -60,8 +60,12 @@ Postgres RPCs
60
60
  ```
61
61
 
62
62
  **ChatGPT Custom GPT Actions** call the eight primitive Edge Functions directly over HTTPS
63
- using an OpenAPI schema. `cerefox-mcp` is not involved (ChatGPT does not support the
64
- Streamable HTTP MCP protocol).
63
+ using an OpenAPI schema. `cerefox-mcp` is not involved. OpenAI retires Custom GPTs on **December 11, 2026**, and custom actions do not carry over to its plugins, so this path ends
64
+ then. ChatGPT now supports remote MCP connectors (OAuth or no auth, no static bearer
65
+ token), so its replacement is expected to be a connector to `cerefox-mcp` over OAuth,
66
+ like cloud Claude; see
67
+ [`chatgpt-after-custom-gpts.md`](../research/chatgpt-after-custom-gpts.md) and
68
+ [#326](https://github.com/fstamatelopoulos/cerefox/issues/326).
65
69
 
66
70
  **curl / scripts / custom HTTP clients** can also call the primitives directly using the
67
71
  same Cerefox token as a Bearer token.
@@ -213,7 +217,7 @@ single container with an internally-held token.
213
217
  | Claude Code / Cursor | HTTPS → `cerefox-mcp` | Cerefox access token (`cfx_pat_…`) | Advanced/fallback; prefer local MCP for daily use |
214
218
  | Claude Desktop | HTTPS → `cerefox-mcp` (via `supergateway`) | Cerefox access token (`cfx_pat_…`) | Advanced/fallback; prefer local MCP for daily use |
215
219
  | **Cloud Claude (claude.ai web + mobile)** | HTTPS → `cerefox-mcp` over **OAuth 2.1** | Owner-pinned OAuth access token (JWKS-verified) | Optional; memory in the browser + on the phone |
216
- | ChatGPT Custom GPT | HTTPS → primitive Edge Functions | Cerefox access token (`cfx_pat_…`) | AI assistant via GPT Actions |
220
+ | ChatGPT Custom GPT | HTTPS → primitive Edge Functions | Cerefox access token (`cfx_pat_…`) | AI assistant via GPT Actions; **ends 2026-12-11** (Custom GPTs retire) |
217
221
  | curl / HTTP scripts | HTTPS → primitive Edge Functions | Cerefox access token (`cfx_pat_…`) | Ad-hoc queries, automation |
218
222
  | Web UI (`cerefox web`) | Supabase REST API | Secret key (or legacy service_role) | Web UI backend (TS Hono) |
219
223
  | **HTTP client → `/api/v1`** | Plain HTTP to `cerefox web` / Cerefox Local | **None from loopback; API key otherwise** (v1.12.0) — `cerefox api-key generate` / `cerefox-local api-key` | A local program or bot harness that wants HTTP rather than MCP. Names itself with `X-Cerefox-Author` / `X-Cerefox-Requestor` (v1.11.0): a declared label for attribution, not a verified identity. See [`api.md`](api.md) |
@@ -179,7 +179,7 @@ default; Cerefox Local picks its own port and `cerefox-local status` prints it).
179
179
 
180
180
  | Method + path | Purpose |
181
181
  |---|---|
182
- | `GET /version` | Server version and environment label. |
182
+ | `GET /version` | Server version, environment label, and the newest release on npm as the server last checked it (`latest`, plus `update_command` when it is newer). |
183
183
  | `GET /schema-version` | Deployed schema version. |
184
184
  | `GET /openapi.json` | This API's OpenAPI 3.1 description, served by the version you are talking to. Every `/api/v1` response points at it with `Link: </api/v1/openapi.json>; rel="service-desc"` (RFC 8631). |
185
185
  | `GET /search?q=…` | Hybrid search (FTS + semantic). `&review_status=approved\|pending_review` filters server-side; `400` while the review workflow is off. The response's `embedder` (`openai` or `local`) says which scale the scores are on. |
@@ -278,7 +278,9 @@ cerefox document set-metadata [OPTIONS] DOCUMENT_ID
278
278
  | `-s, --set <key=value...>` | Set a key. Repeatable. |
279
279
  | `-r, --remove <key...>` | Remove a key. Repeatable. |
280
280
  | `--json <object>` | A JSON object of keys to set; a `null` value removes that key. |
281
- | `--replace` | Set the metadata to **exactly** what was given, discarding every key not listed. |
281
+ | `--replace` | Set the metadata to **exactly** what was given, discarding every key not listed. Needs `--expected-metadata-revision` or `--last-write-wins` (v1.19.0). |
282
+ | `--expected-metadata-revision <n>` | The `metadata_revision` you read (`document get` shows it). The write fails if the metadata changed since. Optional on a merge, required with `--replace`. |
283
+ | `--last-write-wins` | Replace without a revision, regardless of concurrent changes (recorded in the audit log). |
282
284
  | `-a, --author <name>` | Caller identity (audit log). |
283
285
  | `--author-type <type>` | `user` (default) or `agent`. |
284
286
  | `--json-out` | Emit the result as JSON. |
@@ -773,11 +775,11 @@ These flat commands handle install, configuration, and health. Run any with `--h
773
775
  | Command | Purpose |
774
776
  |---|---|
775
777
  | `cerefox init` | Interactive first-run setup; writes `~/.cerefox/.env`, offers `server deploy` + self-docs ingest. |
776
- | `cerefox doctor` | Diagnose the install (credentials, DB reachability, schema version, whether the review workflow is on). |
778
+ | `cerefox doctor` | Diagnose the install (credentials, DB reachability, schema version, whether the review workflow is on, whether a newer release is out). |
777
779
  | `cerefox status` | Show connection + schema status. |
778
780
  | `cerefox configure-agent --tool <client>` | Write MCP client config (`claude-code`, `claude-desktop`, `cursor`, `codex`, `gemini`). |
779
781
  | `cerefox token generate` / `rotate` / `list` | Manage the Cerefox access token (`cfx_pat_…`) — the Edge Function Bearer credential (remote MCP, GPT Actions, curl). See the [`cerefox token`](#cerefox-token-generate--cerefox-token-rotate--cerefox-token-list) section above. |
780
- | `cerefox self-update` | Update the installed `@cerefox/memory` package. |
782
+ | `cerefox self-update` | Update the installed `@cerefox/memory` package. Other commands print a one-line notice when a newer release is out; see [configuration.md → Update check](configuration.md#update-check) to turn it off. |
781
783
  | `cerefox completion` | Emit a shell completion script. |
782
784
  | `cerefox backup create` / `cerefox backup restore` | File-system backup / restore of the knowledge base (see [`ops-scripts.md`](ops-scripts.md)). |
783
785
 
@@ -183,7 +183,7 @@ This handles intermittent OpenAI API errors (500s) that would otherwise cause se
183
183
  | `CEREFOX_MIN_TERM_COVERAGE` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set min_term_coverage <value>`, or the **Settings** page. A per-call override still works (`cerefox search --min-term-coverage`, the MCP `min_term_coverage` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
184
184
  | `CEREFOX_EMBED_MAX_INPUT_CHARS` | `20000` | Safety cap on the characters sent to the embedding model per input. The full chunk content is always stored and reconstructed untouched; only the embedding uses the (rare) truncated prefix, so an oversized chunk can never fail an ingest. |
185
185
  | `CEREFOX_MODELS_DIR` | `~/.cerefox/models` (in-container: inside the data volume) | Where the local embedder caches downloaded model weights (Cerefox Local; `CEREFOX_EMBEDDER=local`). |
186
- | `CEREFOX_ONNX_BATCH` | `4` | Texts per local-embedder inference call. Peak memory scales with this; the small default keeps ingest/reindex safe on small Docker VMs. |
186
+ | `CEREFOX_ONNX_BATCH` | `1` | Texts per local-embedder inference call. Peak memory grows with this (each batch is padded to its longest chunk), and on CPU a larger batch is not faster, so leave it at 1. Was 4 before v1.19.0 (#335). Cerefox Local forwards it from its `.env` (from v1.19.0; after upgrading, run `cerefox-local init` once for a value set there to apply). |
187
187
 
188
188
  ### Metadata filter
189
189
 
@@ -344,6 +344,27 @@ there matters.
344
344
 
345
345
  ---
346
346
 
347
+ ## Update check
348
+
349
+ Cerefox tells you when a newer release is on npm (v1.18.0, #323): one line after
350
+ a CLI command (at most once a day, only at a terminal), an `updates` row in
351
+ `cerefox doctor`, and a note in the web UI footer. Inside the Cerefox Local
352
+ container it points at `cerefox-local upgrade` instead of `cerefox self-update`.
353
+
354
+ The only network call is a GET of the `latest` release from the npm registry
355
+ (`registry.npmjs.org`), the same request `cerefox self-update` makes. It is made
356
+ by `cerefox web` (at startup, then daily), `cerefox mcp` (at startup, when the
357
+ last answer is over a day old), `cerefox doctor` and `cerefox self-update`.
358
+ Every other command only reads the saved answer, `update-check.json` in
359
+ `~/.cerefox` (or in `CEREFOX_CONFIG_DIR`, when set), so it never adds latency.
360
+ Offline, the check stays quiet.
361
+
362
+ | Variable | Default | Description |
363
+ |----------|---------|-------------|
364
+ | `CEREFOX_NO_UPDATE_CHECK` | _(unset)_ | Set to `1` to turn the check off entirely: no request to npm, and no notice anywhere. `NO_UPDATE_NOTIFIER` (the convention other CLIs use) and `CI` do the same. |
365
+
366
+ ---
367
+
347
368
  ## Logging
348
369
 
349
370
  | Variable | Default | Description |
@@ -422,7 +443,7 @@ cerefox server deploy --functions-only
422
443
 
423
444
  Contributors with a repo clone can instead redeploy individual functions with `npx supabase functions deploy cerefox-search` (and `cerefox-ingest`), or through the Supabase Dashboard → Edge Functions → Deploy.
424
445
 
425
- > **If you only use the local MCP server** (Claude Desktop, ChatGPT Desktop, Cursor), Step 3 is
446
+ > **If you only use the local MCP server** (Claude Code, Claude Desktop, Codex, Cursor), Step 3 is
426
447
  > optional — the Edge Functions are only used for GPT Actions and direct HTTP access.
427
448
 
428
449
  > **Future improvement**: the Edge Functions will be updated to read model config from Supabase