@cerefox/memory 1.1.0-beta.6 → 1.1.0-beta.8

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.
@@ -15,7 +15,7 @@
15
15
  href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap"
16
16
  />
17
17
  <title>Cerefox</title>
18
- <script type="module" crossorigin src="/app/assets/index-Co28kX04.js"></script>
18
+ <script type="module" crossorigin src="/app/assets/index-DD7DDGGU.js"></script>
19
19
  <link rel="stylesheet" crossorigin href="/app/assets/index-C1JXZA9m.css">
20
20
  </head>
21
21
  <body>
@@ -18,7 +18,7 @@
18
18
  * doesn't touch `supabase/functions/` leaves it alone).
19
19
  */
20
20
 
21
- export const EF_VERSION = "1.1.0-beta.2";
21
+ export const EF_VERSION = "1.1.0-beta.7";
22
22
 
23
23
  /**
24
24
  * The most recent version whose EF-side SOURCE actually changed (#127).
@@ -28,7 +28,7 @@ export const EF_VERSION = "1.1.0-beta.2";
28
28
  * `cut_release.ts` ONLY when EF source changed since the last tag; doctor
29
29
  * uses it to stay silent on label-only drift.
30
30
  */
31
- export const EF_LAST_CHANGED = "1.1.0-beta.2";
31
+ export const EF_LAST_CHANGED = "1.1.0-beta.7";
32
32
 
33
33
  /**
34
34
  * The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
@@ -86,33 +86,7 @@ function readUnitInterval(name: string): number | undefined {
86
86
  export const DEFAULT_SEARCH_ALPHA = 0.7;
87
87
 
88
88
  export function getSearchAlpha(): number {
89
- return readUnitInterval("CEREFOX_SEARCH_ALPHA") ?? DEFAULT_SEARCH_ALPHA;
90
- }
91
-
92
- /**
93
- * The retrieval tunables a client should actually SEND to the RPCs (#133).
94
- *
95
- * Returns undefined when the operator has expressed no preference, so the
96
- * parameter is omitted and the server resolves it: `cerefox_config` first,
97
- * then the built-in default. That is what lets one `cerefox config set` govern
98
- * every access path. When a value IS configured here it wins, preserving the
99
- * chain: per-call argument > client env > deployment config > built-in.
100
- *
101
- * `min_search_score` has one subtlety: the local embedder needs a higher floor
102
- * (nomic scores unrelated text ~0.4–0.55), so an explicitly local embedder
103
- * counts as "configured" even when CEREFOX_MIN_SEARCH_SCORE is unset —
104
- * otherwise omitting the parameter would silently apply the OpenAI-calibrated
105
- * default to a local deployment.
106
- */
107
- export function getConfiguredMinSearchScore(): number | undefined {
108
- const explicit = readUnitInterval("CEREFOX_MIN_SEARCH_SCORE");
109
- if (explicit !== undefined) return explicit;
110
- if (readEnv("CEREFOX_EMBEDDER") === "local") return DEFAULT_MIN_SEARCH_SCORE_LOCAL;
111
- return undefined;
112
- }
113
-
114
- export function getConfiguredSearchAlpha(): number | undefined {
115
- return readUnitInterval("CEREFOX_SEARCH_ALPHA");
89
+ return DEFAULT_SEARCH_ALPHA;
116
90
  }
117
91
 
118
92
  /**
@@ -127,24 +101,47 @@ export function getConfiguredSearchAlpha(): number | undefined {
127
101
  * EF path doesn't use the host `.env` anyway).
128
102
  */
129
103
  export function getMinSearchScore(): number {
130
- const fallback =
131
- readEnv("CEREFOX_EMBEDDER") === "local"
132
- ? DEFAULT_MIN_SEARCH_SCORE_LOCAL
133
- : DEFAULT_MIN_SEARCH_SCORE;
134
- return readUnitInterval("CEREFOX_MIN_SEARCH_SCORE") ?? fallback;
104
+ return readEnv("CEREFOX_EMBEDDER") === "local"
105
+ ? DEFAULT_MIN_SEARCH_SCORE_LOCAL
106
+ : DEFAULT_MIN_SEARCH_SCORE;
135
107
  }
136
108
 
137
109
  /**
138
- * CEREFOX_MIN_TERM_COVERAGE (v1.0.4): user-configurable default for the
139
- * OR-fallback term-coverage gate. Returns undefined when unset/invalid —
140
- * callers then OMIT p_min_term_coverage from the RPC call, deferring to the
141
- * server default (0.5) and staying compatible with pre-0.9.1 servers
142
- * (an unknown named argument fails the PostgREST function match).
110
+ * Retrieval tuning is server-side state, not a per-machine preference.
111
+ *
112
+ * These used to read CEREFOX_MIN_SEARCH_SCORE / CEREFOX_SEARCH_ALPHA /
113
+ * CEREFOX_MIN_TERM_COVERAGE so a client could override the store's setting.
114
+ * That was the wrong model: the right similarity floor depends on which
115
+ * embedder produced the vectors, and the embedder is a property of the STORE —
116
+ * every client querying one database must use the same one (`doctor` enforces
117
+ * exactly that). So there is no case where two clients should legitimately
118
+ * disagree, and an override only creates a way for search to behave differently
119
+ * depending on who asked.
120
+ *
121
+ * All three now return undefined: the parameter is omitted and the RPC resolves
122
+ * `cerefox_config`, then the built-in default. One `cerefox config set` — or the
123
+ * Settings page — governs every access path.
124
+ *
125
+ * Cerefox Local still needs its higher floor for the nomic embedder; it seeds
126
+ * `min_search_score` into its own `cerefox_config` at container init rather than
127
+ * carrying it in the environment.
128
+ *
129
+ * A per-call argument (`--min-score`, the MCP `min_score` param) still wins, as
130
+ * it always did. `cerefox doctor` reports the retired variables if still set.
143
131
  */
132
+ export function getConfiguredMinSearchScore(): number | undefined {
133
+ return undefined;
134
+ }
135
+
136
+ export function getConfiguredSearchAlpha(): number | undefined {
137
+ return undefined;
138
+ }
139
+
144
140
  export function getMinTermCoverage(): number | undefined {
145
- return readUnitInterval("CEREFOX_MIN_TERM_COVERAGE");
141
+ return undefined;
146
142
  }
147
143
 
144
+
148
145
  export function applyByteBudget(
149
146
  rows: unknown[],
150
147
  maxBytes: number,
@@ -11,16 +11,16 @@
11
11
  * docs/specs/polish-and-distribution-design.md §10d.
12
12
  */
13
13
 
14
- export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **14 MCP tools** (13 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
14
+ export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **14 MCP tools** (13 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
15
15
 
16
16
  /** Sections keyed by their H2 heading text (lower-cased for matching). */
17
17
  export const HELP_SECTIONS: Record<string, string> = {
18
- "Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |",
18
+ "Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
19
19
  "Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.",
20
20
  "Update Workflow (ID-based -- preferred)": "## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.",
21
21
  "Update Workflow (title-based -- fallback)": "## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```",
22
22
  "Catch-Up Workflow": "## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```",
23
- "CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.",
23
+ "CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.",
24
24
  };
25
25
 
26
26
  export const HELP_SECTION_HEADINGS: string[] = ["Tools", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)"];
@@ -0,0 +1,69 @@
1
+ -- 0016_retention_config.sql — version retention becomes a property of the
2
+ -- store, not of whichever client happens to write.
3
+ --
4
+ -- `cerefox_snapshot_version` took `p_retention_hours` / `p_cleanup_enabled` as
5
+ -- parameters, and every client filled them from its own environment
6
+ -- (`CEREFOX_VERSION_RETENTION_HOURS`, `CEREFOX_VERSION_CLEANUP_ENABLED`). The
7
+ -- surviving version history therefore depended on **which client wrote last**:
8
+ -- an operator could set "keep everything" on their machine and still lose
9
+ -- versions the moment an agent running defaults saved a document. Retention
10
+ -- describes the data, so it belongs to the data.
11
+ --
12
+ -- Both parameters now default to NULL, meaning "read the store's policy":
13
+ --
14
+ -- version_retention_hours (default 48)
15
+ -- version_cleanup_enabled (default true)
16
+ --
17
+ -- Passing an explicit value still overrides, for deliberate one-off admin
18
+ -- operations — but no caller supplies one by accident any more.
19
+ --
20
+ -- Adds `cerefox_config_int` / `cerefox_config_bool`, the integer and boolean
21
+ -- companions to the existing `cerefox_config_float`. Same contract: fall back
22
+ -- to the caller's default when the key is unset or unparseable, so a malformed
23
+ -- config row can never break a write path.
24
+ --
25
+ -- Unchanged, and worth restating because it bounds the risk of a long window:
26
+ -- cleanup NEVER deletes the most recent version, and never deletes a version
27
+ -- marked `archived`. Turning cleanup off keeps everything forever; leaving it
28
+ -- on with a long window still guarantees at least the latest version survives.
29
+ --
30
+ -- The functions live in `rpcs.sql`, which `cerefox server deploy` re-applies.
31
+ -- This migration exists so the schema version moves and operators are told to
32
+ -- redeploy — the change is inert until the RPCs are replaced.
33
+ --
34
+ -- Idempotent: safe to re-run.
35
+
36
+ -- ── Fail-safe for EXISTING stores ────────────────────────────────────────────
37
+ --
38
+ -- Upgrading silently changes where retention comes from. An operator running
39
+ -- `CEREFOX_VERSION_CLEANUP_ENABLED=false` (or a long window) would, on their
40
+ -- very next save, fall back to the 48-hour default and lose the history they had
41
+ -- deliberately kept. The env var stops being read the moment the client updates,
42
+ -- which is before anyone reads a release note.
43
+ --
44
+ -- So this migration disables pruning on existing stores. Nothing is deleted;
45
+ -- cleanup simply does not run until the operator states a policy. Pruning is
46
+ -- irreversible and not-pruning is not, so the safe default during an unattended
47
+ -- upgrade is to do nothing. `cerefox doctor` and the Settings page both show the
48
+ -- value, and turning it back on is one command.
49
+ --
50
+ -- Only existing databases get this. A fresh deploy STAMPS migrations as applied
51
+ -- rather than running them (see `_shared/db-deploy`), so new installs keep the
52
+ -- ordinary bounded default (48h, cleanup on) — there is no history there to
53
+ -- lose, and unbounded version growth is a poor default to saddle them with.
54
+ --
55
+ -- ON CONFLICT DO NOTHING: if the operator has already chosen a policy, this must
56
+ -- never overwrite it, including on a re-run.
57
+ INSERT INTO cerefox_config (key, value)
58
+ VALUES ('version_cleanup_enabled', 'false')
59
+ ON CONFLICT (key) DO NOTHING;
60
+
61
+ DO $$
62
+ BEGIN
63
+ RAISE NOTICE
64
+ 'Migration 0016: version retention now reads cerefox_config '
65
+ '(version_retention_hours, version_cleanup_enabled). The CEREFOX_VERSION_* '
66
+ 'environment variables are no longer read. Version pruning has been DISABLED '
67
+ 'on this store as an upgrade precaution — nothing was deleted. Set your policy '
68
+ 'with `cerefox config set version_cleanup_enabled true` or the Settings page.';
69
+ END $$;
@@ -940,7 +940,10 @@ $$;
940
940
  -- Parameters:
941
941
  -- p_document_id : Document to snapshot
942
942
  -- p_source : How the update was triggered ('file','paste','agent','manual')
943
- -- p_retention_hours : Retention window in hours (default: 48)
943
+ -- p_retention_hours : Retention window in hours. NULL (default) reads
944
+ -- `version_retention_hours` from cerefox_config, else 48.
945
+ -- A non-NULL value overrides the store policy for this
946
+ -- call only.
944
947
  --
945
948
  -- Returns: (version_id, version_number, chunk_count, total_chars) of the new version
946
949
 
@@ -949,8 +952,11 @@ DROP FUNCTION IF EXISTS cerefox_snapshot_version(UUID, TEXT, INT, BOOLEAN);
949
952
  CREATE FUNCTION cerefox_snapshot_version(
950
953
  p_document_id UUID,
951
954
  p_source TEXT DEFAULT 'manual',
952
- p_retention_hours INT DEFAULT 48,
953
- p_cleanup_enabled BOOLEAN DEFAULT TRUE
955
+ -- NULL (the new default) means "use the store's policy from
956
+ -- cerefox_config". Passing a value still overrides, for deliberate one-off
957
+ -- admin operations — but callers no longer supply one by accident.
958
+ p_retention_hours INT DEFAULT NULL,
959
+ p_cleanup_enabled BOOLEAN DEFAULT NULL
954
960
  )
955
961
  RETURNS TABLE (
956
962
  version_id UUID,
@@ -967,6 +973,18 @@ DECLARE
967
973
  v_version_number INT;
968
974
  v_chunk_count INT;
969
975
  v_total_chars INT;
976
+ -- Resolve the retention policy from the STORE, not the caller.
977
+ --
978
+ -- These used to arrive as parameters filled from each client's own env, so
979
+ -- the surviving version history depended on which client wrote last: an
980
+ -- agent running defaults would prune versions that an operator had
981
+ -- configured to keep. Retention describes the data, so it belongs to the
982
+ -- data. Same COALESCE(param, config, default) shape the retrieval tunables
983
+ -- already use.
984
+ v_retention INT := COALESCE(p_retention_hours,
985
+ cerefox_config_int('version_retention_hours', 48));
986
+ v_cleanup BOOLEAN := COALESCE(p_cleanup_enabled,
987
+ cerefox_config_bool('version_cleanup_enabled', TRUE));
970
988
  BEGIN
971
989
  -- Count current chunks to record in the version metadata
972
990
  SELECT COUNT(*), COALESCE(SUM(char_count), 0)
@@ -999,11 +1017,11 @@ BEGIN
999
1017
  -- but always keep the most recently created version (the one we just made).
1000
1018
  -- Skip archived versions (archived=true) -- they are protected from cleanup.
1001
1019
  -- Skip cleanup entirely if p_cleanup_enabled is false (immutable mode).
1002
- IF p_cleanup_enabled THEN
1020
+ IF v_cleanup THEN
1003
1021
  DELETE FROM cerefox_document_versions dv
1004
1022
  WHERE dv.document_id = p_document_id
1005
1023
  AND dv.archived IS NOT TRUE
1006
- AND dv.created_at < NOW() - (p_retention_hours || ' hours')::INTERVAL
1024
+ AND dv.created_at < NOW() - (v_retention || ' hours')::INTERVAL
1007
1025
  AND dv.id != (
1008
1026
  SELECT id FROM cerefox_document_versions
1009
1027
  WHERE document_id = p_document_id
@@ -2055,6 +2073,43 @@ BEGIN
2055
2073
  END;
2056
2074
  $$;
2057
2075
 
2076
+ -- Integer/boolean companions to cerefox_config_float. Same contract: fall back
2077
+ -- to the caller's default when the key is unset or unparseable, so a malformed
2078
+ -- row can never break a write path.
2079
+ CREATE OR REPLACE FUNCTION cerefox_config_int(p_key TEXT, p_fallback INT)
2080
+ RETURNS INT
2081
+ LANGUAGE plpgsql
2082
+ STABLE
2083
+ SECURITY DEFINER
2084
+ SET search_path = public, pg_catalog
2085
+ AS $$
2086
+ DECLARE
2087
+ v_raw TEXT;
2088
+ BEGIN
2089
+ SELECT value INTO v_raw FROM cerefox_config WHERE key = p_key;
2090
+ IF v_raw IS NULL OR btrim(v_raw) = '' THEN RETURN p_fallback; END IF;
2091
+ RETURN v_raw::INT;
2092
+ EXCEPTION WHEN others THEN
2093
+ RETURN p_fallback;
2094
+ END;
2095
+ $$;
2096
+
2097
+ CREATE OR REPLACE FUNCTION cerefox_config_bool(p_key TEXT, p_fallback BOOLEAN)
2098
+ RETURNS BOOLEAN
2099
+ LANGUAGE plpgsql
2100
+ STABLE
2101
+ SECURITY DEFINER
2102
+ SET search_path = public, pg_catalog
2103
+ AS $$
2104
+ DECLARE
2105
+ v_raw TEXT;
2106
+ BEGIN
2107
+ SELECT value INTO v_raw FROM cerefox_config WHERE key = p_key;
2108
+ IF v_raw IS NULL OR btrim(v_raw) = '' THEN RETURN p_fallback; END IF;
2109
+ RETURN lower(btrim(v_raw)) = 'true';
2110
+ END;
2111
+ $$;
2112
+
2058
2113
  CREATE OR REPLACE FUNCTION cerefox_set_config(p_key TEXT, p_value TEXT)
2059
2114
  RETURNS VOID
2060
2115
  LANGUAGE plpgsql
@@ -2068,6 +2123,10 @@ DECLARE
2068
2123
  v_allowed TEXT[] := ARRAY[
2069
2124
  'usage_tracking_enabled', 'require_requestor_identity', 'requestor_identity_format',
2070
2125
  'min_search_score', 'min_term_coverage', 'search_alpha',
2126
+ -- Version retention: a property of the STORE, not of whichever client
2127
+ -- happens to write. Previously passed per-call from client env, so the
2128
+ -- surviving history depended on who saved last.
2129
+ 'version_retention_hours', 'version_cleanup_enabled',
2071
2130
  -- Optional features, off by default (iteration 29).
2072
2131
  'relations_enabled'
2073
2132
  ];
@@ -2264,7 +2323,7 @@ SET search_path = public, pg_catalog
2264
2323
  AS $$
2265
2324
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
2266
2325
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
2267
- SELECT '0.10.2'::TEXT;
2326
+ SELECT '0.10.3'::TEXT;
2268
2327
  $$;
2269
2328
 
2270
2329
  -- ── cerefox_content_format_stats ─────────────────────────────────────────────
@@ -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.10.2
8
+ -- @version: 0.10.3
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 —
@@ -574,6 +574,46 @@ cerefox token list # show masked fingerprints of the accepted set
574
574
 
575
575
  ---
576
576
 
577
+ ### `cerefox relation set` / `delete` / `list` / `neighbors`
578
+
579
+ > **Off by default.** The relation feature ships **dormant**: the four MCP
580
+ > relation tools are hidden from agents, and the CLI group is inert, until an
581
+ > operator opts in with `cerefox config set relations_enabled true` (or the
582
+ > **Settings** page in the web UI). The table and schema exist either way, so
583
+ > enabling and disabling are both non-destructive — turning it off hides the
584
+ > tools again without deleting a single edge.
585
+
586
+ **Purpose**: typed, directed links between documents — `source --rel_type--> target`.
587
+ `rel_type` is free text; a dictionary in SQL marks some types symmetric (setting
588
+ one direction implies the other, and deleting removes both).
589
+
590
+ **Synopsis**:
591
+ ```
592
+ cerefox relation set SOURCE_ID REL_TYPE TARGET_ID
593
+ cerefox relation delete SOURCE_ID REL_TYPE TARGET_ID
594
+ cerefox relation list DOCUMENT_ID # every relation touching it, both directions
595
+ cerefox relation neighbors DOCUMENT_ID REL_TYPE # walk the graph along one type
596
+ ```
597
+
598
+ **Notes**:
599
+
600
+ - `neighbors` walks to a bounded `--depth` and is cycle-safe: a document already
601
+ visited on the walk is not revisited, so a loop in the graph terminates rather
602
+ than recursing forever.
603
+ - Self-edges are rejected, as are edges to a document that does not exist.
604
+ - Deleting a document cascades to its edges; soft-deleting one hides them from
605
+ traversal without removing them, so a restore brings the graph back intact.
606
+ - Every write is recorded in the audit log (`relation-set`).
607
+
608
+ **Enabling it**:
609
+ ```
610
+ cerefox config set relations_enabled true # tools appear in every agent's list
611
+ cerefox config set relations_enabled false # hidden again; no data removed
612
+ ```
613
+
614
+ Agents see 10 tools with the flag off and 14 with it on. See
615
+ [`configuration.md`](configuration.md) for the full runtime-config surface.
616
+
577
617
  ### `cerefox config list` / `cerefox config get` / `cerefox config set`
578
618
 
579
619
  **Purpose**: read/write runtime config in `cerefox_config` (e.g. `usage_tracking_enabled`, `require_requestor_identity`).
@@ -151,9 +151,9 @@ This handles intermittent OpenAI API errors (500s) that would otherwise cause se
151
151
  | Variable | Default | Description |
152
152
  |----------|---------|-------------|
153
153
  | `CEREFOX_MAX_RESPONSE_BYTES` | `200000` | Maximum bytes in a single search response (local MCP path). See explanation below. |
154
- | `CEREFOX_MIN_SEARCH_SCORE` | `0.50` (`0.60` with the local embedder) | Minimum cosine similarity for hybrid and semantic search results (0.0–1.0). The default is embedder-aware: nomic scores unrelated text higher than OpenAI, so `CEREFOX_EMBEDDER=local` raises the floor to 0.60. In **hybrid search**, chunks that matched the FTS keyword operator (`@@`) always pass through regardless of their vector score the threshold only filters vector-only results. In **semantic search**, all results are filtered. The pure **FTS search** mode is unaffected. Increase for stricter precision; decrease for wider recall. |
155
- | `CEREFOX_SEARCH_ALPHA` | `0.7` | Hybrid fusion weight (0.0–1.0): `1.0` = pure semantic, `0.0` = pure keyword. Applies to hybrid and document-mode search. Per-call override: `cerefox search --alpha`, or the `alpha` parameter on the `cerefox_search` MCP tool. |
156
- | `CEREFOX_MIN_TERM_COVERAGE` | *(unset server default `0.5`)* | Confidence bar for the keyword OR-fallback (v1.0.4, schema ≥ 0.9.1): when a strict all-terms match fails and search relaxes to any-term matching, a result counts as a confident hit only if it matches at least this fraction of the query's meaningful terms; weaker matches surface as below-confidence candidates. `0` restores pre-gate behavior (any matching term passes); `1` requires every term. Per-call override: `cerefox search --min-term-coverage`. Leave unset against pre-0.9.1 servers. |
154
+ | `CEREFOX_MIN_SEARCH_SCORE` | **Retired in v1.1.0 no longer read.** | Now a store setting: `cerefox config set min_search_score <value>`, or the **Settings** page. The right floor depends on which embedder produced the vectors, and the embedder belongs to the store every client querying one database must use the same one. A per-call override still works (`cerefox search --min-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
155
+ | `CEREFOX_SEARCH_ALPHA` | **Retired in v1.1.0 no longer read.** | Now a store setting: `cerefox config set search_alpha <value>`, or the **Settings** page. A per-call override still works (`cerefox search --min-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
156
+ | `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-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
157
157
  | `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. |
158
158
  | `CEREFOX_MODELS_DIR` | `~/.cerefox/models` (in-container: inside the data volume) | Where the local embedder caches downloaded model weights (Cerefox Local; `CEREFOX_EMBEDDER=local`). |
159
159
  | `CEREFOX_ONNX_BATCH` | `4` | Texts per local-embedder inference call. Peak memory scales with this; the small default keeps ingest/reindex safe on small Docker VMs. |
@@ -240,8 +240,8 @@ Cerefox automatically archives previous document content whenever a document is
240
240
 
241
241
  | Variable | Default | Description |
242
242
  |----------|---------|-------------|
243
- | `CEREFOX_VERSION_RETENTION_HOURS` | `48` | How many hours to keep archived document versions. Versions older than this are lazily deleted the next time the same document is updated. Always keeps at least the most recent version regardless of age. |
244
- | `CEREFOX_VERSION_CLEANUP_ENABLED` | `true` | When `true`, old versions are lazily deleted during updates (respecting `VERSION_RETENTION_HOURS`). Versions marked as `archived` are always protected. When `false`, all versions are retained indefinitely (immutable mode). |
243
+ | `CEREFOX_VERSION_RETENTION_HOURS` | **Retired in v1.1.0 — no longer read.** | Version retention is now a property of the store: `cerefox config set version_retention_hours <hours>`, or the **Settings** page. It moved because it used to be passed per-call from each client's environment, so the surviving history depended on which client wrote last. `cerefox doctor` reports the variable if it is still set. |
244
+ | `CEREFOX_VERSION_CLEANUP_ENABLED` | **Retired in v1.1.0 no longer read.** | Use `cerefox config set version_cleanup_enabled <true\|false>` (or **Settings**). Set to `false` to keep every version forever. Cleanup never deletes the most recent version, nor any version marked `archived`. |
245
245
 
246
246
  **How versioning works:**
247
247
 
@@ -266,7 +266,7 @@ cerefox document get <document-id> --version-id <version-id>
266
266
  |----------|---------|-------------|
267
267
  | `CEREFOX_BACKUP_DIR` | `~/.cerefox/backups` | Local directory where file system backups are stored. Created automatically if it doesn't exist. **Use an absolute path** — a relative value (such as the pre-v0.3.0 `./backups`) resolves against the current working directory, so snapshots scatter depending on where you run the command; `backup create` warns when it sees one. Does **not** follow `CEREFOX_CONFIG_DIR`, so a second environment must set it explicitly. |
268
268
  | `CEREFOX_ENV_LABEL` | _(unset)_ | Names a non-production environment (e.g. `staging`). Purely cosmetic and inert when unset. When set: the web UI shows a banner on every page, `doctor` shows `[LABEL]` on its title line, `backup create` puts the label in the snapshot filename and payload, and `backup restore` warns when a snapshot's environment differs from the target's. See [`staging-env.md`](staging-env.md). |
269
- | `CEREFOX_VERSION_RETENTION_HOURS` | `48` | How long to retain archived document versions (hours). The most recent version is always kept regardless of this setting. |
269
+ | `CEREFOX_VERSION_RETENTION_HOURS` | **Retired in v1.1.0 — no longer read.** | Version retention is now a property of the store: `cerefox config set version_retention_hours <hours>`, or the **Settings** page. It moved because it used to be passed per-call from each client's environment, so the surviving history depended on which client wrote last. `cerefox doctor` reports the variable if it is still set. |
270
270
 
271
271
  ---
272
272
 
@@ -20,6 +20,19 @@ to re-run.
20
20
 
21
21
  ## End-user upgrade
22
22
 
23
+ > **Upgrading to v1.1.0: run `cerefox server deploy`, don't defer it.** Most
24
+ > releases let you postpone the server step. This one should not be postponed:
25
+ > schema **0.10.2** fixes a defect where a stale or blank
26
+ > `expected_content_hash` raised its conflict under a SQLSTATE that infrastructure
27
+ > treats as *retryable*. Because the conflict is permanent, retry-aware layers
28
+ > could replay the request without limit — one report reached ~47 million calls
29
+ > over about a day and exhausted the project's disk-IO budget. The fix lives in
30
+ > `rpcs.sql`, so **upgrading the client alone does not apply it**; the database
31
+ > keeps the old behaviour until the RPCs are redeployed. `cerefox doctor` will
32
+ > say so, and the web UI shows a banner.
33
+ >
34
+ > Cerefox Local users need no separate step — the schema ships inside the image.
35
+
23
36
  ```bash
24
37
  cerefox self-update # or: re-run the installer, or bun/npm update -g @cerefox/memory
25
38
  cerefox server deploy # applies pending migrations, re-applies RPCs, redeploys the 9 Edge Functions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "1.1.0-beta.6",
3
+ "version": "1.1.0-beta.8",
4
4
  "description": "Cerefox — 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",