@cerefox/memory 1.0.2 → 1.0.3

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-CLD16BC4.js"></script>
18
+ <script type="module" crossorigin src="/app/assets/index-Csj-6UHY.js"></script>
19
19
  <link rel="stylesheet" crossorigin href="/app/assets/index-Asx5wD7g.css">
20
20
  </head>
21
21
  <body>
@@ -18,7 +18,17 @@
18
18
  * doesn't touch `supabase/functions/` leaves it alone).
19
19
  */
20
20
 
21
- export const EF_VERSION = "1.0.2";
21
+ export const EF_VERSION = "1.0.3";
22
+
23
+ /**
24
+ * The most recent version whose EF-side SOURCE actually changed (#127).
25
+ * `EF_VERSION` bumps unconditionally at stable cuts (so stable deployments
26
+ * never display a pre-release label), which means a version delta no longer
27
+ * implies the deployed behaviour differs. This constant is bumped by
28
+ * `cut_release.ts` ONLY when EF source changed since the last tag; doctor
29
+ * uses it to stay silent on label-only drift.
30
+ */
31
+ export const EF_LAST_CHANGED = "1.0.3";
22
32
 
23
33
  /**
24
34
  * The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
@@ -11,12 +11,12 @@
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 **10 MCP tools** (9 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_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. **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_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 **10 MCP tools** (9 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_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. **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_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
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_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) |",
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. **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`.",
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. **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```",
@@ -132,16 +132,24 @@ async function handler(
132
132
  doc_title?: string;
133
133
  full_content?: string;
134
134
  best_score?: number;
135
+ score?: number;
135
136
  is_partial?: boolean;
136
137
  chunk_count?: number;
137
138
  total_chars?: number;
138
139
  content_hash?: string;
140
+ below_confidence?: boolean;
139
141
  }>;
140
142
 
143
+ // 28I: nothing cleared the relevance threshold, so the server returned its
144
+ // best-effort top candidates flagged below_confidence instead of an empty
145
+ // set (which agents misread as "this knowledge does not exist").
146
+ const belowConfidence = rows.length > 0 && rows.every((r) => r.below_confidence === true);
147
+
141
148
  const parts: string[] = rows.map((row) => {
142
149
  const title = row.doc_title ?? "Untitled";
143
150
  const docId = row.document_id ? ` [id: ${row.document_id}]` : "";
144
- const score = row.best_score != null ? ` (score: ${row.best_score.toFixed(3)})` : "";
151
+ const rawScore = row.best_score ?? row.score;
152
+ const score = rawScore != null ? ` (score: ${rawScore.toFixed(3)})` : "";
145
153
  const partial = row.is_partial
146
154
  ? ` -- partial (${row.chunk_count} of ${(row.total_chars ?? 0).toLocaleString()} chars)`
147
155
  : "";
@@ -151,6 +159,12 @@ async function handler(
151
159
  });
152
160
 
153
161
  let output = parts.join("\n\n---\n\n");
162
+ if (belowConfidence) {
163
+ output =
164
+ `⚠ No results cleared the confidence threshold. Showing the closest ${rows.length} ` +
165
+ `candidate(s) with scores — judge relevance yourself; a low score means weak signal, ` +
166
+ `not necessarily absent knowledge.\n\n` + output;
167
+ }
154
168
  if (truncated) {
155
169
  output +=
156
170
  `\n\n[Results truncated at ${usedBytes} bytes. Use a more specific query or a smaller match_count to see more.]`;
@@ -63,6 +63,12 @@ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID,
63
63
  DROP FUNCTION IF EXISTS cerefox_reconstruct_doc(UUID);
64
64
  DROP FUNCTION IF EXISTS cerefox_get_document(UUID, UUID);
65
65
 
66
+ -- Iteration 28I (v1.0.3, search recall): below_confidence BOOLEAN added to the
67
+ -- return types of cerefox_hybrid_search and cerefox_search_docs (never-silently-
68
+ -- empty fallback). Drop the pre-change signatures first.
69
+ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB);
70
+ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB);
71
+
66
72
  -- ── Shared return type note ────────────────────────────────────────────────────
67
73
  -- All chunk-level search RPCs return the same shape for consistency:
68
74
  -- chunk_id, document_id, chunk_index, title, content, heading_path,
@@ -106,7 +112,11 @@ RETURNS TABLE (
106
112
  doc_project_ids UUID[],
107
113
  doc_project_names TEXT[],
108
114
  doc_metadata JSONB,
109
- version_count INT
115
+ version_count INT,
116
+ -- 28I: TRUE on every row of a below-confidence fallback response — nothing
117
+ -- cleared the pass-filter, so these are the best-effort top candidates for
118
+ -- the caller to judge (scores included). FALSE on all normal results.
119
+ below_confidence BOOLEAN
110
120
  )
111
121
  LANGUAGE plpgsql
112
122
  SECURITY DEFINER
@@ -120,9 +130,54 @@ DECLARE
120
130
  -- websearch operators (phrase, OR, NOT); semantic ranking is the soft-match
121
131
  -- layer for "broadly related". If operator support is ever needed, gate it
122
132
  -- behind an opt-in flag rather than changing the default.
123
- query_fts tsquery := plainto_tsquery('english', p_query_text);
133
+ --
134
+ -- 28I progressive relaxation: AND-first, OR-fallback. The AND query stays
135
+ -- the primary match (byte-identical behavior whenever it matches anything),
136
+ -- but when it matches ZERO chunks (under the same project/metadata filters),
137
+ -- we retry with an OR-composition of the same tokens: one absent term then
138
+ -- no longer vetoes the terms that DO occur, and ts_rank_cd naturally ranks
139
+ -- chunks matching more terms higher. Multi-term evidence accumulates
140
+ -- instead of vetoing.
141
+ query_fts_and tsquery := plainto_tsquery('english', p_query_text);
142
+ query_fts_or tsquery := NULL;
143
+ query_fts tsquery;
144
+ tok TEXT;
145
+ tok_q tsquery;
146
+ and_matches BOOLEAN := FALSE;
124
147
  candidate_count INT := p_match_count * 5;
125
148
  BEGIN
149
+ -- Build the OR-composed query: plainto each whitespace token (so tokens get
150
+ -- the same normalization/stemming as the AND path), skip stopword-only
151
+ -- tokens, fold with the tsquery OR operator (||).
152
+ FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
153
+ tok_q := plainto_tsquery('english', tok);
154
+ IF numnode(tok_q) > 0 THEN
155
+ query_fts_or := CASE WHEN query_fts_or IS NULL
156
+ THEN tok_q ELSE query_fts_or || tok_q END;
157
+ END IF;
158
+ END LOOP;
159
+
160
+ -- Does the strict AND query match anything at all (under the caller's
161
+ -- filters)? Cheap probe against the partial FTS index.
162
+ IF numnode(query_fts_and) > 0 THEN
163
+ SELECT EXISTS (
164
+ SELECT 1
165
+ FROM cerefox_chunks c
166
+ JOIN cerefox_documents d ON c.document_id = d.id
167
+ WHERE c.version_id IS NULL
168
+ AND d.deleted_at IS NULL
169
+ AND c.fts @@ query_fts_and
170
+ AND (p_project_id IS NULL OR EXISTS (
171
+ SELECT 1 FROM cerefox_document_projects dp
172
+ WHERE dp.document_id = d.id AND dp.project_id = p_project_id
173
+ ))
174
+ AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
175
+ ) INTO and_matches;
176
+ END IF;
177
+
178
+ query_fts := CASE WHEN and_matches THEN query_fts_and
179
+ ELSE COALESCE(query_fts_or, query_fts_and) END;
180
+
126
181
  RETURN QUERY
127
182
  WITH
128
183
  fts_results AS (
@@ -183,7 +238,17 @@ BEGIN
183
238
  f.id IS NOT NULL AS has_fts_match
184
239
  FROM fts_results f
185
240
  FULL OUTER JOIN vec_results v ON f.id = v.id
186
- )
241
+ ),
242
+ -- 28I: pass-filter as a flag rather than a WHERE, so we can fall back.
243
+ -- FTS matches pass through unconditionally: the @@ operator is a hard
244
+ -- gate and guarantees the query terms appear in the chunk. Vector-only
245
+ -- results (no FTS match) are filtered by the cosine threshold.
246
+ flagged AS (
247
+ SELECT *,
248
+ (combined.has_fts_match OR combined.vec_score >= p_min_score) AS passes
249
+ FROM combined
250
+ ),
251
+ any_pass AS (SELECT bool_or(fl.passes) AS ok FROM flagged fl)
187
252
  SELECT
188
253
  c.id AS chunk_id,
189
254
  c.document_id,
@@ -202,16 +267,22 @@ BEGIN
202
267
  WHERE dp.document_id = d.id) AS doc_project_names,
203
268
  d.metadata AS doc_metadata,
204
269
  (SELECT COUNT(*)::INT FROM cerefox_document_versions dv
205
- WHERE dv.document_id = d.id) AS version_count
206
- FROM combined cm
270
+ WHERE dv.document_id = d.id) AS version_count,
271
+ -- 28I: when NOTHING clears the pass-filter, return the top candidates
272
+ -- anyway, flagged — an empty response reads to agent callers as "this
273
+ -- knowledge does not exist", the most expensive wrong conclusion a
274
+ -- memory layer can produce. "Truly nothing" (no candidates at all)
275
+ -- still returns zero rows.
276
+ NOT ap.ok AS below_confidence
277
+ FROM flagged cm
278
+ CROSS JOIN any_pass ap
207
279
  JOIN cerefox_chunks c ON c.id = cm.id
208
280
  JOIN cerefox_documents d ON c.document_id = d.id
209
- -- FTS matches pass through unconditionally: the @@ operator is a hard gate
210
- -- and guarantees the query terms appear in the chunk.
211
- -- Vector-only results (no FTS match) are filtered by the cosine threshold.
212
- WHERE cm.has_fts_match OR cm.vec_score >= p_min_score
281
+ WHERE cm.passes OR NOT ap.ok
213
282
  ORDER BY cm.score DESC
214
- LIMIT p_match_count;
283
+ LIMIT (SELECT CASE WHEN ap2.ok THEN p_match_count
284
+ ELSE LEAST(p_match_count, 3) END
285
+ FROM any_pass ap2);
215
286
  END;
216
287
  $$;
217
288
 
@@ -245,9 +316,42 @@ SECURITY DEFINER
245
316
  SET search_path = public, pg_catalog
246
317
  AS $$
247
318
  DECLARE
248
- -- plainto_tsquery: see rationale comment in cerefox_hybrid_search above.
249
- query_fts tsquery := plainto_tsquery('english', p_query_text);
319
+ -- plainto_tsquery + 28I AND-first/OR-fallback: see the rationale comments
320
+ -- in cerefox_hybrid_search above.
321
+ query_fts_and tsquery := plainto_tsquery('english', p_query_text);
322
+ query_fts_or tsquery := NULL;
323
+ query_fts tsquery;
324
+ tok TEXT;
325
+ tok_q tsquery;
326
+ and_matches BOOLEAN := FALSE;
250
327
  BEGIN
328
+ FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
329
+ tok_q := plainto_tsquery('english', tok);
330
+ IF numnode(tok_q) > 0 THEN
331
+ query_fts_or := CASE WHEN query_fts_or IS NULL
332
+ THEN tok_q ELSE query_fts_or || tok_q END;
333
+ END IF;
334
+ END LOOP;
335
+
336
+ IF numnode(query_fts_and) > 0 THEN
337
+ SELECT EXISTS (
338
+ SELECT 1
339
+ FROM cerefox_chunks c
340
+ JOIN cerefox_documents d ON c.document_id = d.id
341
+ WHERE c.version_id IS NULL
342
+ AND d.deleted_at IS NULL
343
+ AND c.fts @@ query_fts_and
344
+ AND (p_project_id IS NULL OR EXISTS (
345
+ SELECT 1 FROM cerefox_document_projects dp
346
+ WHERE dp.document_id = d.id AND dp.project_id = p_project_id
347
+ ))
348
+ AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
349
+ ) INTO and_matches;
350
+ END IF;
351
+
352
+ query_fts := CASE WHEN and_matches THEN query_fts_and
353
+ ELSE COALESCE(query_fts_or, query_fts_and) END;
354
+
251
355
  RETURN QUERY
252
356
  SELECT
253
357
  c.id AS chunk_id,
@@ -602,7 +706,10 @@ RETURNS TABLE (
602
706
  is_partial BOOL,
603
707
  -- Optimistic-concurrency token (iter-32): the document's current
604
708
  -- content_hash, to pass back as expected_content_hash on update.
605
- content_hash TEXT
709
+ content_hash TEXT,
710
+ -- 28I: TRUE when this is a below-confidence fallback response (nothing
711
+ -- cleared the hybrid pass-filter). See cerefox_hybrid_search.
712
+ below_confidence BOOLEAN
606
713
  )
607
714
  LANGUAGE sql
608
715
  SECURITY DEFINER
@@ -635,6 +742,7 @@ AS $$
635
742
  cr.doc_project_ids,
636
743
  cr.doc_project_names,
637
744
  cr.version_count,
745
+ cr.below_confidence,
638
746
  d.updated_at AS doc_updated_at,
639
747
  d.content_hash
640
748
  FROM chunk_results cr
@@ -720,7 +828,8 @@ AS $$
720
828
  td.doc_updated_at,
721
829
  td.version_count,
722
830
  ac.is_partial,
723
- td.content_hash
831
+ td.content_hash,
832
+ td.below_confidence
724
833
  FROM top_docs td
725
834
  JOIN doc_sizes ds ON ds.document_id = td.document_id
726
835
  JOIN all_content ac ON ac.document_id = td.document_id
@@ -1776,7 +1885,7 @@ SET search_path = public, pg_catalog
1776
1885
  AS $$
1777
1886
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
1778
1887
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
1779
- SELECT '0.8.2'::TEXT;
1888
+ SELECT '0.9.0'::TEXT;
1780
1889
  $$;
1781
1890
 
1782
1891
  -- ── 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.8.2
8
+ -- @version: 0.9.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 —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",