Skip to content

feat(mental-models): decision-model gate in front of a delta page refresh - #5698

Merged
nicoloboschi merged 6 commits into
mainfrom
feat/gated-delta-refresh
Oct 19, 2026
Merged

nicoloboschi merged 6 commits into
mainfrom
feat/gated-delta-refresh

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

A gate in front of a delta knowledge-page refresh. The refresh reads the memories that are new since its last run. It then asks the decision model, one memory at a time: could this change the page? Only the memories it keeps reach the delta LLM call, and the reflect loop does not run. When none are kept, there is no LLM call at all, and the refresh still moves past those memories.

It is on whenever a decision model is set up, like the decision-model paths for reflect and consolidation. A bank opts out with its own setting, mental_model_delta_use_decision_model=false (HINDSIGHT_API_MENTAL_MODEL_DELTA_USE_DECISION_MODEL), separate from the refresh's reflect setting mental_model_refresh_use_decision_model. In these cases it falls back to the normal reflect path:

  • the memory store can't list memories;
  • more than 200 memories arrived;
  • any decision-model call fails.

It builds on #5382, which is merged.

Why

Decision models are about 1000x cheaper than the LLM. A delta refresh used to pay for a full reflect loop to work out what changed. The gate reads the new memories directly and makes 0 or 1 LLM calls.

Results

The model under test is gemini-3.1-flash-lite, judged by gemini-2.5-flash. The decision model is TypeSafe Jev. There is one run per arm, so treat these numbers as directional.

Cost of one delta refresh (test_05, 20 new memories):

LLM calls Input tokens Time Page after
#5382 alone 3 17,549 10.7s 3,680 chars, 22 citations
with the gate 1 4,871 8.0s 3,838 chars, 34 citations

test_01, page convergence: 6/8 without the gate, 7/8 with it. The gate run hit no traps. The run without it hit one, owner-latest.

test_11 (new), a page takes the update: 4/6 without the gate, 6/6 with it. Reflect's search missed "Dana Reyes left the company" and "Kestrel Labs moved to the Team plan", because neither one names the page's topic. The gate judges each memory against the page itself, which does name Dana and Kestrel. How many memories it kept varied by page, from 0 to 51 of 82.

hindsight-decision (our inference API) is not ready for this. It kept 0 of 20 on-topic memories in test_05. In test_01 it scored 5/8 and hit 2 traps.

New eval: test_11_delta_updates

Each case starts with a page that is already written. Then a wave arrives that changes it, mixed with the hard corpus as noise. There are six cases:

  • a memory that only the page links to the topic;
  • a rename followed by an update that uses the new name;
  • a correction of an earlier figure;
  • a list that gains one member and loses another;
  • a new item the page has never mentioned;
  • a quiet wave with nothing relevant.

Before grading the update, it checks that the wave-1 page stated the old value.

Also in this PR

  • Directives still apply. The bank's directives reach the gated edit, the same way they reach reflect.
  • Sibling pages are not offered. A sibling page only changes when its own memories do, and those memories are already in scope.
  • Failure reasons are kept. If a gated edit fails, the refresh records why, for example delta_ops_failed, instead of "empty candidate".
  • One failure stops the rest. If one decision-model call fails, the other calls are cancelled, and the refresh falls back to reflect.
  • System story test_48_delta_gate. It runs the gate on a real server, with the stub answering the decision model. Only the memory that changes the page reaches the edit, and when none does, there is no LLM call.

Not in this PR

  • Batching the decision-model calls. Above 200 new memories, the refresh falls back to reflect.
  • Counting the decision-model calls in the refresh's usage. No decision-model call is counted there yet.

@nicoloboschi
nicoloboschi force-pushed the feat/gated-delta-refresh branch from 80c65a7 to b8dae31 Compare October 16, 2026 16:47
@koriyoshi2041

Copy link
Copy Markdown
Contributor

The zero-kept path bypasses directives, so the claim that directives still apply is only true when at least one memory passes the gate. _gate_delta_refresh() lists directives inside if kept:; when every new memory is rejected, the refresh makes no edit call, advances its memory watermark, and never presents a newly created/changed directive to the page.

I reproduced this at exact head b8dae3149862 by extending test_mental_model_delta_gate.py: create a directive after the page's previous refresh, make could_change_page() return false for both new memories, then assert that the directive reaches the delta edit. The existing kept-memory directive case passes, while the zero-kept control fails with len(gated.llm) == 0:

test_directives_reach_the_gated_edit                         PASSED
test_directive_is_not_lost_when_all_new_memories_are_dropped FAILED
AssertionError: a current directive must still reach the delta edit

Command: HINDSIGHT_API_DATABASE_URL=pg0://hindsight-5698-review:5557 uv run --frozen --group dev --extra embedded-db --extra local-ml pytest -q -n 0 tests/test_mental_model_delta_gate.py -k 'directives_reach or directive_is_not_lost'

Simply always adding current directives would defeat the zero-LLM optimization for banks that have any directive. Could this either track directive freshness and force an edit when a directive changed, or conservatively fall back to the normal path when directives exist and no memory is kept? A regression should also verify that the memory watermark does not make the missed directive permanent.

…resh

A delta refresh reads the memories new since its last run and asks the decision
model, one memory at a time, whether it could change the page. Only the kept
ones reach the delta LLM call and the reflect loop never runs; none kept means
no LLM call, with the watermark still moved past them. Off by default
(HINDSIGHT_API_MENTAL_MODEL_REFRESH_DELTA_GATE), and it falls back to reflect
when the store can't list memories, the wave is over 200, or the decision
model fails.

Adds system-eval test_10 (a page already written takes the next wave's update).
- the gated path keeps MemoryFact, not raw dicts; test helpers return dataclasses, not tuples
- a failed gated edit reports why (delta_ops_failed), not "empty candidate"
- the bank directives reach the gated edit as they do reflect; sibling pages are documented as not offered
- one failed decision-model question cancels the rest (TaskGroup) instead of letting them retry
- tests: could_change_page, the store read (scope, window, order, limit), the per-bank opt-out, directives, the failed edit
- system story test_48: the gate on a real server through the client, with the stub answering it from verdicts
- direct-SQL test setup says why; _ScopeWhere moved above its users; no getattr on a known field
- test_48 asserts the whole dry-run result (mode, outcome, preview, applied ops, warnings)
- the gate's failure log names the failed question, not the TaskGroup wrapper
- ponytail note: the gate's decision-model calls are not in the refresh's usage, like every other decision-model call
- delta_updates.py formatted to the 120-column limit
- Drops HINDSIGHT_API_MENTAL_MODEL_REFRESH_DELTA_GATE. Like reflect and consolidation, the
  gate runs once a decision model is set up; banks opt out with
  mental_model_refresh_use_decision_model.
- Docs: a Knowledge pages paragraph under Decision model; the refresh switch row says what it
  now covers.
- System story runs on the existing decision_model_server; the extra server is gone.
@nicoloboschi
nicoloboschi force-pushed the feat/gated-delta-refresh branch from b8dae31 to c9b8047 Compare October 19, 2026 09:38
- mental_model_delta_use_decision_model (HINDSIGHT_API_MENTAL_MODEL_DELTA_USE_DECISION_MODEL,
  default true) turns the gate off for a bank without touching the refresh's reflect, which
  keeps mental_model_refresh_use_decision_model.
- Wired through the bank config API, Python/TypeScript clients, OpenAPI, bank template schema,
  env templates and docs.
- Test: turning off the refresh's switch leaves the gate on.
- Ask whether a decision model exists before loading the bank config.
- System story: a bank that opts out of the gate refreshes with reflect.
- Unit test: neither the refresh's nor reflect's decision switch turns the gate off.
- test_11 on/off compares the gate alone: both runs keep the decision model, off sets
  MENTAL_MODEL_DELTA_USE_DECISION_MODEL=false.
- Re-wrapped docstrings and README lines; the story no longer mentions the removed server.
@nicoloboschi
nicoloboschi merged commit af7e6be into main Oct 19, 2026
117 checks passed
@nicoloboschi
nicoloboschi deleted the feat/gated-delta-refresh branch October 19, 2026 11:47
@nicoloboschi nicoloboschi mentioned this pull request Nov 3, 2026
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