fix: compaction must not drop messages carrying loaded-tool state (break 5) — ported to today's head, re-measured - #37
Conversation
…eak 5)
Break 5, homeless across three lanes (v5co wrote and measured it but owned
only provider-openai; cal and webu could not apply it). Ported to today's
head and re-measured here.
Compaction removes input items. A message whose metadata records which tools
the model dynamically loaded (`openai:tool_search_items`) is not decoration:
per the vendored tool-search guide TS:854, dropping it (a) makes every tool
it loaded cease to exist for the model and (b) breaks the prompt cache
forward. Both failures are silent, and OpenAI's cache is grow-only, so
"breaks forward" is a full cold rebuild, not a dip.
The ladder had no concept of an un-droppable item. This adds a named,
provider-neutral protected set keyed on METADATA rather than on a provider
name, and unions it into `protected_indices` in
`_remove_messages_with_protection` -- which also protects it from the
tool-pair cascade, since that consults the same set. Truncation and stubbing
already rebuild with `{**msg, ...}` and so preserve metadata; only removal
had to learn this.
Measured at HEAD dd9b9c3 (v5co measured a2a098b):
FAIL-BEFORE 1 failed, 2 passed -> PASS-AFTER 3 passed
full suite 102 passed, 1 xfailed -> 105 passed, 1 xfailed, 0 regressions
ruff check (uvx ruff@0.14.10, as CI runs it): All checks passed
v5co's patch did NOT apply cleanly at this head (hunk 1, fuzz 2: the
token-meter constants from #32/#34 were inserted between its anchors). It
was ported by hand rather than force-applied; the divergence is documented
in docs/lanes/l4s1-context-simple-break5/DONE-NOTE.md section 3.
Default mode is unchanged: with no such metadata key present the new set is
empty and no branch differs -- all 102 pre-existing tests pass unchanged.
Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… in lane note Generated with Amplifier Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Manager verification — all 5 gates re-run by me in a scratch clone. Merging.Head
Gate 1 — I ran the predicate rather than reading itSo a message only becomes un-droppable when a provider has actually recorded loaded-tool state on it. With Gate 3 — the composition is a string match, so I checked the stringProvider main is Gate 4 — the question worth asking about a young CIThis repo's CI landed in this program ( The deviation the lane disclosed, which is the best thing in this PR
I have personal reason to value that call: two cycles ago I let For the record: this is plumbing, not an endorsement
|
Manager verification — all 5 gates re-run by me in a scratch clone. Merging.Head
How I ran the fail-before, and why it took three attemptsMy first two attempts produced That reproduces the lane's claim ( The change is keyed on metadata, not on a provider name# It is deliberately keyed on METADATA, not on a provider name: a second
# provider adding the same kind of state adds a key here and needs no other
# change. Truncation and stubbing already rebuild messages with {**msg, ...},
# so they preserve metadata and need no change --That is the right shape. The protected set keys on The deviation the lane disclosed, which is the part I most wanted to see
Read this as plumbing, not as an endorsement
A G13-only re-run is now buyable at roughly $19 for n=10/arm on terra (measured $0.9386/launch at validity 1.000) — but nobody should buy it without a reason that survives the DON'T-BUILD. |
What this is
Break 5, landed at last. It has been reported homeless across three lanes:
v5cowrote and measured it but owned onlyprovider-openai, so it shipped as averified patch artifact;
calcould not apply it;webucould not either, and filedit as a goal defect. The cost of leaving it:
weburecorded G13 (compactionsurvival) as UNINFORMATIVE, not PASS, because this patch was unlanded — one
pre-registered gate in a $170 experiment that could not be measured at all.
The problem
Compaction removes input items. A message whose
metadatarecords which tools themodel dynamically loaded (
openai:tool_search_items) is not decoration — per thevendored tool-search guide TS:854:
So dropping it costs twice: (a) every tool it loaded silently ceases to exist for
the model, discovered only by failing to call one; (b) the prompt cache breaks forward
— and OpenAI's cache is grow-only (
00-what-we-know.md§2a: strict truncation of acached prefix returns 0, MISS), so that is a full cold rebuild, not a dip. Both
failures are silent.
The ladder had no concept of an un-droppable, position-pinned item.
The fix (+51 lines, one file)
LOADED_TOOL_STATE_METADATA_KEYS: frozenset[str]— keyed on metadata, not on aprovider name, so a second provider adding the same kind of state adds a key and
needs no other change.
_remove_messages_with_protectionunions every carrying message's index intoprotected_indices. Because the tool-pair cascade consults that same set, oneinsertion also protects it from the cascade.
Deliberately not changed:
_truncate_tool_result/_stub_user_messagealreadyrebuild with
{**msg, ...}and so preservemetadata. Only removal had to learnthis.
Measured at today's head —
dd9b9c3(v5comeasureda2a098b)v5co@a2a098bdd9b9c3uvx ruff@0.14.10 check .→ All checks passedThe fail-before counts and failure message are unchanged; the suite totals differ
only because the suite grew 58 → 102 between the two heads. The delta is the same +3.
Why it was ported.
v5coanchored its module-level block onlogger = logging.getLogger(__name__)followed directly byasync def mount(. Sincea2a098b, the token-meter constants from #32/#34 were inserted between thoseanchors, so
patchmatched only by discarding 2 context lines — a silent placementdecision. Ported by hand instead: the block sits after the token-meter constants and
before
mount. Both patches are committed side by side for audit underdocs/lanes/l4s1-context-simple-break5/evidence/.Default mode is unchanged. With no such metadata key present the new set is empty
and no branch differs — all 102 pre-existing tests pass byte-for-byte unchanged.
ruff format --checkstill reports 5 files, pre-existing on clean main anddocumented in
.github/workflowsitself (CI deliberately runsruff checkonly); thischange does not add to that list.
Does G13 become measurable once this lands?
Yes — no code prerequisite remains. The chain was verified across both repos, not
assumed:
702b361definesMETADATA_TOOL_SEARCH_ITEMS = "openai:tool_search_items"(_constants.py:19) andwrites it in two paths (
__init__.py:4752,_response_handling.py:590).when rebuilding the request (
__init__.py:3975), so the state really does live onmessages in the context manager's list.
constant. That equality was checked; a near-miss would have protected nothing while
looking green.
Two caveats, stated rather than discovered later:
TS:893's positional contract foran
additional_toolsinput item is untouched — this provider never persists that iteminto history, it rebuilds it at the input tail each request. If a future implementation
persists it, break 5 acquires a second half this patch does not cover.
survival needs live traffic with
tool_search.modeon and a forced boundary.webu's UNINFORMATIVE call was correct at the time. Once this merges, the reason itgave no longer holds.
Test
tests/test_loaded_tool_state_protection.py(112 lines), fromv5co, guarded againstbeing vacuous — an earlier version was:
add_messageis a coroutine, and an un-awaitedcall left the manager with zero messages while two of three assertions still "passed". It
now asserts 26 messages after the fixture, asserts compaction actually reduced the list,
and asserts ordinary messages are still removable — so the protection cannot pass by
being a blanket do-not-compact.
Provenance & spend
Patch sourced from
amplifier-module-provider-openaimain,docs/lanes/v5co-tool-search-provider-build/context-simple-break5/. Lanel4s1, itemmodel_performance-l4s1. Spend $0.00 against a $0.00 authority — git,pytest, ruff only; no API calls, no DTU, no containers. Full note:
docs/lanes/l4s1-context-simple-break5/DONE-NOTE.md.Draft on purpose — do not merge. The manager verifies the fail-before and merges.