@cerefox/memory 1.18.0 → 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.
- package/AGENT_GUIDE.md +8 -3
- package/AGENT_QUICK_REFERENCE.md +3 -3
- package/dist/bin/cerefox.js +846 -657
- package/dist/frontend/assets/index-01k5C3sG.css +1 -0
- package/dist/frontend/assets/index-HNs8rgY1.js +121 -0
- package/dist/frontend/assets/index-HNs8rgY1.js.map +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/embeddings/onnx-embedder.ts +25 -9
- package/dist/server-assets/_shared/mcp-tools/_document-meta.ts +24 -2
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +92 -0
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +6 -2
- package/dist/server-assets/_shared/mcp-tools/audit-ops.ts +27 -0
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +11 -1
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +55 -17
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +3 -1
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +8 -13
- package/dist/server-assets/_shared/mcp-tools/search.ts +11 -2
- package/dist/server-assets/_shared/mcp-tools/set-document-metadata.ts +31 -5
- package/dist/server-assets/db/migrations/0035_drop_content_hash_unique.sql +21 -0
- package/dist/server-assets/db/migrations/0036_metadata_revision.sql +43 -0
- package/dist/server-assets/db/rpcs.sql +252 -53
- package/dist/server-assets/db/schema.sql +41 -4
- package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +8 -1
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +42 -6
- package/docs/api/openapi.json +61 -1
- package/docs/guides/cli.md +3 -1
- package/docs/guides/configuration.md +1 -1
- package/docs/guides/connect-agents.md +18 -7
- package/docs/guides/ops-scripts.md +1 -1
- package/docs/guides/setup-local.md +30 -6
- package/docs/guides/upgrading.md +14 -0
- package/package.json +6 -6
- package/dist/frontend/assets/index-B0H_N4if.css +0 -1
- package/dist/frontend/assets/index-CxMT7hjH.js +0 -121
- package/dist/frontend/assets/index-CxMT7hjH.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.
|
|
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
|
|
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).
|
|
@@ -289,7 +301,7 @@ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_version
|
|
|
289
301
|
CREATE INDEX IF NOT EXISTS idx_cerefox_docs_metadata
|
|
290
302
|
ON cerefox_documents USING GIN(metadata);
|
|
291
303
|
|
|
292
|
-
-- Documents by content_hash (
|
|
304
|
+
-- Documents by content_hash (the create-time duplicate check and hash lookups)
|
|
293
305
|
CREATE INDEX IF NOT EXISTS idx_cerefox_docs_hash
|
|
294
306
|
ON cerefox_documents(content_hash);
|
|
295
307
|
|
|
@@ -344,6 +356,31 @@ CREATE OR REPLACE TRIGGER trig_cerefox_chunks_updated
|
|
|
344
356
|
BEFORE UPDATE ON cerefox_chunks
|
|
345
357
|
FOR EACH ROW EXECUTE FUNCTION cerefox_set_updated_at();
|
|
346
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
|
+
|
|
347
384
|
|
|
348
385
|
-- ── Live-database migration ───────────────────────────────────────────────────
|
|
349
386
|
-- When applying this schema to an existing database that still has the old
|
|
@@ -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
|
-
//
|
|
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("
|
|
79
|
+
if (message.includes("CEREFOX_METADATA_CONFLICT")) {
|
|
77
80
|
return new Response(
|
|
78
81
|
JSON.stringify({
|
|
79
|
-
error: "
|
|
82
|
+
error: "metadata_conflict",
|
|
80
83
|
message:
|
|
81
|
-
"
|
|
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 },
|
package/docs/api/openapi.json
CHANGED
|
@@ -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"
|
package/docs/guides/cli.md
CHANGED
|
@@ -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. |
|
|
@@ -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` | `
|
|
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
|
|
|
@@ -661,7 +661,7 @@ In the action editor, paste this schema (replace `<your-project-ref>`):
|
|
|
661
661
|
openapi: 3.1.0
|
|
662
662
|
info:
|
|
663
663
|
title: Cerefox Knowledge Base
|
|
664
|
-
version: 4.
|
|
664
|
+
version: 4.4.0
|
|
665
665
|
servers:
|
|
666
666
|
- url: https://<your-project-ref>.supabase.co/functions/v1
|
|
667
667
|
paths:
|
|
@@ -793,9 +793,10 @@ paths:
|
|
|
793
793
|
metadata:
|
|
794
794
|
type: object
|
|
795
795
|
description: >
|
|
796
|
-
Arbitrary JSON metadata. On an UPDATE
|
|
797
|
-
|
|
798
|
-
|
|
796
|
+
Arbitrary JSON metadata. On an UPDATE it is MERGED into the
|
|
797
|
+
existing metadata (v4.4.0): keys you pass are set, a null
|
|
798
|
+
value removes that key, keys you omit are kept. Omitting
|
|
799
|
+
the field keeps the metadata as it is.
|
|
799
800
|
update_if_exists:
|
|
800
801
|
type: boolean
|
|
801
802
|
default: false
|
|
@@ -813,6 +814,13 @@ paths:
|
|
|
813
814
|
If the document changed since it was read, the update fails
|
|
814
815
|
with HTTP 409 — re-read the document, merge your changes,
|
|
815
816
|
retry with the new hash. Not needed when creating.
|
|
817
|
+
expected_metadata_revision:
|
|
818
|
+
type: integer
|
|
819
|
+
description: >
|
|
820
|
+
Optional, with metadata on an update (v4.4.0): the
|
|
821
|
+
metadata_revision returned by getDocument. If the metadata
|
|
822
|
+
changed since, the update fails with HTTP 409
|
|
823
|
+
(error "metadata_conflict").
|
|
816
824
|
last_write_wins:
|
|
817
825
|
type: boolean
|
|
818
826
|
default: false
|
|
@@ -862,7 +870,9 @@ paths:
|
|
|
862
870
|
changes, and retry with the new hash — do not overwrite blindly),
|
|
863
871
|
or the target document is soft-deleted (error
|
|
864
872
|
"document_deleted") — restore it first, then retry, or create a
|
|
865
|
-
new document
|
|
873
|
+
new document, or the metadata changed since it was read (error
|
|
874
|
+
"metadata_conflict") — re-read with getDocument and retry with
|
|
875
|
+
the new metadata_revision.
|
|
866
876
|
'422':
|
|
867
877
|
description: >
|
|
868
878
|
Unresolved document links (error "unresolved_links") — the content
|
|
@@ -922,7 +932,8 @@ paths:
|
|
|
922
932
|
description: >
|
|
923
933
|
Document content and metadata:
|
|
924
934
|
{ document_id, doc_title, full_content, chunk_count, total_chars,
|
|
925
|
-
is_archived, version_id, content_hash
|
|
935
|
+
is_archived, version_id, content_hash, metadata,
|
|
936
|
+
metadata_revision }.
|
|
926
937
|
content_hash is the document's CURRENT hash — pass it back as
|
|
927
938
|
expected_content_hash when updating via ingestNote.
|
|
928
939
|
'404':
|
|
@@ -1155,7 +1166,7 @@ curl -s -X POST \
|
|
|
1155
1166
|
}'
|
|
1156
1167
|
```
|
|
1157
1168
|
|
|
1158
|
-
If the same content
|
|
1169
|
+
If a live document already has the same content (SHA-256 hash match), a create returns that document with `"skipped": true` instead of a copy. Documents in the trash do not count, and updates never de-duplicate.
|
|
1159
1170
|
|
|
1160
1171
|
**Edge Function parameters — `cerefox-search`:**
|
|
1161
1172
|
|
|
@@ -177,7 +177,7 @@ Output: `backup-data/cerefox-20260308T143022Z-before-v2-migration.json`
|
|
|
177
177
|
|
|
178
178
|
## backup_restore.ts — Restore from a backup
|
|
179
179
|
|
|
180
|
-
Restores documents and chunks from a previously created backup file. Idempotent —
|
|
180
|
+
Restores documents and chunks from a previously created backup file. Idempotent — a document is skipped when its id is already present, or when a live document outside the backup already has the same content. (End users use `cerefox backup restore`.)
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
bun scripts/backup_restore.ts BACKUP_FILE [OPTIONS]
|
|
@@ -4,9 +4,11 @@ Run Cerefox **fully on your own machine** — no hosted Supabase, no cloud datab
|
|
|
4
4
|
Docker container bundles everything: Postgres + pgvector, the PostgREST Data API, and the
|
|
5
5
|
Cerefox web server. You get the same web UI, CLI, and MCP server as the cloud setup.
|
|
6
6
|
|
|
7
|
-
> **
|
|
8
|
-
>
|
|
9
|
-
>
|
|
7
|
+
> **Two embedding choices.** By default embeddings use the OpenAI API (an
|
|
8
|
+
> `OPENAI_API_KEY` is needed). Or choose the **local embedder**
|
|
9
|
+
> (`--local-embedder`, `CEREFOX_EMBEDDER=local`): a model that runs inside the
|
|
10
|
+
> container, fully offline, no API key. See
|
|
11
|
+
> [Choose your embedder](#choose-your-embedder-openai-vs-fully-local).
|
|
10
12
|
|
|
11
13
|
## Cloud vs. Local — pick one
|
|
12
14
|
|
|
@@ -49,9 +51,14 @@ install; switching later requires a re-index (see below).
|
|
|
49
51
|
The local model (~130 MB) downloads once — at install/init when selected — into the
|
|
50
52
|
data volume, so it survives `cerefox-local upgrade`.
|
|
51
53
|
|
|
52
|
-
> **Memory**:
|
|
53
|
-
>
|
|
54
|
-
>
|
|
54
|
+
> **Memory**: with the local embedder, each Cerefox Local instance's web
|
|
55
|
+
> process sits around **0.3–0.5 GB** idle and reaches about **1.5 GB** while
|
|
56
|
+
> ingesting long documents dense with tables, ids or code (measured on
|
|
57
|
+
> synthetic 20,000-character documents; Postgres and PostgREST add a little
|
|
58
|
+
> more). Give the Docker VM room for **each** instance you run, plus everything
|
|
59
|
+
> else in it: **4 GB is comfortable for one instance**, not two. Colima defaults
|
|
60
|
+
> to 2 GB (`colima start --memory 4`, or `--memory 8` for two instances). If the
|
|
61
|
+
> VM runs out, the kernel kills the web process; see Troubleshooting.
|
|
55
62
|
|
|
56
63
|
> **Switching embedders on existing data is breaking**: the two models produce
|
|
57
64
|
> incompatible vector spaces, so documents embedded with one are invisible to
|
|
@@ -193,6 +200,23 @@ Re-run the installer with the key, or set it and `cerefox-local upgrade`.
|
|
|
193
200
|
|
|
194
201
|
**Port already in use** — re-install with a free port: `PORT=8017 sh -c "$(curl -fsSL …/install-local.sh)"`.
|
|
195
202
|
|
|
203
|
+
**Requests fail with "socket connection closed", or the web process restarts on
|
|
204
|
+
its own** — usually the Docker VM ran out of memory and the kernel killed the
|
|
205
|
+
web process (most often during ingest with the local embedder). s6 restarts it
|
|
206
|
+
within seconds, so the container keeps running. Since v1.19.0 the container log
|
|
207
|
+
says so (`docker logs cerefox-local` shows "killed (SIGKILL), most likely by the
|
|
208
|
+
kernel's out-of-memory killer"); on older versions the log shows only the
|
|
209
|
+
restart. Confirm it in the VM's kernel log:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
colima ssh -- sudo dmesg | grep -i "out of memory" # Colima
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
(Docker Desktop: the same `dmesg` from a privileged container, e.g.
|
|
216
|
+
`docker run --rm --privileged alpine dmesg | grep -i "out of memory"`.) The fix is
|
|
217
|
+
more memory for the VM, or fewer instances in it; see **Memory** above.
|
|
218
|
+
`CEREFOX_ONNX_BATCH` (default 1) trades memory for nothing useful above 1, so leave it.
|
|
219
|
+
|
|
196
220
|
---
|
|
197
221
|
|
|
198
222
|
## Contributor notes
|
package/docs/guides/upgrading.md
CHANGED
|
@@ -194,6 +194,20 @@ knowing about:
|
|
|
194
194
|
document must be restored before its content can be updated.
|
|
195
195
|
- **v1.13.0 — the review workflow is a store setting.** Upgraded stores keep
|
|
196
196
|
it on; fresh installs start with it off. See the callout above.
|
|
197
|
+
- **Schema 0.20.0 — content no longer has to be unique.** Updates that match
|
|
198
|
+
another document, or a document in the trash, are accepted; only creating a
|
|
199
|
+
document de-duplicates, against live documents. Needs `cerefox server deploy`.
|
|
200
|
+
Until the server is deployed, a 1.19.0 client writing content another
|
|
201
|
+
document (trashed ones included) already has gets an error telling it to
|
|
202
|
+
deploy. A client older than 1.19.0 against the new server is safe: when two
|
|
203
|
+
identical creates race, the loser gets an error, as it did before.
|
|
204
|
+
- **Schema 0.21.0 — metadata merges on content updates and has its own token.**
|
|
205
|
+
A content update's `metadata` now merges (`null` removes a key) instead of
|
|
206
|
+
replacing, so `{}` no longer clears tags; `replace` needs
|
|
207
|
+
`expected_metadata_revision`. Scripts that relied on replace-on-update should
|
|
208
|
+
use `document set-metadata --replace`. Needs `cerefox server deploy`; then
|
|
209
|
+
restart MCP clients and `cerefox web`, since an older client's metadata
|
|
210
|
+
`replace` is refused for lack of the token.
|
|
197
211
|
- **v1.13.1 — the flag hides, it does not rewrite.** Agent writes are
|
|
198
212
|
recorded `pending_review` whether or not the workflow is shown (v1.13.0
|
|
199
213
|
stored `approved` while off). Redeploy to pick it up.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Cerefox \u2014 user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"CHANGELOG.md"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@hono/node-server": "^2.1.
|
|
42
|
+
"@hono/node-server": "^2.1.4",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
44
|
-
"@supabase/supabase-js": "^2.117.
|
|
44
|
+
"@supabase/supabase-js": "^2.117.3",
|
|
45
45
|
"cli-progress": "^3.12.0",
|
|
46
46
|
"commander": "^15.0.0",
|
|
47
|
-
"hono": "^4.13.
|
|
47
|
+
"hono": "^4.13.13",
|
|
48
48
|
"mammoth": "^1.13.0",
|
|
49
49
|
"ora": "^9.4.0",
|
|
50
50
|
"picocolors": "^1.0.0",
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"zod": "^4.6.5"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@huggingface/transformers": "^4.3.
|
|
57
|
+
"@huggingface/transformers": "^4.3.1",
|
|
58
58
|
"onnxruntime-node": "^1.21.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/bun": "^1.4.2",
|
|
62
62
|
"@types/cli-progress": "^3.11.5",
|
|
63
|
-
"@types/node": "^24.19.
|
|
63
|
+
"@types/node": "^24.19.1",
|
|
64
64
|
"@types/prompts": "^2.4.9",
|
|
65
65
|
"typescript": "^6.0.3"
|
|
66
66
|
},
|