feat(knowledge): delivery_policy column + typed-memory backfill (ADR 0108 D4) - #3242
Conversation
…0108 D4) Adds the last of the ADR 0108 D4 typed-memory columns, `delivery_policy` (`always` | `retrieved` | `on_demand`, NULL = retrieved), and the one-shot backfill #3205 never shipped: on the first open of an existing store every untyped row is classified from `domain` (+ `source_type`) per the D4 table into `memory_kind` and `delivery_policy`. Only NULL cells are written; the pass is stamped in `_kb_meta` so it retries if it dies mid-way and never re-runs once done. - Store: column + index, `Chunk.delivery_policy`, `add_chunk`/`add_document` kwarg, `search`/`list_chunks` filters (FTS, LIKE, vector — both rankings), `promote()` forwards it. A `domain="hot"` write with no explicit policy is stamped `always` (`infer_delivery_policy`, shared with the backfill). - Tools: `memory_ingest(delivery_policy=)`, `memory_recall`/`memory_list` filters + `policy=` tag. The hot-write confirm gate now also refuses `delivery_policy="always"` on any domain. - Console rows and `sdk.knowledge_add` carry the typed columns. - Delivery is unchanged: `get_hot_memory_entries` still selects on `domain="hot"` until D6 (#3187) switches the reader to the policy column. Refs #3072, #3184. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (16)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ackfill) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
…ed kwargs `python scripts/gen_plugin_api.py` — the generated page gates on the SDK signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
…forces always, tool-boundary validation, ingest gate, Chunk.from_row - `retrieved` now matches NULL rows (NULL *means* retrieved): one `_delivery_policy_clause()` helper next to `_namespace_clause`, used at all four SQL sites (FTS, LIKE, list_chunks, hybrid vector). - `domain="hot"` FORCES `delivery_policy="always"` in add_chunk (not default-only): the reader keys on the domain, so the column must be truthful or D6's switch would strand hot+non-always rows. Logged at debug when a caller passed something else. - Tool boundary validates + normalizes `delivery_policy` (`_norm_delivery_policy` against `DELIVERY_POLICIES`; "ALWAYS" → "always", "hot" → Error) in memory_ingest / memory_recall / memory_list; store + SDK stay permissive. - `knowledge_ingest(domain="hot")` is gated by `knowledge_hot_write_confirm` too — shared `_ALWAYS_ON_REFUSAL` text names both tools. Settings/config descriptions, guide, explanation and reference docs say so. - `Chunk.from_row()` drops columns this build doesn't know; every `Chunk(**dict(row))` site uses it, so a DB a newer build has widened (the host-level shared commons especially) still reads. - Changelog fragment: plain bold lead-in, period outside the bold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
There was a problem hiding this comment.
QA panel review — PASS
code-review-structural · head 1c13de5ed63e · formal
Low-risk PR: a documentation addition explaining the memory/knowledge trust gate. No code defects surfaced. The one finding is a doc-accuracy nit — the explanation doc's phrasing implies knowledge_ingest also checks delivery_policy, but its signature has no such parameter; the sibling guide doc states the distinction correctly. Fix first: a one-line reword in docs/explanation/memory-and-knowledge.md to match the guide doc's precision. The panel did not disagree on any point. Verification confirmed the finding but flagged a gap: the exact gate code in tools/lg_tools.py was unreadable via API (404 on both head and default branch), so the claim rests on the doc signatures, the guide doc's wording, and the test suite rather than the source directly.
Findings
| Severity | Location | Finding | Verified | |
|---|---|---|---|---|
| ⚪ | nit | docs/explanation/memory-and-knowledge.md:127 |
The explanation doc says the confirm gate "refuses that policy [delivery_policy=always] on any domain — through memory_ingest and knowledge_ingest alike", but … | confirmed |
findings JSON (machine-readable)
[
{
"file": "docs/explanation/memory-and-knowledge.md",
"line": 127,
"severity": "nit",
"category": "conventions",
"claim": "The explanation doc says the confirm gate \"refuses that policy [delivery_policy=always] on any domain \u2014 through memory_ingest and knowledge_ingest alike\", but knowledge_ingest has no delivery_policy parameter, so it cannot refuse that policy \u2014 its gate keys on domain=\"hot\" only; the sibling guide doc states this correctly and the explanation doc is the loose one.",
"evidence": "docs/explanation/memory-and-knowledge.md: \"and the gate refuses that policy on *any*\\n domain \u2014 through `memory_ingest` and `knowledge_ingest` alike\" \u2014 while tools/lg_tools.py knowledge_ingest gate is `if dom.lower() == \"hot\" and getattr(graph_config, \"knowledge_hot_write_confirm\", False):` (no delivery_policy check) and its signature is knowledge_ingest(source, domain, title?).",
"verdict": "confirmed",
"note": "The explanation doc's 'refuses that policy on any domain \u2014 through memory_ingest and knowledge_ingest alike' overstates: knowledge_ingest has no delivery_policy param (signature confirmed in starter-tools.md) and the guide doc correctly scopes its gate to domain=\"hot\" only. The exact lg_tools.py gate code is unverified (file unreadable via API), but the substance is grounded in the doc signatures, the guide doc's precise wording, and the test suite (which only exercises domain=\"hot\" for knowledge_ingest)."
}
]1 finding(s) downgraded to uncertain: the code they quote as evidence does not appear in the file at the reviewed head, nor in this PR's patch for it. A finding that cannot be grounded does not gate a merge (issue #25) — it still stands for a human to judge.
docs/explanation/memory-and-knowledge.md(nit) — quoted evidence not found at this head:if dom.lower() == "hot" and getattr(graph_config, "knowledge_hot_write_confirm", False):
1 panel step(s) hit their time budget and were skipped this round: find_crossfile. The verdict stands on the remaining angles; a finding only that step would have caught could be missed — the next push re-runs the full panel.
What
The last ADR 0108 D4 typed-memory column —
delivery_policy— plus the one-shot backfill that #3205 never shipped. Context v2 Phase 2b; the gate for D6 (delivery budget), D7 (write lifecycle) and D9 (digest gating).delivery_policy TEXT(nullable) onchunks:always(every turn — whatdomain="hot"has always meant) ·retrieved(on a RAG match; NULL = retrieved, and aretrievedfilter matches NULL rows) ·on_demand(tool call only). Indexidx_chunks_delivery_policy. One_delivery_policy_clause()helper (next to_namespace_clause) builds the predicate at all four SQL sites — FTS, LIKE fallback,list_chunks, hybrid vector.Plumbed everywhere the other typed columns are:
Chunk+as_dict(),add_chunk/add_document,search(hybrid filters BOTH rankings),list_chunks, layeredsearch(both tiers) andpromote().domain="hot"forcesdelivery_policy="always"inadd_chunk— not default-only. Today's reader keys on the domain, so a hot row IS always-on whatever the column says; the column must be truthful or D6's flip would strand ahot+on_demandrow. A caller passing something else is overridden (logged at debug).memory_kindis not inferred at write time — feat: typed memory schema — additive columns for memory classification #3205's omitted-stays-NULL contract holds.One-shot backfill of legacy rows (both
memory_kindanddelivery_policy— feat: typed memory schema — additive columns for memory classification #3205 left every existing row NULL), run on the first open of an existing store:domainmemory_kinddelivery_policyhotstandingalwayspreferencesprofilegeneralsource_type="conversation"notegeneralsource_typereferencefindingfactfactfactconversationnotelegacyOnly NULL cells are written (an already-typed row keeps what it has); the mapping lives in one place (
infer_memory_kind/infer_delivery_policy) shared by the backfill and the write path. The pass is stamped in_kb_meta(typed_memory_backfill) rather than keyed on the column's existence, so a pass that dies after theALTER TABLEretries on the next open and a finished one never re-runs. A fresh DB gets the column from_SCHEMAand is stamped with nothing to classify.Agent tools:
memory_ingest(delivery_policy=),memory_recall(delivery_policy=),memory_list(delivery_policy=)+ apolicy=tag in listings. The value is validated and normalized at the tool boundary (_norm_delivery_policyagainstDELIVERY_POLICIES:" ALWAYS "→always;"hot"→Error: delivery_policy must be one of …); the store and SDK stay permissive, matching feat: typed memory schema — additive columns for memory classification #3205'smemory_kindstance.The always-on confirm gate (
knowledge.hot_write_confirm) now covers every agent path to an always-on row:memory_ingestwithdomain="hot"ordelivery_policy="always", andknowledge_ingestwithdomain="hot"(it had no check — a local file was an ungated path). One shared_ALWAYS_ON_REFUSALtext names both tools; settings description, config comment, guide, explanation and reference docs updated to match.Console + SDK:
_knowledge_rowexposesmemory_kind/review_state/delivery_policy(the typed columns weren't reaching the console at all);sdk.knowledge_add(memory_kind=, delivery_policy=), forwarded only when set so pre-D4 plugin backends keep working.docs/reference/plugin-sdk-api.mdregenerated (scripts/gen_plugin_api.py).Chunk.from_row()replaces every strictChunk(**dict(row))— unknown columns are dropped, not fatal (see Compatibility).Boundary — what this PR deliberately does NOT do
D6 (#3187) owns switching hot-memory reads to
delivery_policy.get_hot_memory_entriesstill selects ondomain="hot",_publish_hot_writestill fires ondomain == "hot", and the console hot-edit route still pins"hot"(which now stampsalways). D4 records the policy; D6 makes the delivery layer read it.D6 note. After this PR no
hot+non-alwaysrow can exist — the write path forces it and the backfill stamps every legacy hot row — so D6 can flip readers todelivery_policy='always'with no migration step and no NULL special-casing (_delivery_policy_clausealready treats NULL asretrieved).Compatibility
The commons DB is host-level (
~/.protoagent/commons/knowledge.db, shared by the default and dev instances). The first upgraded instanceALTERs it; an older build then reads a widened table, and its strictChunk(**dict(r))fails inLayeredKnowledgeStore.list_chunks(console Store view,memory_list, the fact consolidator). That is exactly the exposure #3205 already created when it added four columns. This PR makes this build immune to future columns (D7's) viaChunk.from_row(); older builds remain as exposed as they were.Rollback
Nullable additive column; the backfill only fills NULLs. Reverting the code restores v1 reads.
Gates (local, worktree off
origin/main@49aeb6bd)uv run ruff check .→All checks passed!uv run lint-imports→Contracts: 3 kept, 0 broken.test_knowledge_typed_memory,test_knowledge_layered,test_knowledge_trust,test_hot_memory,test_memory_routes,test_sdk_knowledge,test_plugin_api_reference) → 120 passedpython -m pytest tests/ -q(after the review round, head1c13de5e) → 6846 passed, 16 skipped, 156 warnings in 333.19s — EXIT=0Refs #3072 (closed by #3205), #3184 (tracker).
🤖 Generated with Claude Code
https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby