@cerefox/memory 0.10.3 → 0.11.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 +47 -20
- package/AGENT_QUICK_REFERENCE.md +16 -11
- package/dist/bin/cerefox.js +525 -331
- package/dist/frontend/assets/{index-DVXDQ7__.js → index-ojNhWSxm.js} +3 -3
- package/dist/frontend/assets/index-ojNhWSxm.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +1 -1
- package/dist/server-assets/_shared/mcp-tools/_projects.ts +117 -1
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +6 -2
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +6 -6
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +71 -6
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +20 -12
- package/dist/server-assets/_shared/mcp-tools/search.ts +4 -1
- package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +9 -74
- package/dist/server-assets/db/rpcs.sql +77 -11
- package/dist/server-assets/db/schema.sql +1 -1
- package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +5 -1
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +65 -1
- package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +25 -7
- package/docs/guides/access-paths.md +1 -1
- package/docs/guides/agent-coordination.md +14 -1
- package/docs/guides/cli.md +109 -6
- package/docs/guides/connect-agents.md +51 -7
- package/package.json +1 -1
- package/dist/frontend/assets/index-DVXDQ7__.js.map +0 -1
|
@@ -38,6 +38,13 @@ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOO
|
|
|
38
38
|
DROP FUNCTION IF EXISTS cerefox_fts_search(TEXT, INT, UUID);
|
|
39
39
|
DROP FUNCTION IF EXISTS cerefox_semantic_search(VECTOR(768), INT, BOOLEAN, UUID, FLOAT);
|
|
40
40
|
DROP FUNCTION IF EXISTS cerefox_reconstruct_doc(UUID);
|
|
41
|
+
|
|
42
|
+
-- Iteration 32 (v0.11, optimistic concurrency): content_hash added to the return
|
|
43
|
+
-- types of all document-shaped reads — the writer's concurrency token must be
|
|
44
|
+
-- obtainable from every read surface. Drop the pre-change signatures first.
|
|
45
|
+
DROP FUNCTION IF EXISTS cerefox_get_document(UUID, UUID);
|
|
46
|
+
DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB);
|
|
47
|
+
DROP FUNCTION IF EXISTS cerefox_metadata_search(JSONB, UUID, TIMESTAMPTZ, TIMESTAMPTZ, INT, BOOLEAN, INT);
|
|
41
48
|
DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT);
|
|
42
49
|
|
|
43
50
|
-- Iteration 13: Drop pre-metadata-filter signatures so we can add p_metadata_filter JSONB.
|
|
@@ -592,7 +599,10 @@ RETURNS TABLE (
|
|
|
592
599
|
total_chars INT,
|
|
593
600
|
doc_updated_at TIMESTAMPTZ,
|
|
594
601
|
version_count INT,
|
|
595
|
-
is_partial BOOL
|
|
602
|
+
is_partial BOOL,
|
|
603
|
+
-- Optimistic-concurrency token (iter-32): the document's current
|
|
604
|
+
-- content_hash, to pass back as expected_content_hash on update.
|
|
605
|
+
content_hash TEXT
|
|
596
606
|
)
|
|
597
607
|
LANGUAGE sql
|
|
598
608
|
SECURITY DEFINER
|
|
@@ -625,7 +635,8 @@ AS $$
|
|
|
625
635
|
cr.doc_project_ids,
|
|
626
636
|
cr.doc_project_names,
|
|
627
637
|
cr.version_count,
|
|
628
|
-
d.updated_at AS doc_updated_at
|
|
638
|
+
d.updated_at AS doc_updated_at,
|
|
639
|
+
d.content_hash
|
|
629
640
|
FROM chunk_results cr
|
|
630
641
|
JOIN cerefox_documents d ON d.id = cr.document_id
|
|
631
642
|
ORDER BY cr.document_id, cr.score DESC
|
|
@@ -707,7 +718,8 @@ AS $$
|
|
|
707
718
|
ds.total_chars, -- always full document size, even for partial results
|
|
708
719
|
td.doc_updated_at,
|
|
709
720
|
td.version_count,
|
|
710
|
-
ac.is_partial
|
|
721
|
+
ac.is_partial,
|
|
722
|
+
td.content_hash
|
|
711
723
|
FROM top_docs td
|
|
712
724
|
JOIN doc_sizes ds ON ds.document_id = td.document_id
|
|
713
725
|
JOIN all_content ac ON ac.document_id = td.document_id
|
|
@@ -832,7 +844,11 @@ RETURNS TABLE (
|
|
|
832
844
|
full_content TEXT,
|
|
833
845
|
chunk_count INT,
|
|
834
846
|
total_chars INT,
|
|
835
|
-
created_at TIMESTAMPTZ
|
|
847
|
+
created_at TIMESTAMPTZ,
|
|
848
|
+
-- Current content_hash of the document — the optimistic-concurrency token
|
|
849
|
+
-- to pass back as expected_content_hash on update (iter-32). Note: always
|
|
850
|
+
-- the CURRENT hash, even when an archived version is being retrieved.
|
|
851
|
+
content_hash TEXT
|
|
836
852
|
)
|
|
837
853
|
LANGUAGE sql
|
|
838
854
|
SECURITY DEFINER
|
|
@@ -853,7 +869,8 @@ AS $$
|
|
|
853
869
|
STRING_AGG(c.content, E'\n\n' ORDER BY c.chunk_index) AS full_content,
|
|
854
870
|
COUNT(*)::INT AS chunk_count,
|
|
855
871
|
SUM(c.char_count)::INT AS total_chars,
|
|
856
|
-
d.created_at
|
|
872
|
+
d.created_at,
|
|
873
|
+
d.content_hash
|
|
857
874
|
FROM cerefox_documents d
|
|
858
875
|
JOIN cerefox_chunks c ON c.document_id = d.id
|
|
859
876
|
WHERE d.id = p_document_id
|
|
@@ -861,7 +878,7 @@ AS $$
|
|
|
861
878
|
(p_version_id IS NULL AND c.version_id IS NULL) OR
|
|
862
879
|
(p_version_id IS NOT NULL AND c.version_id = p_version_id)
|
|
863
880
|
)
|
|
864
|
-
GROUP BY d.id, d.title, d.source, d.metadata, d.created_at;
|
|
881
|
+
GROUP BY d.id, d.title, d.source, d.metadata, d.created_at, d.content_hash;
|
|
865
882
|
$$;
|
|
866
883
|
|
|
867
884
|
-- ── cerefox_list_document_versions ────────────────────────────────────────────
|
|
@@ -1037,11 +1054,21 @@ $$;
|
|
|
1037
1054
|
-- p_source_label : version source label for snapshot ('file','paste','agent','manual')
|
|
1038
1055
|
-- p_retention_hours : for version cleanup (default 48)
|
|
1039
1056
|
-- p_cleanup_enabled : whether version cleanup runs (default true)
|
|
1057
|
+
-- p_expected_content_hash : optimistic-concurrency token (iter-32). On the UPDATE
|
|
1058
|
+
-- path this must equal the document's current content_hash —
|
|
1059
|
+
-- the caller proves they based their edit on the live version.
|
|
1060
|
+
-- Mismatch → CEREFOX_CONFLICT (SQLSTATE 40001). Absent (NULL)
|
|
1061
|
+
-- without p_last_write_wins → CEREFOX_TOKEN_REQUIRED (22023).
|
|
1062
|
+
-- Ignored on the CREATE path.
|
|
1063
|
+
-- p_last_write_wins : explicit opt-out of the concurrency check (filesystem-sync
|
|
1064
|
+
-- flows where an external source of truth makes conflicts
|
|
1065
|
+
-- meaningless). Recorded in the audit description when used.
|
|
1040
1066
|
--
|
|
1041
1067
|
-- Returns: document_id, chunk_count, total_chars, operation ('create' or 'update-content'),
|
|
1042
1068
|
-- version_id (UUID of snapshot, null on create)
|
|
1043
1069
|
|
|
1044
1070
|
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN);
|
|
1071
|
+
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT, BOOLEAN);
|
|
1045
1072
|
CREATE FUNCTION cerefox_ingest_document(
|
|
1046
1073
|
p_document_id UUID DEFAULT NULL,
|
|
1047
1074
|
p_title TEXT DEFAULT 'Untitled',
|
|
@@ -1055,7 +1082,9 @@ CREATE FUNCTION cerefox_ingest_document(
|
|
|
1055
1082
|
p_author_type TEXT DEFAULT 'user',
|
|
1056
1083
|
p_source_label TEXT DEFAULT 'manual',
|
|
1057
1084
|
p_retention_hours INT DEFAULT 48,
|
|
1058
|
-
p_cleanup_enabled BOOLEAN DEFAULT TRUE
|
|
1085
|
+
p_cleanup_enabled BOOLEAN DEFAULT TRUE,
|
|
1086
|
+
p_expected_content_hash TEXT DEFAULT NULL,
|
|
1087
|
+
p_last_write_wins BOOLEAN DEFAULT FALSE
|
|
1059
1088
|
)
|
|
1060
1089
|
RETURNS TABLE (
|
|
1061
1090
|
document_id UUID,
|
|
@@ -1075,6 +1104,7 @@ DECLARE
|
|
|
1075
1104
|
v_operation TEXT;
|
|
1076
1105
|
v_version_id UUID := NULL;
|
|
1077
1106
|
v_old_chars INT := 0;
|
|
1107
|
+
v_current_hash TEXT;
|
|
1078
1108
|
v_chunk JSONB;
|
|
1079
1109
|
v_snap RECORD;
|
|
1080
1110
|
v_status TEXT;
|
|
@@ -1114,9 +1144,38 @@ BEGIN
|
|
|
1114
1144
|
v_doc_id := p_document_id;
|
|
1115
1145
|
v_operation := 'update-content';
|
|
1116
1146
|
|
|
1117
|
-
--
|
|
1118
|
-
|
|
1119
|
-
|
|
1147
|
+
-- Lock the row and read its current state. FOR UPDATE makes the
|
|
1148
|
+
-- concurrency check below atomic with the write: two simultaneous
|
|
1149
|
+
-- updaters serialize here, and the second one sees the first one's
|
|
1150
|
+
-- hash — the race window (chunk + embed latency) is closed at the
|
|
1151
|
+
-- only place all transports share (iter-32).
|
|
1152
|
+
SELECT COALESCE(d.total_chars, 0), d.content_hash
|
|
1153
|
+
INTO v_old_chars, v_current_hash
|
|
1154
|
+
FROM cerefox_documents d WHERE d.id = v_doc_id
|
|
1155
|
+
FOR UPDATE;
|
|
1156
|
+
|
|
1157
|
+
IF NOT FOUND THEN
|
|
1158
|
+
RAISE EXCEPTION 'cerefox_ingest_document: document not found: %', v_doc_id
|
|
1159
|
+
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1160
|
+
END IF;
|
|
1161
|
+
|
|
1162
|
+
-- ── Optimistic concurrency check (iter-32) ───────────────────
|
|
1163
|
+
-- Content updates must prove freshness (expected hash) or explicitly
|
|
1164
|
+
-- choose last-write-wins. Message prefixes are machine-detectable:
|
|
1165
|
+
-- transport handlers map them to agent-first retry instructions.
|
|
1166
|
+
IF NOT p_last_write_wins THEN
|
|
1167
|
+
IF p_expected_content_hash IS NULL THEN
|
|
1168
|
+
RAISE EXCEPTION
|
|
1169
|
+
'CEREFOX_TOKEN_REQUIRED: content updates require expected_content_hash (the content_hash you read) or last_write_wins=true. Current hash: %',
|
|
1170
|
+
v_current_hash
|
|
1171
|
+
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1172
|
+
ELSIF p_expected_content_hash <> v_current_hash THEN
|
|
1173
|
+
RAISE EXCEPTION
|
|
1174
|
+
'CEREFOX_CONFLICT: document % changed since it was read (expected hash %, current hash %). Re-read the document, merge your changes, and retry with the new hash.',
|
|
1175
|
+
v_doc_id, p_expected_content_hash, v_current_hash
|
|
1176
|
+
USING ERRCODE = '40001'; -- serialization_failure
|
|
1177
|
+
END IF;
|
|
1178
|
+
END IF;
|
|
1120
1179
|
|
|
1121
1180
|
-- Snapshot old version (archives current chunks, runs retention cleanup)
|
|
1122
1181
|
SELECT sv.version_id INTO v_version_id
|
|
@@ -1183,6 +1242,8 @@ BEGIN
|
|
|
1183
1242
|
p_size_before := CASE WHEN v_operation = 'create' THEN NULL ELSE v_old_chars END,
|
|
1184
1243
|
p_size_after := v_total_chars,
|
|
1185
1244
|
p_description := v_operation || ': ' || p_title || ' (' || v_chunk_count || ' chunks, ' || v_total_chars || ' chars)'
|
|
1245
|
+
|| CASE WHEN p_last_write_wins AND v_operation = 'update-content'
|
|
1246
|
+
THEN ' [last-write-wins]' ELSE '' END
|
|
1186
1247
|
);
|
|
1187
1248
|
|
|
1188
1249
|
RETURN QUERY SELECT v_doc_id, v_chunk_count, v_total_chars, v_operation, v_version_id;
|
|
@@ -1407,6 +1468,9 @@ RETURNS TABLE (
|
|
|
1407
1468
|
project_ids UUID[],
|
|
1408
1469
|
project_names TEXT[],
|
|
1409
1470
|
version_count INT,
|
|
1471
|
+
-- Optimistic-concurrency token (iter-32): pass back as
|
|
1472
|
+
-- expected_content_hash on update.
|
|
1473
|
+
content_hash TEXT,
|
|
1410
1474
|
content TEXT
|
|
1411
1475
|
)
|
|
1412
1476
|
LANGUAGE plpgsql
|
|
@@ -1436,6 +1500,7 @@ BEGIN
|
|
|
1436
1500
|
WHERE dp.document_id = d.id) AS project_names,
|
|
1437
1501
|
(SELECT COUNT(*)::INT FROM cerefox_document_versions dv
|
|
1438
1502
|
WHERE dv.document_id = d.id) AS version_count,
|
|
1503
|
+
d.content_hash,
|
|
1439
1504
|
CASE WHEN p_include_content THEN
|
|
1440
1505
|
(SELECT STRING_AGG(c.content, E'\n\n' ORDER BY c.chunk_index)
|
|
1441
1506
|
FROM cerefox_chunks c
|
|
@@ -1474,6 +1539,7 @@ BEGIN
|
|
|
1474
1539
|
project_ids := v_row.project_ids;
|
|
1475
1540
|
project_names := v_row.project_names;
|
|
1476
1541
|
version_count := v_row.version_count;
|
|
1542
|
+
content_hash := v_row.content_hash;
|
|
1477
1543
|
content := v_row.content;
|
|
1478
1544
|
RETURN NEXT;
|
|
1479
1545
|
END LOOP;
|
|
@@ -1694,7 +1760,7 @@ SET search_path = public, pg_catalog
|
|
|
1694
1760
|
AS $$
|
|
1695
1761
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
1696
1762
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
1697
|
-
SELECT '0.
|
|
1763
|
+
SELECT '0.5.0'::TEXT;
|
|
1698
1764
|
$$;
|
|
1699
1765
|
|
|
1700
1766
|
|
|
@@ -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.5.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 —
|
|
@@ -17,7 +17,7 @@ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/inde
|
|
|
17
17
|
* { document_id: string, version_id?: string | null }
|
|
18
18
|
*
|
|
19
19
|
* Response (200):
|
|
20
|
-
* { document_id, doc_title, full_content, chunk_count, total_chars, is_archived, version_id }
|
|
20
|
+
* { document_id, doc_title, full_content, chunk_count, total_chars, is_archived, version_id, content_hash }
|
|
21
21
|
* Response (404):
|
|
22
22
|
* { error: "Document not found" }
|
|
23
23
|
* Response (400):
|
|
@@ -98,6 +98,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
98
98
|
full_content?: string;
|
|
99
99
|
chunk_count?: number;
|
|
100
100
|
total_chars?: number;
|
|
101
|
+
content_hash?: string;
|
|
101
102
|
} | undefined;
|
|
102
103
|
|
|
103
104
|
if (!row) {
|
|
@@ -125,6 +126,9 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
125
126
|
total_chars: row.total_chars ?? 0,
|
|
126
127
|
is_archived: version_id !== null,
|
|
127
128
|
version_id,
|
|
129
|
+
// Optimistic-concurrency token (iter-32): always the CURRENT hash —
|
|
130
|
+
// pass back as expected_content_hash when updating via ingest.
|
|
131
|
+
content_hash: row.content_hash ?? null,
|
|
128
132
|
}),
|
|
129
133
|
{ status: 200, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
130
134
|
);
|
|
@@ -40,6 +40,42 @@ interface IngestRequest {
|
|
|
40
40
|
update_if_exists?: boolean;
|
|
41
41
|
author?: string;
|
|
42
42
|
author_type?: string; // 'user' | 'agent'
|
|
43
|
+
// Optimistic concurrency (iter-32): REQUIRED on content updates — the
|
|
44
|
+
// content_hash of the version this edit was based on. Conflict → HTTP 409.
|
|
45
|
+
expected_content_hash?: string;
|
|
46
|
+
// Explicitly skip the concurrency check (external source of truth).
|
|
47
|
+
last_write_wins?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Map the RPC's CEREFOX_CONFLICT / CEREFOX_TOKEN_REQUIRED errors to HTTP
|
|
51
|
+
// responses (409 conflict / 400 missing token). Returns null for other errors.
|
|
52
|
+
function concurrencyErrorResponse(
|
|
53
|
+
message: string,
|
|
54
|
+
headers: Record<string, string>,
|
|
55
|
+
): Response | null {
|
|
56
|
+
if (message.includes("CEREFOX_CONFLICT")) {
|
|
57
|
+
return new Response(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
error: "conflict",
|
|
60
|
+
message:
|
|
61
|
+
"Document changed since it was read. Re-read it (getDocument), merge your changes, and retry with the new expected_content_hash.",
|
|
62
|
+
detail: message,
|
|
63
|
+
}),
|
|
64
|
+
{ status: 409, headers },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (message.includes("CEREFOX_TOKEN_REQUIRED")) {
|
|
68
|
+
return new Response(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
error: "expected_content_hash required",
|
|
71
|
+
message:
|
|
72
|
+
"Content updates require expected_content_hash (the content_hash returned by getDocument / searchKnowledgeBase / metadataSearch) or last_write_wins=true.",
|
|
73
|
+
detail: message,
|
|
74
|
+
}),
|
|
75
|
+
{ status: 400, headers },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
interface Chunk {
|
|
@@ -428,7 +464,7 @@ Deno.serve(async (req: Request) => {
|
|
|
428
464
|
});
|
|
429
465
|
}
|
|
430
466
|
|
|
431
|
-
const { title, content, document_id = null, project_name, source = "agent", metadata = {}, update_if_exists = false, author = "agent", author_type = "agent" } = body;
|
|
467
|
+
const { title, content, document_id = null, project_name, source = "agent", metadata = {}, update_if_exists = false, author = "agent", author_type = "agent", expected_content_hash = null, last_write_wins = false } = body;
|
|
432
468
|
|
|
433
469
|
// Validate + normalize project_names if provided (full-set destructive form)
|
|
434
470
|
let project_names: string[] | null = null;
|
|
@@ -525,6 +561,16 @@ Deno.serve(async (req: Request) => {
|
|
|
525
561
|
);
|
|
526
562
|
}
|
|
527
563
|
|
|
564
|
+
// Optimistic-concurrency fast-fail (iter-32): stale token fails BEFORE
|
|
565
|
+
// the embedding spend. Advisory only — the authoritative race-free check
|
|
566
|
+
// is inside the RPC (SELECT … FOR UPDATE).
|
|
567
|
+
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
568
|
+
return concurrencyErrorResponse(
|
|
569
|
+
`CEREFOX_CONFLICT: document ${existingDoc.id} changed since it was read (expected hash ${expected_content_hash}, current hash ${existingDoc.content_hash}).`,
|
|
570
|
+
headers,
|
|
571
|
+
)!;
|
|
572
|
+
}
|
|
573
|
+
|
|
528
574
|
// Content changed -- re-chunk, re-embed, ingest via RPC
|
|
529
575
|
const chunks = chunkMarkdown(content);
|
|
530
576
|
if (chunks.length === 0) {
|
|
@@ -562,9 +608,13 @@ Deno.serve(async (req: Request) => {
|
|
|
562
608
|
p_author: author,
|
|
563
609
|
p_author_type: author_type,
|
|
564
610
|
p_source_label: source,
|
|
611
|
+
p_expected_content_hash: expected_content_hash,
|
|
612
|
+
p_last_write_wins: last_write_wins,
|
|
565
613
|
});
|
|
566
614
|
|
|
567
615
|
if (ingestErr) {
|
|
616
|
+
const mapped = concurrencyErrorResponse(ingestErr.message ?? "", headers);
|
|
617
|
+
if (mapped) return mapped;
|
|
568
618
|
return new Response(JSON.stringify({ error: `Ingest RPC failed: ${ingestErr.message}` }), { status: 500, headers });
|
|
569
619
|
}
|
|
570
620
|
|
|
@@ -593,6 +643,7 @@ Deno.serve(async (req: Request) => {
|
|
|
593
643
|
chunk_count: chunks.length,
|
|
594
644
|
total_chars: totalChars,
|
|
595
645
|
updated: true,
|
|
646
|
+
content_hash: contentHash,
|
|
596
647
|
...(note && { note }),
|
|
597
648
|
}),
|
|
598
649
|
{ headers },
|
|
@@ -625,6 +676,14 @@ Deno.serve(async (req: Request) => {
|
|
|
625
676
|
);
|
|
626
677
|
}
|
|
627
678
|
|
|
679
|
+
// Optimistic-concurrency fast-fail (iter-32) — see ID-based path.
|
|
680
|
+
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
681
|
+
return concurrencyErrorResponse(
|
|
682
|
+
`CEREFOX_CONFLICT: document ${existingDoc.id} changed since it was read (expected hash ${expected_content_hash}, current hash ${existingDoc.content_hash}).`,
|
|
683
|
+
headers,
|
|
684
|
+
)!;
|
|
685
|
+
}
|
|
686
|
+
|
|
628
687
|
// Content changed — re-chunk, re-embed, ingest via RPC
|
|
629
688
|
const chunks = chunkMarkdown(content);
|
|
630
689
|
if (chunks.length === 0) {
|
|
@@ -667,9 +726,13 @@ Deno.serve(async (req: Request) => {
|
|
|
667
726
|
p_author: author,
|
|
668
727
|
p_author_type: author_type,
|
|
669
728
|
p_source_label: source,
|
|
729
|
+
p_expected_content_hash: expected_content_hash,
|
|
730
|
+
p_last_write_wins: last_write_wins,
|
|
670
731
|
});
|
|
671
732
|
|
|
672
733
|
if (ingestErr) {
|
|
734
|
+
const mapped = concurrencyErrorResponse(ingestErr.message ?? "", headers);
|
|
735
|
+
if (mapped) return mapped;
|
|
673
736
|
return new Response(
|
|
674
737
|
JSON.stringify({ error: `Ingest RPC failed: ${ingestErr.message}` }),
|
|
675
738
|
{ status: 500, headers },
|
|
@@ -701,6 +764,7 @@ Deno.serve(async (req: Request) => {
|
|
|
701
764
|
chunk_count: chunks.length,
|
|
702
765
|
total_chars: totalChars,
|
|
703
766
|
updated: true,
|
|
767
|
+
content_hash: contentHash,
|
|
704
768
|
}),
|
|
705
769
|
{ headers },
|
|
706
770
|
);
|
|
@@ -15,8 +15,11 @@ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/inde
|
|
|
15
15
|
* Note: cerefox-mcp calls the RPC directly (not this Edge Function).
|
|
16
16
|
*
|
|
17
17
|
* Request body (JSON):
|
|
18
|
-
* metadata_filter object
|
|
18
|
+
* metadata_filter object optional Key-value pairs (AND semantics)
|
|
19
19
|
* project_id string optional Project UUID filter
|
|
20
|
+
*
|
|
21
|
+
* At least one of metadata_filter / project_id / updated_since / created_since
|
|
22
|
+
* must be supplied (an empty filter + project_id lists that project's docs).
|
|
20
23
|
* updated_since string optional ISO-8601 lower bound for updated_at
|
|
21
24
|
* created_since string optional ISO-8601 lower bound for created_at
|
|
22
25
|
* limit number optional Max results (default: 10)
|
|
@@ -53,13 +56,12 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
53
56
|
const metadata_filter = body.metadata_filter;
|
|
54
57
|
|
|
55
58
|
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Array.isArray(metadata_filter)
|
|
59
|
-
Object.keys(metadata_filter).length === 0
|
|
59
|
+
metadata_filter !== undefined &&
|
|
60
|
+
metadata_filter !== null &&
|
|
61
|
+
(typeof metadata_filter !== "object" || Array.isArray(metadata_filter))
|
|
60
62
|
) {
|
|
61
63
|
return new Response(
|
|
62
|
-
JSON.stringify({ error: "metadata_filter
|
|
64
|
+
JSON.stringify({ error: "metadata_filter must be a JSON object when provided" }),
|
|
63
65
|
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
64
66
|
);
|
|
65
67
|
}
|
|
@@ -67,6 +69,22 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
67
69
|
const project_id = body.project_id ?? null;
|
|
68
70
|
const updated_since = body.updated_since ?? null;
|
|
69
71
|
const created_since = body.created_since ?? null;
|
|
72
|
+
|
|
73
|
+
// metadata_filter is optional, but at least one narrowing criterion is
|
|
74
|
+
// required so this never becomes an unbounded whole-KB dump. An empty
|
|
75
|
+
// filter + project_id lists a project's documents (the RPC's
|
|
76
|
+
// `metadata @> '{}'` matches every row; the project predicate narrows it).
|
|
77
|
+
const has_metadata =
|
|
78
|
+
metadata_filter && typeof metadata_filter === "object" &&
|
|
79
|
+
Object.keys(metadata_filter).length > 0;
|
|
80
|
+
if (!has_metadata && !project_id && !updated_since && !created_since) {
|
|
81
|
+
return new Response(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
error: "Provide at least one of: metadata_filter, project_id, updated_since, or created_since.",
|
|
84
|
+
}),
|
|
85
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
70
88
|
const limit = body.limit ?? 10;
|
|
71
89
|
const include_content = body.include_content ?? false;
|
|
72
90
|
const requested_max_bytes = body.max_bytes;
|
|
@@ -102,7 +120,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
const params: Record<string, unknown> = {
|
|
105
|
-
p_metadata_filter: metadata_filter,
|
|
123
|
+
p_metadata_filter: has_metadata ? metadata_filter : {},
|
|
106
124
|
p_project_id: project_id,
|
|
107
125
|
p_updated_since: updated_since,
|
|
108
126
|
p_created_since: created_since,
|
|
@@ -209,7 +209,7 @@ paths.
|
|
|
209
209
|
| Tier | Operations | Reversible? | Where exposed |
|
|
210
210
|
|---|---|---|---|
|
|
211
211
|
| 1. Reads + soft mutations | search, get, list-*, ingest (create/update), metadata-search, get-audit-log | n/a (reads) / yes (versioned) | All paths — MCP, Edge Functions, CLI, web UI |
|
|
212
|
-
| 2. Soft-destructive | `delete_document` (soft delete to trash), `set_review_status` | yes — restorable via web UI | All paths (CLI: `cerefox document delete`; web UI;
|
|
212
|
+
| 2. Soft-destructive | `delete_document` (soft delete to trash), `set_review_status` | yes — restorable via web UI | All paths (CLI: `cerefox document delete`; web UI; **not** MCP or Edge Functions today) |
|
|
213
213
|
| 3. **Hard-destructive** | `purge_document` (permanent), `restore_document` (un-trash), `set_version_archived` (toggle version retention) | no (purge) / yes (restore, but recovers from a destructive action) | **Web UI only** |
|
|
214
214
|
|
|
215
215
|
### Why purge / restore are web-UI-only
|
|
@@ -33,6 +33,19 @@ The coordination model is **asynchronous and knowledge-based**:
|
|
|
33
33
|
|
|
34
34
|
This is not real-time orchestration. It is persistent, searchable shared memory.
|
|
35
35
|
|
|
36
|
+
### Concurrent writers are conflict-guarded (v0.11+)
|
|
37
|
+
|
|
38
|
+
Shared memory means two agents can hold the same document at once. Cerefox
|
|
39
|
+
protects content updates with **optimistic concurrency control**: every read
|
|
40
|
+
surface returns the document's `content_hash`, and a content update must pass
|
|
41
|
+
it back as `expected_content_hash`. If the document changed in between, the
|
|
42
|
+
write fails with a **conflict** instead of silently overwriting the other
|
|
43
|
+
writer's work — the losing agent re-reads, merges, and retries with the fresh
|
|
44
|
+
hash. (Versioning remains the recovery net; the conflict guard is the
|
|
45
|
+
prevention layer.) An explicit `last_write_wins: true` exists for re-sync flows
|
|
46
|
+
where an external source of truth makes conflicts meaningless — agents should
|
|
47
|
+
never use it to silence a conflict. See `AGENT_GUIDE.md → Concurrent writers`.
|
|
48
|
+
|
|
36
49
|
---
|
|
37
50
|
|
|
38
51
|
## Coordination Patterns
|
|
@@ -53,7 +66,7 @@ A living document where agents record decisions, experiment outcomes, and lesson
|
|
|
53
66
|
|
|
54
67
|
**Example**: A coding agent working on a project records "Chose PostgreSQL RPC approach over application-level logic because..." in a decision log document. Next week, a different agent working on a related feature searches Cerefox, finds the decision log, and understands the rationale without re-deriving it.
|
|
55
68
|
|
|
56
|
-
**How it works**: Create a document with a structured format (date, context, decision, outcome). Use a consistent title or project tag so agents can find it.
|
|
69
|
+
**How it works**: Create a document with a structured format (date, context, decision, outcome). Use a consistent title or project tag so agents can find it. To add entries over time, re-ingest with `update_if_exists: true` (or `document_id`) — this replaces the document in place, so build the new full content by appending to the prior content you fetched, and pass the `content_hash` you fetched as `expected_content_hash` (two agents appending entries concurrently is exactly the conflict the guard catches).
|
|
57
70
|
|
|
58
71
|
**Best for**: Project-level institutional memory, avoiding repeated decisions, onboarding new agent sessions.
|
|
59
72
|
|
package/docs/guides/cli.md
CHANGED
|
@@ -46,6 +46,8 @@ cerefox document ingest --paste --title "<title>" [OPTIONS] # stdin
|
|
|
46
46
|
| `--metadata` | `-m` | JSON | `{}` | Extra metadata as a JSON object, e.g. `'{"tags":["work"]}'`. |
|
|
47
47
|
| `--update-if-exists` | `-u` | flag | off | Title/source-path-based fallback update. Mutually exclusive with `--document-id`. |
|
|
48
48
|
| `--document-id` | `-i` | UUID | _none_ | Deterministic ID-based update. Errors if the document doesn't exist. |
|
|
49
|
+
| `--expected-content-hash` | — | sha256 | _none_ | **Required on content updates** (v0.11 optimistic concurrency): the `content_hash` of the version this edit is based on, shown by `cerefox document get` / `cerefox search`. Stale → conflict error (re-read, merge, retry). |
|
|
50
|
+
| `--last-write-wins` | — | flag | off | Skip the concurrency check and overwrite regardless of concurrent changes. For re-sync flows where an external source of truth makes conflicts meaningless. Recorded in the audit log. |
|
|
49
51
|
| `--source` | — | str | `paste` / `file` | Source label recorded on the document. |
|
|
50
52
|
| `--author` | — | str | `CEREFOX_AUTHOR_NAME` or `unknown` | Audit-log author identity. |
|
|
51
53
|
| `--author-type` | — | `user`\|`agent` | `CEREFOX_AUTHOR_TYPE` or `user` | Caller type. Agent writes auto-routed to `pending_review`. |
|
|
@@ -63,12 +65,19 @@ cerefox document ingest notes.md \
|
|
|
63
65
|
--author "claude-code" --author-type "agent" \
|
|
64
66
|
--project-name "research" --metadata '{"type":"design-doc"}'
|
|
65
67
|
|
|
66
|
-
# Deterministic update (preferred — agents should search → grab ID → ingest)
|
|
68
|
+
# Deterministic update (preferred — agents should search → grab ID + hash → ingest)
|
|
67
69
|
cerefox document ingest --paste --title "Same Title" \
|
|
68
70
|
--document-id "abc12345-..." \
|
|
71
|
+
--expected-content-hash "<hash from `document get`>" \
|
|
69
72
|
--author "claude-code" --author-type "agent"
|
|
70
73
|
```
|
|
71
74
|
|
|
75
|
+
> **Concurrency (v0.11+)**: content updates require `--expected-content-hash`
|
|
76
|
+
> (or an explicit `--last-write-wins`). On a conflict, re-run
|
|
77
|
+
> `cerefox document get <id>`, merge your changes into the latest content, and
|
|
78
|
+
> retry with the new hash. `document ingest-dir` and `guides ingest` bypass the
|
|
79
|
+
> check internally (the filesystem / npm package is their source of truth).
|
|
80
|
+
|
|
72
81
|
**Output**: human-readable summary line(s) — "Ingested" or "Updated" with the document ID, chunk count, character count.
|
|
73
82
|
|
|
74
83
|
**Exit codes**: `0` success, `1` on validation error (missing `--title`, invalid JSON, document-not-found, mutually-exclusive flags, etc.).
|
|
@@ -183,7 +192,7 @@ cerefox document get abc12345-... --version-id <version-uuid> # archived
|
|
|
183
192
|
cerefox document get abc12345-... | bat -l md # pipe to viewer
|
|
184
193
|
```
|
|
185
194
|
|
|
186
|
-
**Output**: title + metadata line, blank line, then raw markdown.
|
|
195
|
+
**Output**: title + metadata line + `content_hash` line (the optimistic-concurrency token — pass back via `document ingest --expected-content-hash` when updating), blank line, then raw markdown.
|
|
187
196
|
|
|
188
197
|
**MCP equivalent**: [`cerefox_get_document`](../../AGENT_GUIDE.md).
|
|
189
198
|
|
|
@@ -207,7 +216,9 @@ cerefox document list [OPTIONS]
|
|
|
207
216
|
| `--deleted` | flag | off | List soft-deleted (trashed) documents instead of active ones, newest-deleted first. Pair the ids with `cerefox document restore` / `cerefox document delete`. |
|
|
208
217
|
| `--json` | flag | off | Machine-readable JSON output. |
|
|
209
218
|
|
|
210
|
-
**Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`).
|
|
219
|
+
**Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`).
|
|
220
|
+
|
|
221
|
+
**MCP equivalent**: scope-by-project / metadata / time listing maps to [`cerefox_metadata_search`](../../AGENT_GUIDE.md) — e.g. `cerefox_metadata_search(project_name="research")` lists that project's documents (the `metadata_filter` may be empty when another scope is supplied). The `--deleted` (trash) view and unscoped whole-KB listing remain CLI-only.
|
|
211
222
|
|
|
212
223
|
---
|
|
213
224
|
|
|
@@ -240,6 +251,43 @@ cerefox document edit <doc-id> --set-meta status=archived --unset-meta draft
|
|
|
240
251
|
|
|
241
252
|
---
|
|
242
253
|
|
|
254
|
+
### `cerefox document set-projects`
|
|
255
|
+
|
|
256
|
+
**Purpose**: replace a document's project memberships with **exactly** the given set (full-set replace — any project not listed is removed). This is the CLI equivalent of the `cerefox_set_document_projects` MCP tool; both share one membership-replace core, so they behave identically. Content is untouched; the change is logged as an `update-metadata` audit entry.
|
|
257
|
+
|
|
258
|
+
**Synopsis**:
|
|
259
|
+
```
|
|
260
|
+
cerefox document set-projects [OPTIONS] DOCUMENT_ID [PROJECT_NAMES...]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Options**:
|
|
264
|
+
|
|
265
|
+
| Flag | Type | Default | Description |
|
|
266
|
+
|---|---|---|---|
|
|
267
|
+
| `[project-names...]` | variadic args | _none_ | One or more project names. Each is created if missing; order preserved; case-insensitively de-duplicated. |
|
|
268
|
+
| `--clear` | flag | off | Remove the document from **all** projects. Mutually exclusive with passing names. |
|
|
269
|
+
| `--author <name>` (`-a`) | str | `CEREFOX_AUTHOR_NAME` or `unknown` | Identity recorded in the audit log. |
|
|
270
|
+
| `--author-type <type>` | `user`\|`agent` | `user` | Caller type recorded in the audit log. |
|
|
271
|
+
|
|
272
|
+
To set memberships **and** update content in one shot, use `cerefox document ingest --document-id <id> --project-name …` instead. Use this command when you only need to change membership.
|
|
273
|
+
|
|
274
|
+
**Examples**:
|
|
275
|
+
```bash
|
|
276
|
+
# Set the document to belong to exactly these two projects (replaces any others)
|
|
277
|
+
cerefox document set-projects <doc-id> research archive
|
|
278
|
+
|
|
279
|
+
# Remove the document from all projects
|
|
280
|
+
cerefox document set-projects <doc-id> --clear
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Output**: a confirmation line with the document title and the resulting project set (or a "cleared all memberships" line), plus a reminder that the previous set was replaced.
|
|
284
|
+
|
|
285
|
+
**Exit codes**: `0` on success; `1` on validation error (no names and no `--clear`, or both) or if the document is missing / soft-deleted.
|
|
286
|
+
|
|
287
|
+
**MCP equivalent**: [`cerefox_set_document_projects`](../../AGENT_GUIDE.md).
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
243
291
|
### `cerefox document restore`
|
|
244
292
|
|
|
245
293
|
**Purpose**: restore a soft-deleted (trashed) document back to active.
|
|
@@ -594,15 +642,64 @@ Every MCP parameter has an exact-name CLI flag (kebab-cased). Short forms exist
|
|
|
594
642
|
| MCP tool | CLI command |
|
|
595
643
|
|---|---|
|
|
596
644
|
| `cerefox_search(query, match_count, project_name, metadata_filter, requestor)` | `cerefox search "<q>" --match-count N --project-name <name> --metadata-filter '<json>' --requestor <name>` |
|
|
597
|
-
| `cerefox_ingest(title, content, project_name, metadata, update_if_exists, document_id, source, author, author_type)` (file) | `cerefox document ingest <path> --title <t> --project-name <n> --metadata '<json>' --update-if-exists\|--document-id <uuid> --source <s> --author <a> --author-type <t>` |
|
|
645
|
+
| `cerefox_ingest(title, content, project_name, metadata, update_if_exists, document_id, expected_content_hash, last_write_wins, source, author, author_type)` (file) | `cerefox document ingest <path> --title <t> --project-name <n> --metadata '<json>' --update-if-exists\|--document-id <uuid> --expected-content-hash <hash>\|--last-write-wins --source <s> --author <a> --author-type <t>` |
|
|
598
646
|
| `cerefox_ingest(...)` (paste) | `printf '...' \| cerefox document ingest --paste --title "<t>"` (same flags) |
|
|
599
647
|
| `cerefox_get_document(document_id, version_id, requestor)` | `cerefox document get <id> --version-id <vid> --requestor <name>` |
|
|
600
648
|
| `cerefox_list_versions(document_id, requestor)` | `cerefox document version list <id> --requestor <name>` |
|
|
601
649
|
| `cerefox_list_projects(requestor)` | `cerefox project list --requestor <name>` |
|
|
650
|
+
| `cerefox_set_document_projects(document_id, project_names, author)` | `cerefox document set-projects <id> <name...> --author <a> --author-type <t>` (or `--clear` to remove all) |
|
|
602
651
|
| `cerefox_list_metadata_keys()` | `cerefox metadata keys` |
|
|
603
652
|
| `cerefox_metadata_search(metadata_filter, project_name, updated_since, created_since, limit, include_content, requestor)` | `cerefox metadata search --metadata-filter '<json>' --project-name <n> --updated-since <iso> --created-since <iso> --limit N --include-content --requestor <name>` |
|
|
604
653
|
| `cerefox_get_audit_log(document_id, author, operation, since, until, limit, requestor)` | `cerefox audit list --document-id <id> --author <a> --operation <op> --since <iso> --until <iso> --limit N --requestor <name>` |
|
|
605
654
|
|
|
655
|
+
## CLI ↔ MCP parity matrix
|
|
656
|
+
|
|
657
|
+
The table above is MCP-first (it lists the tools that *have* a CLI form). This
|
|
658
|
+
one is **CLI-first** — every `cerefox` command, with its MCP equivalent or an
|
|
659
|
+
explicit reason it has none. It exists to make parity gaps visible. Legend:
|
|
660
|
+
**✅ mapped** · **⚠️ gap** (a capability one surface has and the other lacks,
|
|
661
|
+
arguably worth closing) · **🔒 intentional** (deliberately not on the MCP/agent
|
|
662
|
+
surface).
|
|
663
|
+
|
|
664
|
+
| CLI command | MCP equivalent | Status |
|
|
665
|
+
|---|---|---|
|
|
666
|
+
| `document ingest` | `cerefox_ingest` | ✅ |
|
|
667
|
+
| `document ingest-dir` | — (agents loop `cerefox_ingest`) | 🔒 bulk filesystem walk; no server-side dir access from MCP |
|
|
668
|
+
| `search` | `cerefox_search` | ✅ (CLI adds `--mode`/`--alpha`/`--min-score`/`--only-metadata`) |
|
|
669
|
+
| `document get` | `cerefox_get_document` | ✅ |
|
|
670
|
+
| `document list` | `cerefox_metadata_search` (scope by `project_name` / metadata / time) | ✅ as of this change. Unscoped whole-KB listing has no MCP path by design (scope it) |
|
|
671
|
+
| `document edit` (title / metadata in place) | — | 🔒 intentional: a human/web-parity convenience. Agents update title+metadata deterministically via `cerefox_ingest` (with `document_id`); a metadata-only edit isn't a needed agent primitive |
|
|
672
|
+
| `document delete` (soft-delete) | — | 🔒 destructive; trust model keeps delete/restore on CLI + web only |
|
|
673
|
+
| `document restore` | — | 🔒 trust model (CLI + web only) |
|
|
674
|
+
| `document version list` | `cerefox_list_versions` | ✅ |
|
|
675
|
+
| `document version archive` / `unarchive` | — | 🔒 intentional: version-retention protection is exposed only to CLI + web (a maintenance concern, not an agent primitive) |
|
|
676
|
+
| `document set-projects` | `cerefox_set_document_projects` | ✅ full-set replace of a document's project memberships (shared core; `--clear` to remove all) |
|
|
677
|
+
| `project list` | `cerefox_list_projects` | ✅ |
|
|
678
|
+
| `project create` / `edit` / `delete` | — | 🔒 project mutations CLI + web only |
|
|
679
|
+
| `metadata keys` | `cerefox_list_metadata_keys` | ✅ |
|
|
680
|
+
| `metadata search` | `cerefox_metadata_search` | ✅ |
|
|
681
|
+
| `audit list` | `cerefox_get_audit_log` | ✅ |
|
|
682
|
+
| `guides list` / `show` / `open` / `ingest` | `cerefox_get_help` (partial) | ✅~ `get_help` returns the bundled quick-reference; `guides` is the richer CLI form |
|
|
683
|
+
| `server deploy` / `server reindex` | — | 🔒 operator/deploy surface |
|
|
684
|
+
| `config list` / `get` / `set` | — | 🔒 runtime config; operator surface |
|
|
685
|
+
| `web` / `mcp` | — | 🔒 lifecycle (`mcp` *is* the MCP server) |
|
|
686
|
+
| `init` / `doctor` / `status` / `configure-agent` / `self-update` / `completion` / `backup *` | — | 🔒 install / health / ops |
|
|
687
|
+
|
|
688
|
+
**Gap status** (the 🔒 rows are deliberate and out of scope):
|
|
689
|
+
|
|
690
|
+
1. `document list` → **closed**: project/metadata/time-scoped listing now routes
|
|
691
|
+
through `cerefox_metadata_search` (it accepts an empty `metadata_filter` when
|
|
692
|
+
another scope is supplied).
|
|
693
|
+
2. `cerefox_set_document_projects` → **closed**: added `cerefox document
|
|
694
|
+
set-projects` (full-set replace, `--clear` to remove all), sharing the
|
|
695
|
+
membership-replace core with the MCP tool.
|
|
696
|
+
3. `document edit` (metadata/title-only edit) → **intentional non-gap**: a
|
|
697
|
+
human/web-parity convenience; agents use `cerefox_ingest` for content+metadata
|
|
698
|
+
updates. Revisit only if a concrete agent workflow needs metadata-only edits.
|
|
699
|
+
4. `document version archive` / `unarchive` → **intentional non-gap**: version-retention
|
|
700
|
+
protection is exposed only to CLI + web (a maintenance concern, deliberately not on
|
|
701
|
+
the MCP/agent surface).
|
|
702
|
+
|
|
606
703
|
## Known issues
|
|
607
704
|
|
|
608
705
|
None outstanding as of v0.1.17 (cerefox#27 — the `cerefox search` NameError — is resolved). When new bugs surface, they are tracked in the GitHub issues list; check there before relying on a behaviour the docs imply.
|
|
@@ -621,12 +718,18 @@ cerefox document ingest-dir ./papers --extensions .md \
|
|
|
621
718
|
# Step 1: find it
|
|
622
719
|
cerefox search "the OAuth design doc" --match-count 1
|
|
623
720
|
|
|
624
|
-
# Step 2:
|
|
625
|
-
|
|
721
|
+
# Step 2: read it — note the id AND the `content_hash:` line (the concurrency token)
|
|
722
|
+
cerefox document get "<uuid>"
|
|
723
|
+
|
|
724
|
+
# Step 3: update in place, proving freshness with the hash from step 2
|
|
626
725
|
printf '%s' "$NEW_CONTENT" | cerefox document ingest --paste \
|
|
627
726
|
--title "OAuth 2.1 Design Document" \
|
|
628
727
|
--document-id "<uuid>" \
|
|
728
|
+
--expected-content-hash "<hash>" \
|
|
629
729
|
--author "claude-code" --author-type "agent"
|
|
730
|
+
|
|
731
|
+
# On a conflict error: repeat from step 2 (fresh content + fresh hash),
|
|
732
|
+
# merge your changes into the latest content, then retry.
|
|
630
733
|
```
|
|
631
734
|
|
|
632
735
|
### Unattended sync job
|