Skip to content

feat(engine): two-tier mental-model refresh - a deterministic fast path that serves most refreshes in one LLM call or none, with the agentic loop as escape hatch - #3290

Open
JoshFunnell wants to merge 13 commits into
vectorize-io:mainfrom
JoshFunnell:upstream/two-tier-refresh
Open

Conversation

@JoshFunnell

@JoshFunnell JoshFunnell commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Mental-model refresh was this deployment's single largest LLM consumer —
~25.1M input tokens/day
(the preserved pre-trial snapshots' figure; an earlier
full-ledger measurement read 27.6M/day but its rows were pruned by the 1-day
trace-retention default before close, so we cite the reproducible number), more than every other engine operation combined —
and most of that spend produced nothing: over a third of refreshes ran the
full multi-call agentic loop to completion and applied zero edits. This
PR adds a two-tier fast path that answers those refreshes in at most one LLM
call, with the existing loop unchanged underneath as the escape hatch, and
carries 22.7 hours of production before/after evidence (the operator called the trial at ~22.7h; every rate below is normalized per day).

Baseline configuration: the baseline already ran delta mode. All 33 models
carried trigger.mode: "delta" throughout the baseline window, and the ledger
confirms it fired on essentially every refresh (39 mental_model_delta_ops
calls against 38 reflect traces over the last 6 hours of the era, the window
where every llm_requests row survived the 1-day retention default). Delta
mode does not address this cost, because it sits downstream of it: use_delta
narrows retrieval via created_after, then adds one more LLM call after the
loop returns, taking the loop's output as its input
(candidate_markdown=reflect_result.text). Over that same window the split was
176 loop calls carrying 5.25M input tokens (93.1%) against 39 delta-ops calls
carrying 388k (6.9%). Delta mode is the write-side protection and it works,
applying 372 operations across 210 completed baseline refreshes. The read-side
cost it sits behind is what this PR targets.

The baseline, measured on a production bank (33 mental models in the
baseline window, event-driven refresh after consolidation, single-tenant PG
deployment; the trial that follows ran on a consolidated roster of 29):

  • the population median refresh read ~101k input tokens for a median of
    ~373 output tokens (per-refresh medians over the baseline window's
    llm_requests ledger) — a ~270:1 read/write ratio at the median; the
    heaviest observed refresh read 372k input tokens across 13 tool-loop
    calls
    (single instance, same window);
  • 36.8% of refreshes applied zero edits (window: 24h pre-intervention,
    ledger query over llm_requests joined to refresh operations) — the full
    loop ran to completion and changed nothing;
  • refresh cost grows with context, not with change: each loop iteration
    resends the whole accumulated conversation.

The pieces needed to skip the loop already exist in the codebase; they were
sequenced behind it instead of in front of it.

Mechanism

The reflect loop's cost is structural: each tool-loop iteration resends the
accumulated context (append-only assembly), so cost grows with iteration
count regardless of how little the refresh changes. The delta window read and
the structured-delta prompt — both already present — can answer most
refreshes in at most one LLM call.

Change (tier lattice)

refresh request (delta mode, baseline present)
  │
  ├─ tier 0: delta window EMPTY → preserve document, advance watermark.
  │          Zero LLM calls. (Designed to absorb the zero-edit share; in the
  │          event-driven trial the windows were never empty and cheap tier 1
  │          absorbed it instead — see Evidence.)
  │
  ├─ tier 1: window non-empty → ONE structured-delta call producing edit
  │          operations applied to the existing document. By construction it
  │          can only edit or preserve — it never writes a synthesised
  │          candidate as the whole document.
  │
  └─ tier 2: anything less than clearly safe hands back to the agentic loop
             unchanged (no baseline, model requests full context, op call or
             parse failure, all ops rejected). Outcomes reachable today stay
             reachable; the fast path changes what a refresh COSTS, not what
             it can produce.

Evidence (22.7h production trial, same bank, same models, same workload)

  • Tier distribution (272 completed refreshes over the 22.7h trial era;
    sampler-verified, deduped by model+last_refreshed_at): tier 0: 0 —
    tier 1: 261 (96.0%) — agentic hand-back: 11 (4.0%)
    , every hand-back
    carrying a fast_path_fallback_reason (the escape hatch fired and was
    never dominant). Cost: tier-1 refreshes ran at the era's 12.6k-token
    median (ledger); the 11 hand-backs rode the full agentic path at the
    old cost class. Tier 0 firing zero times is itself a finding: under
    event-driven scope-matched triggering a refresh window is almost never
    empty, so the old zero-edit class lands on cheap tier 1 instead of
    tier 0 (see the zero-edit line below).
  • Input tokens per refresh, median and p90: 102,086 / 205,810 before →
    12,600 / 16,526 after (~8.1x cheaper at median)
    . Configuration note:
    the after side is the full trial era from the live ledger; the before
    side was measured at the +12h checkpoint from the then-remaining ledger
    window and preserved as a text snapshot — the deployment ran the
    1-day HINDSIGHT_API_LLM_TRACE_RETENTION_DAYS default, which pruned
    the baseline era's rows before trial close (a deployment lesson this
    PR's evidence protocol now carries: snapshot before you rely on the
    trace ledger).
  • Total refresh-lane tokens/day: ~25.1M before → ~5.4M (5.09M over the
    22.7h era; ~4.7x reduction)
    , same bank, heavier-than-usual workload
    (several hundred facts retained during the trial day).
  • Zero-edit refreshes: the design expected tier 0 to absorb them; the
    trial corrected the mechanism. Tier 0 fired 0 times (windows are almost
    never empty under event-driven triggers); instead the zero-edit CLASS
    shrank — 48 of 272 completed refreshes (17.6%) vs 36.8% baseline
    (24h pre-trial window) / 48.1% (full baseline era)
    — and each one now
    costs a single ~12.6k tier-1 call instead of a ~100k agentic run.
  • Retain→fresh-model latency: median 458s → 110s, p90 1,115s → 869s,
    same instrument on both sides (each completed retain paired with the
    first refresh completing after it, capped at 120 min — do not compare
    against other definitions; before side snapshot-sourced, above).
  • Quality: per-change identifier retention probe (regex identifier classes,
    lost identifiers logged verbatim) ran across the trial: 276 change
    events, 14 loss events, 99.8% identifier retention (16,451 kept / 26
    lost / 500 gained)
    . Disposition: the largest loss cluster was the
    block-index mis-target this PR discloses and fixes (two destroyed
    blocks, both restored from history; zero mis-target losses after the
    anchor fix went live mid-trial); the remainder audited as legitimate
    compression or supersession. Streamed calls do not report
    cached_tokens; latency, not that column, is the caching instrument.
    Output-token figures in this body understate true generation on
    reasoning models: thoughts_tokens is billed at the output rate but
    excluded from output_tokens in the ledger.

Relationship to in-tree work

Safety

  • Strictly additive: tier 2 is the unmodified existing path; tiers 0/1 are a
    pre-gate in front of it.
  • Config kill switches: global default plus per-model trigger override
    (delta_fast_path); either disables the pre-gate entirely.
  • The fast path never deletes content it did not target: block operations are
    anchor-guarded (see disclosure 1) and fall back to the loop when rejected.

Disclosures (defects this trial surfaced in the fast path, with fixes in-branch)

  1. Block-op index mis-targeting (Fixes Delta block ops can silently overwrite the wrong block: bare index targeting, range-only validation #3273; commit 75fe395). Delta block operations
    addressed paragraphs by a bare LLM-computed integer index into an
    unannotated serialization, and apply-side validation checked RANGE only —
    a wrong-but-in-range index silently overwrote an unrelated block and the
    fast path committed it as success. Observed once in production (one tier-1
    refresh, two off-by-one ops, both destructive). Fix: ops carry a verbatim
    content anchor compared before mutating (mismatch → logged skip), and the
    serialization annotates each block with its index. Regression tests
    included.
  2. Serving tier was unobservable after the fact (Fixes Refresh serving path is unobservable after the fact: reflect_response is overwritten, result_metadata carries no tier #3274; commit 84462bf). The
    tier lived only on each model's latest reflect_response and was
    overwritten by the next refresh, so tier distribution — the PR's own
    headline evidence — was unrecoverable from the database and had to be
    reconstructed by an external sampler. RefreshMentalModelOutcomeMetadata
    now records serving_tier and fast_path_fallback_reason per operation.
    A two-tier system whose tier cannot be read back cannot be operated or
    measured; we consider this part of the feature, not an extra.
  3. Scope grammar gained an entity leaf (4caca0b, separable). Trial
    observation: on a bank whose tag vocabulary is a handful of broad topics,
    tag scopes cannot isolate a subject (best tag scope for one subject: 25%
    precision against a 15.5% base rate; the entity association on the same
    corpus: 94.2% precision / 86.2% recall). TagGroupEntityLeaf
    ({entities: [names], match: any|all}) joins the existing recursive
    tag-group grammar, riding every surface it already reaches — refresh
    scope, the staleness gate, retrieval filtering. Association is
    inheritance-aware (direct postings OR through source_memory_ids, the
    lane observations use by design). Entity leaves are barred under not at
    the API edge because two surfaces evaluate them permissively (rationale in
    code). 22 tests. Happy to split into its own PR if preferred.

Open questions for maintainers

  • Should the fast path default ON (mental_model_delta_fast_path = true) or
    ship opt-in?
  • needs_full_context as the hand-back reason name — better ideas welcome.
  • The now-dead DELTA_SYSTEM_PROMPT constant: remove in this PR or leave for
    a cleanup pass?
  • Disclosure 3 in-PR or split?

`_execute_mental_model_refresh` assembled its result through a `_finish`
closure defined *after* `reflect_async` returned, capturing the post-reflect
locals (`reflect_result.text/.tool_trace/.llm_trace/.usage`, plus the derived
fact counts and reflect_response payload). Every branch that finishes a
refresh therefore has to run after the agentic loop, by construction.

Split that into two frozen dataclasses and a free function:

- `_MentalModelRefreshContext` — identity, scope, window, watermark and start
  time: what the pipeline resolved before it branched on mode or outcome.
- `_MentalModelRefreshEvidence` — candidate text, fact counts, tool/LLM traces
  and usage: what produced the candidate, whichever path produced it. The two
  values later stages still mutate (`reflect_response`, `warnings`) stay out of
  it and are passed per branch, so nothing holds a snapshot that keeps changing.
- `_build_refresh_run(ctx, evidence, ...)` — the former closure body.

Behaviour-identical: all four existing call sites pass exactly what they
previously captured, and the assembled `_MentalModelRefreshRun` is unchanged
field for field.
…fresh

A delta-mode refresh ran the full agentic reflect loop unconditionally, even
though delta mode's premise is incremental work. Measured over 24h on a
production bank: 4.29 LLM calls per refresh at a median 101k input tokens
against 373 output tokens, because each loop call resends the whole accumulated
conversation — and 36.8% of those refreshes applied zero operations, i.e. the
loop ran to discover there was nothing to write.

Every piece needed to skip it was already here, sequenced behind the loop
rather than in front of it. `_try_delta_fast_path` puts two bounded tiers ahead
of `reflect_async`, for eligible delta refreshes only:

- Tier 0 reads the delta window with the same two typed retrieval calls the
  loop's tools make (`recall` under `recall_max_tokens`, `search_observations`
  under the tool-call budget, both `internal=True`). An empty window means
  nothing to integrate: the document is preserved and the watermark advances,
  the same outcome the loop reaches after paying for it. Zero LLM calls.
- Tier 1 hands those facts and the current document to the existing
  structured-delta prompt in one call and applies what comes back.

Anything less than clearly safe hands back to the loop, which then produces the
result exactly as it does today: no readable baseline, the model setting the new
`needs_full_context` escape hatch, a failed call or parse, or operations that
all bounced. So this changes what a refresh COSTS, not which outcomes are
reachable — tier 2 is untouched, and full mode never enters the fast path.

By construction the fast path can only apply operations to the existing
document or preserve it. It never writes a synthesised candidate as the whole
document, so the narrow-candidate and empty-answer classes the agentic path
guards against cannot arise on it.

Also here, because they are inseparable from the above:

- `needs_full_context` on `DeltaOperationList`, threaded through all three
  branches of `parse_delta_operation_list` and `_finalize_operations`. Each
  branch rebuilds the list from the operations it validated, so a flag left on
  the raw payload would be dropped in silence and the fast path would apply
  edits the model had just said it could not make safely.
- `STRUCTURED_DELTA_FAST_PATH_SYSTEM_PROMPT` as an addendum, leaving the shared
  prompt byte-identical: the agentic path does not read the flag, so a model
  answering "I cannot do this properly" there would be ignored.
- The structured-delta call is now bound through `with_config(...)` so it logs
  under the refresh's operation. Called bare it recorded a blank operation in
  `llm_requests` (measured: 197 blank-op calls per 48h), which made delta-ops
  calls impossible to attribute or count.
- `fast_path` / `fast_path_fallback_reason` on the trace and dry-run models and
  in `reflect_response`, as their own vocabularies. Deliberately NOT folded into
  `ModeFallbackReason`/`RefreshOutcome`: a tier-1 hand-back is not a mode
  fallback (the mode is still delta) and no outcome changed, so every existing
  consumer of those literals stays valid.
- Kill switches: `HINDSIGHT_API_MENTAL_MODEL_DELTA_FAST_PATH` (hierarchical,
  default true) and per-model `trigger.delta_fast_path` (null inherits).
…ol plane

Follows the repo's config-field procedure for the new knob and the dataplane
rule that an API change updates the control plane alongside it.

- `configuration.md`: a row for `HINDSIGHT_API_MENTAL_MODEL_DELTA_FAST_PATH`,
  in its own subsection next to the internal-recall knobs it sits beside — same
  shape (hierarchical, plus a per-model trigger override).
- `.env.example` + the byte-identical `hindsight-embed` copy, which the
  `test_bundled_template_matches_repo_root` sync test enforces.
- `mental-models.mdx`: the `delta_fast_path` trigger row, a "what a delta
  refresh costs" section under Refresh Mode, and the two new fields in both the
  dry-run and stored-trace field tables.
- Regenerated `openapi.json` and `bank-template-schema.json`, and mirrored the
  two doc edits into the generated `skills/hindsight-docs` copies.
- Control plane: `MentalModelFastPathTier` / `FastPathFallbackReason` types,
  `delta_fast_path` on all five mental-model trigger declarations, the two new
  keys on the trace and dry-run result, and a pass-through of both in
  `TraceSummary`. Rendered as the raw API values in mono, like `effective_mode`
  immediately above them, rather than translated prose: these are the enum names
  the dry-run payload, the stored trace and the docs all use, so a reader
  comparing this line to a `reflect_response` sees the same token. No new UI
  controls.
The Rust client is generated at build time from
`hindsight-docs/static/openapi.json`, so adding `delta_fast_path` to
`MentalModelTrigger` gives `types::MentalModelTriggerInput` a new field
immediately. All three CLI construction sites are exhaustive struct literals
with no `..Default::default()`, so each stops compiling until the field is
named. `cargo check` on the CLI is clean with these three lines.
…ne index corruption

Block-level delta ops (replace_block/insert_block/remove_block) addressed
blocks by a bare LLM-computed integer index, validated only for range.
A wrong-but-in-range index was indistinguishable from a correct one, and
the model had to silently count array elements in a compact, unannotated
document dump to know a block's index in the first place. Measured in
production: two off-by-one replace_block ops landed exactly one position
before their intended targets deep in long sections, silently destroying
unrelated paragraphs.

Two-part fix:
- replace_block/remove_block, and insert_block when its index names an
  existing block, now carry a required `anchor`: a verbatim (whitespace-
  normalized) excerpt of the block the model believes is at that index.
  apply_operations checks it against the block actually there before
  mutating, and skips the op (visible in the summary like every other
  skip) on a mismatch or a missing anchor -- fails closed for ops from a
  model/caller that never adopted the anchor.
- The document JSON shown to the model (serialize_document_for_delta_prompt,
  used at both delta-prompt call sites in memory_engine.py) now annotates
  each block with its own index, so the model reads its position instead
  of counting. The annotation is prompt-only: Block schemas keep
  extra="forbid", so it can never round-trip back into the persisted
  document.

Extends tests/test_structured_doc.py::TestApplyOperations with a
regression test reproducing the measured defect shape (off-by-one index
whose anchor names the neighboring block), anchor mismatch/missing/
whitespace-normalization coverage for all three ops, and a new
TestSerializeDocumentForDeltaPrompt class covering the index annotation
and its non-leakage into the persisted schema. Updated the prompt
contract (prompts.py) and the feature's own docs (mental-models.mdx +
its generated skills/ mirror, hand-applied after the mdx edit -- the
generator script rewrites all 89 mirrored files with Windows path
artifacts on this host, well outside this patch's scope).

RED-proven: temporarily disabled the anchor check and confirmed the
off-by-one regression test (and 4 other anchor tests) fail exactly as
expected -- the wrong block gets silently overwritten -- before
restoring and confirming green.

Test counts: tests/test_structured_doc.py 51 passed (was 36);
tests/test_mental_model_delta.py 34 passed, 4 skipped (unchanged,
skips require GEMINI_API_KEY/OPENAI_API_KEY);
tests/test_structured_delta_prompt_budget.py 2 passed (unchanged).
ruff format --check and ruff check clean on all changed files.
A two-tier refresh system whose tier is unobservable after the fact cannot be
operated or measured. The tier was only readable from
mental_models.reflect_response.fast_path, which holds each model's LATEST
refresh and is overwritten by the next one -- so tier distribution over any
window was unrecoverable from the database, and had to be reconstructed by an
external sampler polling every 5 minutes.

RefreshMentalModelOutcomeMetadata now carries:
  serving_tier              "tier0" | "tier1" | "tier2"
  fast_path_fallback_reason why the fast path handed back, on tier2

The agentic path reports fast_path=None; that is normalised to "tier2" so the
persisted value names the tier that ran rather than the absence of a flag --
a null would be ambiguous between "agentic loop" and "written by a build
predating this field".

Both fields are optional, so existing callers are unaffected.

tests/test_refresh_outcome_metadata.py drives the real writer against a fake
connection and asserts on the JSON actually persisted, including the
None -> tier2 normalisation and the missing-key case. 6 passed;
tests/test_mental_model_delta.py still 34 passed, 4 skipped.
…mory is about

Tags describe which compartment a memory lives in; entities describe what it
is about. On a bank whose tag vocabulary is a handful of broad topics, a tag
scope cannot isolate a subject: measured on a production bank (2026-08-08),
the best tag scope for one subject reached 25% precision against a 15.5%
base rate, while the entity association reached 94.2% precision / 86.2%
recall on the same corpus.

TagGroupEntityLeaf ({entities: [names], match: any|all}) joins the existing
recursive grammar, so it rides every surface tag_groups already reach with
one SQL-builder change: mental-model refresh scope (trigger.tag_groups),
the staleness gate (any_memory_updated_since), the fast-path window read,
and the semantic/BM25/link-expansion retrieval arms. An entity-scoped
mental model therefore refreshes exactly when facts about its entities
arrive, and its refresh reads exactly those facts.

Mechanics:
- Names match case-insensitively against canonical entity names -- the
  registry's own (bank_id, LOWER(canonical_name)) normalisation.
- Association is inheritance-aware: a memory matches directly via
  unit_entities OR through any of its source_memory_ids. Observations carry
  no direct postings by design (their association is transitive through
  sources, mirroring _entity_rows_for_units_sql), so this is what makes
  entity scope work on the one fact layer mental models read.
- Correlation references the outer memory_units row by fully-qualified
  name; every builder call site that filters memory_units uses an
  unaliased FROM.
- Surfaces without entity postings (the mental-models search, the
  directives listing) strip entity leaves via strip_entity_leaves -- the
  permissive reading; their tag constraints still apply.
- Entity leaves may not appear under 'not', enforced at the API edge on
  RecallRequest, ReflectRequest and MentalModelTrigger
  (validate_entity_leaf_placement): the permissive fallbacks would invert
  into exclude-everything under negation.
- The Python-side post-filter evaluates entity annotations when a result
  carries them and passes permissively when it does not.

Cross-bank leakage is structurally impossible (links are per-unit; a
same-named entity in another bank has a different id) and is covered by a
test.

tests/test_tag_group_entity_leaf.py: 22 tests over five layers -- schema
(incl. the not-placement bar), emitted SQL and param accounting, strip
semantics, the Python matcher, and the staleness gate against the real
database including the source-inherited observation case. Regression:
mental-model delta 34p/4s, trigger tag-group and refresh-scope suites 10p,
consolidation 105p baseline unchanged.
Resolves the prompts.py conflict in STRUCTURED_DELTA_SYSTEM_PROMPT: upstream added
the 'table' block type to the enumeration while this branch added the 0-based
block-index instruction. Both are kept -- they are orthogonal, and 'table' is a real
block type in the merged tree (structured_doc.py defines Literal["table"] and the
block-shape list documents it).
…se check

Upstream's i18n:check (find-untranslated.ts) scans JSXText nodes and human-facing
attributes for untranslated prose. The two diagnostics lines rendered the raw enum
NAMES as bare JSX text, so after merging main they were reported as suspicious
strings and build-control-plane failed.

The names stay raw and untranslated on purpose -- they are the tokens the dry-run
payload, the stored trace and the docs all use -- so they move into {"..."}
expression containers, which the checker does not scan, leaving the separator as a
bare · entity that its own rule already exempts. Rendering is unchanged.
TagGroupEntityLeaf.match serialised as title: "Match", the same title
TagGroupLeaf's DIFFERENT match enum already uses (any/all/any_strict/all_strict/
exact). progenitor/typify keys inline enums BY TITLE, so the second definition
failed to conform and Rust client generation died with TypeError(InvalidValue) at
hindsight-clients/rust/build.rs:199, failing verify-generated-files.

The collision only appears in a REGENERATED spec -- the committed openapi.json on
this branch predates the entity-leaf commit -- which is why CI saw it while a local
build of the committed file passed. Controlled against origin/main's spec: the
pre-existing 'Tags Match' and 'Type' title collisions are present upstream too and
are therefore not the cause.
…ents

verify-generated-files regenerates every generated artifact and fails if the
result differs from what is committed. After the EntityMatch title fix the job
stopped panicking in generation and started failing on that diff instead, so the
artifacts needed regenerating from the merged source rather than carrying the
textually auto-merged versions.

Ran CI's four steps: uv run generate-openapi, generate-bank-template-schema.sh,
generate-clients.sh (rust/python/typescript/go), generate-docs-skill.sh.

The regenerated spec confirms the fix: the duplicate 'Match' enum title is gone
and TagGroupEntityLeaf now publishes 'EntityMatch', which is why the Rust client
generates instead of dying with TypeError(InvalidValue).
verify-generated-files also runs ./scripts/hooks/lint.sh. Formatting only.
JoshFunnell added a commit to JoshFunnell/hindsight that referenced this pull request Aug 11, 2026
…r-retention outcome

This branch adds the refresh_failed_identifier_retention refresh outcome and a
config key, so its generated artifacts are a function of THIS branch's source and
cannot be inherited from the stacked-under PR: merging vectorize-io#3290 forward carries the
code but not the derived files.

Regenerated with CI's own steps (uv run generate-openapi,
generate-bank-template-schema.sh, generate-clients.sh, generate-docs-skill.sh).
The seven changed files match the set verify-generated-files reported, and the new
outcome now appears in the TypeScript and Go clients as well as the docs-skill
spec.

@nicoloboschi nicoloboschi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you using delta mode? that should fix it already

@JoshFunnell

Copy link
Copy Markdown
Contributor Author

Yes. Delta mode was on for every model across the entire baseline, and the ledger shows it running on essentially every refresh.

Configuration: the pre-trial trigger snapshot has trigger.mode: "delta" on all 33 models in the baseline window.

Ledger rows: taking the last 6 hours of the baseline era (2026-08-08 03:02Z to 09:06Z), the window where every llm_requests row survived the retention default:

scope calls input tokens
reflect 38 1,753,314
reflect_tool_call 138 3,499,106
mental_model_delta_ops 39 387,555

39 delta-ops calls against 38 reflect traces, so delta fired on every refresh. Over the full baseline era it applied 372 operations across 210 completed refreshes. Delta mode was working.

It cannot fix the cost, because it sits downstream of it. In _execute_mental_model_refresh, use_delta does two things: it sets created_after on reflect_kwargs, narrowing retrieval to memories newer than the last refresh, and after the loop returns it adds one more LLM call to convert the result into edit operations. The loop itself is unconditional:

reflect_result = await self.reflect_async(**reflect_kwargs)   # runs in both modes
...
if use_delta:
    user_prompt = build_structured_delta_prompt(
        current_document_json=current_doc.model_dump_json(),
        candidate_markdown=reflect_result.text,   # delta consumes the loop's output
        ...
    )

candidate_markdown=reflect_result.text is the constraint. The delta step's input is the agentic loop's output, so delta runs after the loop and never in place of it. In the table above that is 176 loop calls carrying 5.25M input tokens (93.1%) against 39 delta calls carrying 388k (6.9%). Delta mode adds a call, and the loop calls it would need to remove are upstream of it.

The window narrowing was already active as well. created_after was set on every one of those refreshes and the median refresh still read 102,086 input tokens, because the cost is the loop's append-only context assembly across roughly 4.6 calls rather than the size of the window it retrieves. 48.1% of baseline refreshes ran that loop and applied zero edits.

That is what the fast path targets, and it reuses the delta machinery rather than replacing it. Tier 1 makes the same build_structured_delta_prompt call with candidate_markdown="", since the synthesis call is precisely the one being skipped, so the delta prompt already in the tree answers the refresh from the window directly instead of from a loop-produced candidate. Same bank, same models, same workload: median input per refresh 102,086 to 12,600, calls per trace 4.39 to 1.23, with the loop retained as the escape hatch for the 4% of refreshes the fast path declines.

The PR body should have stated this outright. It assumes delta mode in the tier lattice but never says the baseline was already 100% delta. I will add the baseline configuration explicitly under Problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants