Skip to content

fix: stop evicting in-flight curation decisions on an unrelated publish - #306

Merged
Barsoomx merged 2 commits into
masterfrom
fix/shortlist-revalidation-granularity
Jul 30, 2026
Merged

fix: stop evicting in-flight curation decisions on an unrelated publish#306
Barsoomx merged 2 commits into
masterfrom
fix/shortlist-revalidation-granularity

Conversation

@Barsoomx

Copy link
Copy Markdown
Owner

What

Shortlist revalidation compares the shortlist the verdict actually depended on — its entries
and comparison_complete — instead of a hash that also includes a project-wide memory count.

_manifest_hash and everything it hashes are unchanged.

Why

Measured on the stand: in one 20-minute window, 46 successful curation derivations against 45
stale_decision
. Roughly half of all judged candidates were thrown away after the provider
call was paid for. At the observed rate that was the single largest remaining source of
discarded paid inference — ~$1.8/hour with a 74% discard rate.

_manifest_hash (curation_shortlist.py:181) hashed three things:

authorized_corpus_count   <-- a bare COUNT of every authorized memory in the project
comparison_complete
entries[]

_revalidate_shortlist rebuilt the shortlist inside the settle transaction and required the
whole hash to match. So any newly published memory anywhere in the project bumped the count
and evicted every curation decision then in flight — including decisions about candidates the
new memory has nothing to do with and would never have appeared alongside. With concurrent
workers on one project, a single publish wasted the calls racing beside it.

authorized_corpus_count is not an input to any decision. Every rung of derive_decision reads
candidate tier, per-target facts and comparison_complete; _apply_evidence_policy reads the
same. The count exists for telemetry and for the stored manifest identity.

Soundness

The shortlist is top-K (MAX_ENTRIES = 12). A new memory that would have ranked into it changes
entries and is still caught. A memory that ranks below the cut could not have appeared in the
shortlist the judge saw, so it cannot change the relation set that was judged. Dropping the bare
count therefore removes evictions that could not have affected the verdict, and only those.

Fences deliberately left intact

  • _manifest_hash itself — it is stored on CurationDecision.comparison_manifest_hash, carried
    on CurationJudgeResult, and compared in _validate_verdict. Changing it would invalidate
    stored records and that parity check. The new shortlist_revalidation_hash is separate.
  • _shortlist_memory_fence — target advanced still means stale_decision.
  • _candidate_fence, _acquire_targetless_publication_lock, and the _validate_verdict
    manifest parity check are untouched.
  • revalidate_curation_shortlist still returns False on CurationShortlistError.

Tests

  • revalidation succeeds when an unrelated authorized memory appears after the freeze (the defect)
  • revalidation fails when a selected entry's transition advances
  • revalidation fails when a selected entry's body changes without a transition
  • revalidation fails when a new memory displaces a shortlist entry
  • revalidation fails when comparison_complete regresses
  • settle path: a publish commits while an unrelated memory appears mid-flight
  • settle path: a publish is still stale_decision when the appearing memory would have been
    shortlisted
  • a verdict judged against a different manifest is still stale_decision

Verified: pytest -m "not transactional" → 3490 passed; pytest -m transactional → 89 passed;
engram_curator_eval → all thresholds pass; ruff check and ruff format --check clean.

Operational note

Curation is currently paused on the stand via ENGRAM_CANDIDATE_DECISION_ENABLED=0 (B-012),
because it was burning ~$43/day at a 74% discard rate. The flag lives only in the server's
.env, which a release overwrites from the local copy, so deploying this change lifts the pause
automatically and the blocked works clear on the configuration-fingerprint change.

Barsoomx added 2 commits July 30, 2026 02:15
Revalidation currently compares the whole comparison manifest, which includes
authorized_corpus_count, a project-wide COUNT of authorized memories. Any
publish anywhere in the project bumps it and evicts every curation decision in
flight, after the judge call has already been paid for.

Red before the fix:
  - test_revalidation_ignores_unrelated_authorized_memory_added_after_freeze
    (predicate returns False although entries and comparison_complete are
    byte-identical)
  - test_empty_shortlist_publish_commits_when_unrelated_memory_appears
    (settle raises stale_decision: authorized shortlist changed before
    settlement)

The remaining new tests are regression guards for the fences that must survive
the change: entry transition advance, in-place body rewrite, a new memory
displacing an entry, comparison_complete regressing, and the full manifest
parity check in _validate_verdict.
revalidate_curation_shortlist compared the whole comparison manifest, and that
manifest hashes authorized_corpus_count - a COUNT of every authorized memory in
the project. So any publish anywhere in the project evicted every curation
decision in flight, after its judge call was already paid for. On the stand that
was 45 stale_decision against 46 successful derivations in one 20-minute window.

The count is not an input to any decision. build_derivation_facts,
feasible_outcomes, derive_decision and _apply_evidence_policy read the candidate
tier, comparison_complete and the per-entry facts, and nothing reads the count.
It only travels as an informational field in the judge prompt envelope and as an
unread field on CurationJudgeResult.

Revalidation now compares a separate hash over the entries and
comparison_complete. Why entries-only is sound: the shortlist is top-K
(MAX_ENTRIES=12), so a new memory that would have ranked into it changes the
entries and is still caught; a memory ranking below K could not have entered the
comparison set the model judged, so it cannot change the relation set. The
count check was never the mechanism that prevented a duplicate either - it
aborts on any change without distinguishing what changed, and a retry that
rebuilds the shortlist reaches the same verdict, so it delayed rather than
prevented. Two remaining, deliberate limits: this is exactly as strong as the
shortlist's own recall (a true duplicate the legs never retrieve was never in
the manifest to begin with), and a freshly published memory still flips
comparison_complete to False until its embedding lands, which keeps evicting
in-flight decisions that were frozen with comparison_complete=True.

_manifest_hash is untouched, byte for byte: it is stored on
CurationDecision.comparison_manifest_hash, carried on CurationJudgeResult, and
_validate_verdict still compares it in full, so a verdict judged against another
shortlist is still stale_decision. The per-entry payload is now shared by both
hashes, and canonical_json_bytes sorts keys, so the manifest bytes cannot drift.
Every other fence is unchanged: _shortlist_memory_fence, _candidate_fence,
_acquire_targetless_publication_lock, and CurationShortlistError still returning
False.

The settle-path stub follows production instead of the count: it now asks
whether a newly authorized memory would rank into this shortlist, modelled by a
title match, which keeps the concurrent targetless publication race honest -
two identical candidates still serialize and the loser is still relisted.
@Barsoomx

Copy link
Copy Markdown
Owner Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

Affected Assets, Files, and Routes:

view changes for bundle: engram-frontend-client-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
server/middleware-*.js 2.72kB 3.21kB 548.89% ⚠️
server/middleware-*.js -2.72kB 495 bytes -84.59%
static/qGnLcJ8hWrta19wOF19jp/_buildManifest.js (New) 1.51kB 1.51kB 100.0% 🚀
static/qGnLcJ8hWrta19wOF19jp/_ssgManifest.js (New) 77 bytes 77 bytes 100.0% 🚀
static/LjnlkYCfPCZsainkYH8p_/_buildManifest.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️
static/LjnlkYCfPCZsainkYH8p_/_ssgManifest.js (Deleted) -77 bytes 0 bytes -100.0% 🗑️

@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.10%. Comparing base (ae40b22) to head (f916cf7).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #306   +/-   ##
=======================================
  Coverage   91.09%   91.10%           
=======================================
  Files         223      223           
  Lines       23344    23350    +6     
=======================================
+ Hits        21266    21273    +7     
+ Misses       2078     2077    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Barsoomx
Barsoomx merged commit 6179d90 into master Jul 30, 2026
14 checks passed
@Barsoomx
Barsoomx deleted the fix/shortlist-revalidation-granularity branch July 30, 2026 01:21
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.

2 participants