fix(smem-hygiene): remember the gate's refusals, not only its saves - #213
Merged
acidkill merged 1 commit intoSep 7, 2026
Conversation
The auto-capture hooks recorded a fragment in the idempotency ledger only when the write gate accepted it. A refusal left no trace, so the next Stop in the same session re-extracted the same fragment, re-encoded it and asked the gate again — and the gate, being deterministic, refused it again. A session that produced one rejected fragment paid for it on every subsequent Stop, and PreCompact repeated the whole exercise from the beginning. Refusals are now recorded the same way accepted content is. capture_state gains the vocabulary for it, stop.py records the refusal in its main loop, and pre_compact.py honours a refusal that Stop already recorded instead of re-deciding it. There is one deliberate rough edge, called out rather than hidden: when every detected fragment was duplicate-filtered, the run reports the "duplicate content already captured" status and does not fall through to a session summary. Replacing an accurate duplicate status with a brand-new summary save would be worse than the slight imprecision of reusing the message, and a separate counter for the two cases seemed more machinery than the distinction is worth. Say the word if you would rather have it split. The tests are written to fail without the fix, which the first version of them did not manage. Both refusal tests now count gate invocations and force the path through the gate: an earlier shape of test_pre_compact_honours_refusal_recorded_by_stop passed with the fix entirely removed, because PreCompact never reached the gate on that input and the assertion could not tell that apart from success. It now asserts the gate was actually called before asserting it was not called again. Also drops a stale absolute path from this module's docstring, since the file is being edited anyway.
acidkill
approved these changes
Sep 7, 2026
acidkill
left a comment
Owner
There was a problem hiding this comment.
Reviewed the full diff. Rejection keys namespaced by threshold are the right shape — remembered refusals without silencing content if auto_capture_min_score is later lowered; shared seen-set across Stop and PreCompact kept consistent.
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.
Summary
PreCompacthonours a refusalStopalready recorded, and records its own.Why
The ledger from #115 remembered what the gate accepted. A refusal left no trace at all, so the next Stop in the same session re-extracted the same fragment, re-encoded it, and put it to the gate again — which, being deterministic, refused it again.
PreCompactthen repeated the exercise from the top. Nothing crashes and nothing corrupts; the gate simply does the same homework on the same content forever, which is a shame in a mechanism that exists to stop precisely that.Measured on one live brain over twenty-four hours on 2026-08-08: 499 auto decisions carried just 134 distinct contents, one fragment having been judged thirty-six times. That inflates the gate's denominator about 3.7-fold and makes its observed accept rate look several times worse than it is — 0.39% counted over rows against roughly 1.4% over distinct content. The wasted encoding is the smaller half of the cost; the misleading acceptance statistic is the half that gets acted upon.
Changes
hooks/capture_state.py: vocabulary for recording a refusal, keyed so that it is scoped to the threshold that produced it. A refusal recorded under one threshold does not suppress a re-judgement after the threshold changes, whilst an acceptance survives such a change.hooks/stop.py: the main capture loop records the refusal alongside the outcomes it already recorded.hooks/pre_compact.py: honours a refusal recorded byStoprather than re-deciding it, and records its own refusals the same way.tests/unit/test_capture_state.py(+6) andtests/unit/test_hook_capture_idempotency.py(+4).One rough edge, disclosed rather than hidden
When every detected fragment was duplicate-filtered, the run reports the existing
duplicate content already capturedstatus and does not fall through to a session summary. Reusing that message is slightly imprecise — the two situations are not identical — but replacing an accurate duplicate status with a brand-new summary save would be the worse outcome, and a separate counter looked like more machinery than the distinction earns. Happy to split it if you would rather the two were distinguishable.The module docstring in
test_hook_capture_idempotency.pyalso loses a stale absolute path from a developer's machine. The file is being edited here anyway, so it seemed a good moment.Test plan
pytest tests/unit/test_hook_capture_idempotency.py tests/unit/test_capture_state.py— 43 passed.test_pre_compact_honours_refusal_recorded_by_stoppassed with the fix entirely removed, becausePreCompactnever reached the gate on that input and the assertion could not distinguish that from success. The tests now count gate invocations and assert the gate was called before asserting it was not called again.hooks/pre_compact.py: 2 failed, 14 passed —test_pre_compact_honours_refusal_recorded_by_stopandtest_pre_compact_records_its_own_refusal.hooks/pre_compact.pyandhooks/stop.py: 3 failed, 40 passed — the two above plustest_stop_hook_does_not_rejudge_rejected_content.capture_state.py,pre_compact.pyandstop.pyis executed by the tests above — 12 statements, 0 uncovered. (The modules' own totals are low because they are large hook entry points with a great deal of unrelated code.)pytest tests/ -m "not stress" -n 4against a live SurrealDB v3.2.0 — 7293 passed, 48 skipped, 1 xfailed, which ismain's 7283 plus exactly the ten tests added here. Two tests intests/unit/test_dashboard_brains_scope.pyfail on this branch and onmainalike: they want a live database and collide with one another under-n. Both pass when that file is run on its own.ruff check src/ tests/clean;ruff format --check src/ tests/reports 739 files already formatted.mypy src/ --ignore-missing-imports— success, no issues found in 354 source files.main.CHANGELOG.mduntouched — left to the release entry, as with fix(storage): bind datetimes in time comparisons so they select by value #191–fix(memory): refresh content-derived fields on compress, restore, and refine #193.Related issues
None filed. #115 introduced the ledger this extends; the refusal half was simply never part of it.
Verified by
@RobertSigmundsson