Skip to content

fix(remember): honor write_class=deliberate gate contract; grading never blocks write - #148

Merged
cdeust merged 1 commit into
mainfrom
fix/issue-147
Jul 15, 2026
Merged

fix(remember): honor write_class=deliberate gate contract; grading never blocks write#148
cdeust merged 1 commit into
mainfrom
fix/issue-147

Conversation

@cdeust

@cdeust cdeust commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Root cause (two independent defects, not one)

1. write_class="deliberate" novelty-rejected — contract violation

handlers/remember.py:167-169 computes resolved_write_class but never
threads it into evaluate_gate (handlers/remember.py:193-194 pre-fix) ->
_compute_gate_decision -> core/write_gate.py::determine_bypass
(write_gate.py:115-133 pre-fix), which had zero knowledge of write_class
— it only bypasses on force / error-keyword content / decision-keyword
content / important/critical tags. A plain, low-novelty deliberate
write (or one that omits write_class, which source-falls-back to
deliberate per core/write_class.py) fell straight through to the
ordinary novelty threshold and got below_threshold-rejected, contradicting
the 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_class through evaluate_gate /
_compute_gate_decision / determine_bypass. determine_bypass now
bypasses on write_class == "deliberate", checked last (after
force/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 generic
bypass_write_class_deliberate.

2. force=True intermittent FileNotFoundError + misleading DATABASE_URL hint

handlers/remember_helpers.py::insert_and_post_process (line 717-719
pre-fix) calls:

grade_report = validate_memory.grade_from_content(
    content, directory_context=directory, base_dir=directory or os.getcwd()
)

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_attribution
classification a few lines below, the habituation signature a few lines
after that). os.getcwd() raises FileNotFoundError: [Errno 2] No such file or directory when the process's current working directory has been
removed — 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 happens
before the row is ever inserted, so the write is lost entirely.

The escaped exception then hit tool_error_handler.py::safe_handler's
generic branch, which appended "If this persists, check that PostgreSQL is running and DATABASE_URL is set correctly." to any unclassified
exception type — including FileNotFoundError — misleading a user chasing
a genuine filesystem issue into checking a database that was healthy the
entire time.

Fix:

  • _grade_content_best_effort wraps the os.getcwd() + grade_from_content
    call; it never raises. On failure it returns a fallback UNVERIFIABLE
    ProvenanceReport and the caller appends an observable
    prov-grading-failed:<ExceptionType> tag to the row (never silently
    absorbed — matches the repo's anti-silent-fallback standard).
  • tool_error_handler.py::safe_handler no longer appends the DATABASE_URL
    hint for exception types _classify_error didn'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 bug

The issue speculated this was evidence of an internal exception in the
bypass machinery. Investigated and confirmed by design:
bypass_error is the gate_reason determine_bypass returns whenever
content matches core/thermodynamics.py::is_error_content's
error/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
TestBypassErrorIsNotAnInternalFailure so it isn't "re-fixed" later.

Files changed

  • mcp_server/core/write_gate.pydetermine_bypass gains a
    write_class parameter, checked last.
  • mcp_server/handlers/remember.py — threads resolved_write_class into
    evaluate_gate.
  • mcp_server/handlers/remember_helpers.pyevaluate_gate /
    _compute_gate_decision accept and forward write_class; new
    _grade_content_best_effort wraps the provenance-grading call.
  • mcp_server/tool_error_handler.py — removes the misleading DATABASE_URL
    hint for unclassified, non-DB exception types.
  • tests_py/handlers/test_issue_147_write_class_and_grading.py — 13 new
    regression tests.

Test plan

  • pytest tests_py/handlers/test_issue_147_write_class_and_grading.py — 13/13 pass
  • pytest 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 pass
  • Full suite: pytest -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 failing
    identically on unmodified main (unrelated to this change — verified via
    git stash + targeted re-run before restoring the diff).
  • ruff check . — clean
  • ruff format --check on all touched files — clean

Out of scope / self-flagged

  • mcp_server/handlers/remember_helpers.py (935 lines) and
    mcp_server/core/write_gate.py (450 lines) already exceed the 300-line
    file cap on main, pre-dating this change. Not touched beyond the
    minimal diff required for this fix — a full split is a separate,
    much larger refactor out of this PR's blast radius.
  • The recall/scoring path was not touched by this fix.

Fixes #147

🤖 Generated with Claude Code

…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
cdeust merged commit 3f20e29 into main Jul 15, 2026
23 of 24 checks passed
@cdeust
cdeust deleted the fix/issue-147 branch July 15, 2026 13:40
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remember: write_class='deliberate' contract violated (below_threshold rejection) and force=true intermittently raises FileNotFoundError

1 participant