fix(remember): honor write_class=deliberate gate contract; grading never blocks write - #148
Merged
Conversation
…ver blocks write Two independent defects in the remember() write path (issue #147): 1. write_class="deliberate" (or omitted, source-fallback) was rejected by the novelty gate, violating the documented contract ("deliberate — NEVER rejected by the gate for low novelty"). Root cause: handlers/remember.py computed resolved_write_class but never threaded it into evaluate_gate -> determine_bypass, which had zero knowledge of write_class (mcp_server/handlers/remember.py:167-194, mcp_server/core/write_gate.py:115). Fixed by threading write_class through evaluate_gate / _compute_gate_decision / determine_bypass, checked LAST so a more specific content-based bypass reason (bypass_error/bypass_decision/ bypass_important_tag) still wins when it applies. 2. force=True (and plain writes) intermittently raised a bare FileNotFoundError with a misleading "check DATABASE_URL" hint despite a healthy PG connection. Root cause: insert_and_post_process's write-time provenance grading step (mcp_server/handlers/remember_helpers.py:717-719, pre-fix) called validate_memory.grade_from_content(..., base_dir=directory or os.getcwd()) unguarded — the ONE enrichment step in that function not wrapped in the same defensive try/except pattern used by every sibling step (source_attribution, habituation signature). os.getcwd() raises FileNotFoundError: [Errno 2] when the process cwd has been removed mid-session (e.g. a worktree cleanup) — unrelated to the DB. Fixed by wrapping the call in _grade_content_best_effort (never raises; degrades to an UNVERIFIABLE report + an observable prov-grading-failed:<Type> tag) and by removing tool_error_handler.py's blanket DATABASE_URL hint for exception types _classify_error did not recognize as DB-related. The third symptom (reason: "bypass_error" on a SUCCESSFUL store) was investigated and found to be BY DESIGN: bypass_error is the legitimate gate_reason for content matching the error/exception keyword regex (core.thermodynamics.is_error_content) — not an internal failure. Pinned by TestBypassErrorIsNotAnInternalFailure so it isn't "fixed" again later. Fixes #147 New regression tests: tests_py/handlers/test_issue_147_write_class_and_grading.py (13 tests — deliberate-never-rejected at the unit/gate/handler level, grading-survives-missing-cwd at the unit/handler level, no bypass_error misdiagnosis, no misleading DATABASE_URL hint on non-DB errors). Co-Authored-By: Claude <noreply@anthropic.com>
cdeust
added a commit
that referenced
this pull request
Jul 17, 2026
Pre-tag guard on the exact release tree 4e3a202: LongMemEval MRR 0.9166/R@10 0.9820 PASS; LoCoMo 3-run mean MRR 0.7998/R@10 0.9135 — MRR mean sits 0.0002 below the floor-tolerance threshold and was adjudicated as sampling noise by explicit maintainer decision (delta vs 4.14.2 mean −0.0011 < SEM ~0.0013; rep1 0.8013 vs 0.8015; reranker loaded in all MANIFESTs; no released commit in the LoCoMo harness dependency graph — ingestion via BenchmarkDB, not the #148 write path). BEAM 0.5417, inside the documented noise band. Evidence: benchmarks/results/repro/20260717-v4.14.3-pretag/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BxpvWMSYo2FLCw4c1xw7iJ
cdeust
added a commit
that referenced
this pull request
Jul 17, 2026
…ps bump (#154) * chore(release): prepare v4.14.3 (fix write-gate, launcher-deps; deps bump; refactor split) Bump version 4.14.2 -> 4.14.3 across pyproject.toml, .claude-plugin/plugin.json, .claude-plugin/marketplace.json (SemVer patch: 4 fix/chore/refactor commits since v4.14.2, no new features -- #147/#148, #149/#150, #152, #134/#153). CHANGELOG.md: new [4.14.3] entry per Keep a Changelog, dated 2026-07-17, with a PENDING placeholder in the Verified section for the mandatory pre-tag benchmarks/reproduce.sh non-regression evidence (user mandate 2026-07-10) -- this commit prepares the release, it does not run the bench or cut the tag. marketplace.json version_note for the cortex plugin entry updated to match, same PENDING placeholder. zetetic-team-subagents cross-reference (2.28.1) left unchanged: no open release PR found on cdeust/zetetic-team-subagents and its latest tag (v2.28.1) already matches the pinned version. Co-Authored-By: Claude <noreply@anthropic.com> * chore(release): commit v4.14.3 pre-tag bench evidence and adjudication Pre-tag guard on the exact release tree 4e3a202: LongMemEval MRR 0.9166/R@10 0.9820 PASS; LoCoMo 3-run mean MRR 0.7998/R@10 0.9135 — MRR mean sits 0.0002 below the floor-tolerance threshold and was adjudicated as sampling noise by explicit maintainer decision (delta vs 4.14.2 mean −0.0011 < SEM ~0.0013; rep1 0.8013 vs 0.8015; reranker loaded in all MANIFESTs; no released commit in the LoCoMo harness dependency graph — ingestion via BenchmarkDB, not the #148 write path). BEAM 0.5417, inside the documented noise band. Evidence: benchmarks/results/repro/20260717-v4.14.3-pretag/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BxpvWMSYo2FLCw4c1xw7iJ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause (two independent defects, not one)
1.
write_class="deliberate"novelty-rejected — contract violationhandlers/remember.py:167-169computesresolved_write_classbut neverthreads it into
evaluate_gate(handlers/remember.py:193-194pre-fix) ->_compute_gate_decision->core/write_gate.py::determine_bypass(
write_gate.py:115-133pre-fix), which had zero knowledge of write_class— it only bypasses on
force/ error-keyword content / decision-keywordcontent /
important/criticaltags. A plain, low-noveltydeliberatewrite (or one that omits
write_class, which source-falls-back todeliberatepercore/write_class.py) fell straight through to theordinary novelty threshold and got
below_threshold-rejected, contradictingthe tool's own documented contract ("deliberate — NEVER rejected by the
gate for low novelty; near-duplicates are still merged/linked/superseded by
curation").
Fix: thread the already-resolved
write_classthroughevaluate_gate/_compute_gate_decision/determine_bypass.determine_bypassnowbypasses on
write_class == "deliberate", checked last (afterforce/error/decision/tag) so a more specific, more informative bypass
reason still wins when it applies — a deliberate write with error-shaped
content still reports
bypass_error, not the genericbypass_write_class_deliberate.2.
force=TrueintermittentFileNotFoundError+ misleading DATABASE_URL hinthandlers/remember_helpers.py::insert_and_post_process(line 717-719pre-fix) calls:
unconditionally, for every successful insert (create/supersede), as
part of the M-D5 write-time provenance-grading step. This was the one
enrichment call in that function not wrapped in the same defensive
try/except pattern already used for its siblings (
source_attributionclassification a few lines below, the habituation signature a few lines
after that).
os.getcwd()raisesFileNotFoundError: [Errno 2] No such file or directorywhen the process's current working directory has beenremoved — e.g. a worktree cleaned up mid-session — which is exactly the
observed intermittence (a call at ~12:30 succeeded because the cwd still
existed; two calls at ~14:00 failed because it had since been removed).
This is unrelated to the DB and unrelated to
write_class; it happensbefore the row is ever inserted, so the write is lost entirely.
The escaped exception then hit
tool_error_handler.py::safe_handler'sgeneric branch, which appended
"If this persists, check that PostgreSQL is running and DATABASE_URL is set correctly."to any unclassifiedexception type — including
FileNotFoundError— misleading a user chasinga genuine filesystem issue into checking a database that was healthy the
entire time.
Fix:
_grade_content_best_effortwraps theos.getcwd()+grade_from_contentcall; it never raises. On failure it returns a fallback
UNVERIFIABLEProvenanceReportand the caller appends an observableprov-grading-failed:<ExceptionType>tag to the row (never silentlyabsorbed — matches the repo's anti-silent-fallback standard).
tool_error_handler.py::safe_handlerno longer appends the DATABASE_URLhint for exception types
_classify_errordidn't recognize as DB-related— the two genuinely DB-related categories already carry their own
actionable guide text as the message body.
Symptom 3 —
reason: "bypass_error"on a successful store: not a bugThe issue speculated this was evidence of an internal exception in the
bypass machinery. Investigated and confirmed by design:
bypass_erroris thegate_reasondetermine_bypassreturns whenevercontent matches
core/thermodynamics.py::is_error_content'serror/exception-keyword regex — a legitimate, existing, tested bypass path
(
tests_py/core/test_write_gate.py::TestDetermineBypass::test_error_content_bypasses).Two unrelated symptoms shared this label by pure name collision with the
observer's hypothesis, not by a shared code path. Pinned with
TestBypassErrorIsNotAnInternalFailureso it isn't "re-fixed" later.Files changed
mcp_server/core/write_gate.py—determine_bypassgains awrite_classparameter, checked last.mcp_server/handlers/remember.py— threadsresolved_write_classintoevaluate_gate.mcp_server/handlers/remember_helpers.py—evaluate_gate/_compute_gate_decisionaccept and forwardwrite_class; new_grade_content_best_effortwraps the provenance-grading call.mcp_server/tool_error_handler.py— removes the misleading DATABASE_URLhint for unclassified, non-DB exception types.
tests_py/handlers/test_issue_147_write_class_and_grading.py— 13 newregression tests.
Test plan
pytest tests_py/handlers/test_issue_147_write_class_and_grading.py— 13/13 passpytest tests_py/core/test_write_gate.py tests_py/handlers/test_remember.py tests_py/handlers/test_remember_provenance_at_write.py tests_py/handlers/test_remember_helpers_novelty_normalize.py tests_py/handlers/test_remember_helpers_silent_failures.py tests_py/server/test_tool_error_handler_classification.py— 128/128 passpytest -q— 5178 passed, 2 pre-existing failures(
tests_py/hooks/test_hook_receipts.py::test_agent_briefing_emits_receipt_with_marker,::test_agent_briefing_skips_superseded_prior_work) confirmed failingidentically on unmodified
main(unrelated to this change — verified viagit stash+ targeted re-run before restoring the diff).ruff check .— cleanruff format --checkon all touched files — cleanOut of scope / self-flagged
mcp_server/handlers/remember_helpers.py(935 lines) andmcp_server/core/write_gate.py(450 lines) already exceed the 300-linefile cap on
main, pre-dating this change. Not touched beyond theminimal diff required for this fix — a full split is a separate,
much larger refactor out of this PR's blast radius.
Fixes #147
🤖 Generated with Claude Code