@cerefox/memory 0.10.4 → 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 +37 -14
- package/AGENT_QUICK_REFERENCE.md +13 -8
- package/dist/bin/cerefox.js +179 -59
- 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/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 +3 -1
- package/dist/server-assets/_shared/mcp-tools/search.ts +4 -1
- 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/docs/guides/agent-coordination.md +14 -1
- package/docs/guides/cli.md +20 -5
- package/docs/guides/connect-agents.md +40 -3
- package/package.json +1 -1
- package/dist/frontend/assets/index-DVXDQ7__.js.map +0 -1
|
@@ -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
|
);
|
|
@@ -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. 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.
|
|
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
|
|
|
@@ -633,7 +642,7 @@ Every MCP parameter has an exact-name CLI flag (kebab-cased). Short forms exist
|
|
|
633
642
|
| MCP tool | CLI command |
|
|
634
643
|
|---|---|
|
|
635
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>` |
|
|
636
|
-
| `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>` |
|
|
637
646
|
| `cerefox_ingest(...)` (paste) | `printf '...' \| cerefox document ingest --paste --title "<t>"` (same flags) |
|
|
638
647
|
| `cerefox_get_document(document_id, version_id, requestor)` | `cerefox document get <id> --version-id <vid> --requestor <name>` |
|
|
639
648
|
| `cerefox_list_versions(document_id, requestor)` | `cerefox document version list <id> --requestor <name>` |
|
|
@@ -709,12 +718,18 @@ cerefox document ingest-dir ./papers --extensions .md \
|
|
|
709
718
|
# Step 1: find it
|
|
710
719
|
cerefox search "the OAuth design doc" --match-count 1
|
|
711
720
|
|
|
712
|
-
# Step 2:
|
|
713
|
-
|
|
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
|
|
714
725
|
printf '%s' "$NEW_CONTENT" | cerefox document ingest --paste \
|
|
715
726
|
--title "OAuth 2.1 Design Document" \
|
|
716
727
|
--document-id "<uuid>" \
|
|
728
|
+
--expected-content-hash "<hash>" \
|
|
717
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.
|
|
718
733
|
```
|
|
719
734
|
|
|
720
735
|
### Unattended sync job
|
|
@@ -568,6 +568,11 @@ You have access to a personal knowledge base via the searchKnowledgeBase action.
|
|
|
568
568
|
When the user asks a question, always search the knowledge base first using a
|
|
569
569
|
relevant query. Present results by document title, citing the source for every claim.
|
|
570
570
|
Use ingestNote to save any new information the user asks you to remember.
|
|
571
|
+
When UPDATING an existing document, first call getDocument and note its
|
|
572
|
+
content_hash, then pass it as expected_content_hash on ingestNote. If you get a
|
|
573
|
+
409 conflict, the document changed underneath you: call getDocument again, merge
|
|
574
|
+
your changes into the latest content, and retry with the new hash — never
|
|
575
|
+
overwrite blindly.
|
|
571
576
|
```
|
|
572
577
|
|
|
573
578
|
### Path B verification
|
|
@@ -604,7 +609,7 @@ In the action editor, paste this schema (replace `<your-project-ref>`):
|
|
|
604
609
|
openapi: 3.1.0
|
|
605
610
|
info:
|
|
606
611
|
title: Cerefox Knowledge Base
|
|
607
|
-
version:
|
|
612
|
+
version: 2.0.0
|
|
608
613
|
servers:
|
|
609
614
|
- url: https://<your-project-ref>.supabase.co/functions/v1
|
|
610
615
|
paths:
|
|
@@ -731,6 +736,23 @@ paths:
|
|
|
731
736
|
instead of creating a new one. The previous content is archived
|
|
732
737
|
as a version. If content is unchanged, the document is skipped
|
|
733
738
|
(no re-indexing). Ignored when document_id is provided.
|
|
739
|
+
expected_content_hash:
|
|
740
|
+
type: string
|
|
741
|
+
description: >
|
|
742
|
+
REQUIRED on content updates (optimistic concurrency, v2.0.0):
|
|
743
|
+
the content_hash of the version this edit was based on, as
|
|
744
|
+
returned by getDocument / searchKnowledgeBase / metadataSearch.
|
|
745
|
+
If the document changed since it was read, the update fails
|
|
746
|
+
with HTTP 409 — re-read the document, merge your changes,
|
|
747
|
+
retry with the new hash. Not needed when creating.
|
|
748
|
+
last_write_wins:
|
|
749
|
+
type: boolean
|
|
750
|
+
default: false
|
|
751
|
+
description: >
|
|
752
|
+
Explicitly skip the concurrency check and overwrite regardless
|
|
753
|
+
of concurrent changes. Use ONLY when an external source of
|
|
754
|
+
truth makes conflicts meaningless. Recorded in the audit log.
|
|
755
|
+
Never use it to silence a 409 conflict.
|
|
734
756
|
author:
|
|
735
757
|
type: string
|
|
736
758
|
description: >
|
|
@@ -753,8 +775,19 @@ paths:
|
|
|
753
775
|
project_id?, project_name?, # set when a project was assigned on create
|
|
754
776
|
skipped?, # true when identical content was deduplicated
|
|
755
777
|
updated?, # true when an existing doc was updated
|
|
778
|
+
content_hash?, # the NEW hash after an update (the next edit's token)
|
|
756
779
|
message?, # human note on dedup/skip/update
|
|
757
780
|
note? } # note when a flag (e.g. update_if_exists) was overridden
|
|
781
|
+
'400':
|
|
782
|
+
description: >
|
|
783
|
+
Missing expected_content_hash on a content update (and
|
|
784
|
+
last_write_wins not set). Read the document first, then retry
|
|
785
|
+
with its content_hash.
|
|
786
|
+
'409':
|
|
787
|
+
description: >
|
|
788
|
+
Conflict — the document changed since it was read. Call getDocument
|
|
789
|
+
for the latest content + content_hash, merge your changes, and
|
|
790
|
+
retry with the new hash. Do not overwrite blindly.
|
|
758
791
|
/cerefox-metadata:
|
|
759
792
|
post:
|
|
760
793
|
operationId: listMetadataKeys
|
|
@@ -802,7 +835,9 @@ paths:
|
|
|
802
835
|
description: >
|
|
803
836
|
Document content and metadata:
|
|
804
837
|
{ document_id, doc_title, full_content, chunk_count, total_chars,
|
|
805
|
-
is_archived, version_id }
|
|
838
|
+
is_archived, version_id, content_hash }.
|
|
839
|
+
content_hash is the document's CURRENT hash — pass it back as
|
|
840
|
+
expected_content_hash when updating via ingestNote.
|
|
806
841
|
'404':
|
|
807
842
|
description: Document not found
|
|
808
843
|
/cerefox-list-versions:
|
|
@@ -952,7 +987,9 @@ paths:
|
|
|
952
987
|
Array of matching documents:
|
|
953
988
|
[{ document_id, title, doc_metadata, review_status, source, created_at,
|
|
954
989
|
updated_at, total_chars, chunk_count, project_ids, project_names,
|
|
955
|
-
version_count, content }]
|
|
990
|
+
version_count, content_hash, content }].
|
|
991
|
+
content_hash is the concurrency token — pass it back as
|
|
992
|
+
expected_content_hash when updating via ingestNote.
|
|
956
993
|
```
|
|
957
994
|
|
|
958
995
|
**Step 3 — Configure authentication**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Cerefox — user-owned shared memory for AI agents. The local TypeScript runtime: stdio MCP server in v0.4; CLI binary added in v0.5; in-process web server in v0.6; ingestion pipeline in v0.7.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|