portfolio-optimiser/docs/2026-09-02-read-bundle-kontekstkostnad.md
Kjell Tore Guttormsen b799cc527f feat(explore): read_bundle costs O(documents), never O(bytes of the base)
[skip-docs]

S2c / MAJOR-3, order 20260902T151931Z-250257273. The measurement landed first
in ce7f687; this commit is the one seam it authorised, plus the after-table.

read_bundle returned okf.bundle_context -- the WHOLE navigated base. Because
the exploration's participants share one conversation history, that single
function_result rode in FIVE later prompts at full price without anyone asking
for it again: 54-59 percent of every prompt-token in a CLI --explore run.

It now returns the catalogue form one rung down the ladder -- one entry per
concept document (name, type, title, chars) -- with read_file as the next rung.
Tunnel base: 12 595 -> 259 o200k tokens, exploration prompt-tokens -91 percent.

The listing is built from Bundle.context_files and never from files: that is
the property which drops the type: verdict layer at every level, and a listing
built from files would route prior verdicts in front of the navigator around
the gated ExpeL fold while every cost arm stayed green.

A premise was felled before anything was built on it: the tunnel base's root
index body is 4 763 chars alone, nearly the whole ceiling, for a field the
catalogue already excerpts and read_file still returns whole. So read_bundle
carries the listing and not the index.

The tool description and the navigator's instruction both claimed "read its
navigated context" and were updated in the same move -- a description that lies
about the body IS the model's instruction. Two pre-existing asserts would have
gone vacuously true against a list and were strengthened rather than left.

Ceiling lives in the test, not in explore.py. Deviation stated there and in the
docs: it bounds CHARACTERS, not tokens, because tiktoken is not a project
dependency and a gate that skips when an optional package is missing is a gate
that can be silently absent; the conversion was measured (2.89 chars/token) and
the order's own token criterion verified once by the instrument.

Load-bearing measured: seven mutations, all red against the WHOLE suite; green
control 1195 passed / 5 skipped (from 1189/5, strict superset); golden
demo-transcript.stdout byte-unchanged; and the debate's three bundle_context
copies are byte-identical before and after, which proves run.py and the
nav-goldens were not touched rather than asserting it.
2026-09-03 00:36:08 +02:00

10 KiB
Raw Blame History

S2c / MAJOR-3 — read_bundles kontekstkostnad: målingen

Ordre: 20260902T151931Z-250257273-from-.claude. Bindende ordretekst: ordreutkast M3 i docs/2026-09-02-misjonsreview-v2.md § 7. Regelen ordren gir: MÅL FØRST, så ÉN søm. Denne fila er tallene med nevner — skrevet og committet FØR noe ble endret, og utvidet med ETTER-tabellen i sømmens egen commit.

0. Instrumentet, og hvorfor det er troverdig

Efemert, i /tmp/claude-s2c/ — utenfor repoet, som forrige gang (misjonsreview-v2 § 8): et måleinstrument i tests/ ville vært en beslutning om å beholde det, og ordren ba om en måling, ikke en ny produksjonsflate. Tokens telles med tiktokens o200k_base, kjørt med uv run --with tiktoken (tiktoken er fortsatt IKKE en prosjekt-avhengighet).

Kjent-positiv kontroll FØR noe ble målt (Verifiseringsloven ansikt 4 — en spørring skal bevises å KUNNE finne): instrumentet ble kjørt mot de tre eksempelbundlene og reproduserte commons' egne publiserte fasittall for okf.bundle_context eksakt — 3 861 / 10 406 / 12 595. Et instrument som ikke treffer en kjent fasit måler seg selv.

Prompten måles som tekst + function_call + function_result, ALDRI .text alene. Den skriptede klientens egen sink samler kun .text, og en prompt hvis hele nyttelast er et verktøyresultat måles da til noen få tegn. Den fella er dokumentert i misjonsreview-v2 vedlegg A og er unngått her ved en egen wrapper på ScriptedChatClient._inner_get_response.

Kjøringen er ekte, ikke rekonstruert: hver rad er én run.main([... --explore ...]) gjennom CLI-døra, med navigatøren skriptet som list_bundles → read_bundle → tekst (trinn-manuset fra økt 67, MAJOR-1 b). At navigatøren faktisk ÅPNET basen er lest av kjøringens eget artefakt ({run_id}-exploration.jsons tool_calls), ikke antatt.

Ærlighetsgrense, uttalt: kopi-telleren er en 160-tegns skive fra MIDTEN av read_bundles nyttelast (samme sonde som misjonsreview-v2 § 4). Skiva finnes i dag i BEGGE de to strengene kjøringen sender — utforskningens function_result OG debattens okf.bundle_context — fordi de er den samme strengen. Kolonnene under skiller dem derfor på ROLLE, som er det eneste som skiller dem: manager/navigator/hypothesiser er utforskningen (sømmen denne ordren rører), proposer/checker er debatten (som ordren forbyr å røre).

1. FØR — hva read_bundle koster, og hvor mange prompts det rir med i

Per base, én CLI-kjøring med --explore:

Base read_bundle-nyttelast Prompts totalt Prompt-tokens totalt Utforsknings-kopier Tokens i dem Debatt-kopier Tokens i dem
bygg-energi-mikro 12 005 tegn / 3 861 tok 15 35 773 5 22 013 3 11 738
veglys-fv-soer 32 201 tegn / 10 406 tok 19 88 881 5 54 623 3 31 376
tunnel-hauglia 39 583 tegn / 12 595 tok 19 106 519 5 65 698 3 37 943

De fem utforsknings-kopiene, navngitt (tunnel-tall): navigatørens egen prompt etter kallet (12 770) · managerens tre progress-ledger-/final-prompts (13 527 / 13 549 / 13 075) · hypotesiserens ene tur (12 777). Ett read_bundle-kall gjør hele basen til et function_result, og det resultatet rir med i hver senere prompt i samme samtale — deltakerne deler samtalehistorikk, så ingen av dem betaler for å be om det.

Selve nyttelasten, som andel av alt: 5 kopier × nyttelast = 19 305 / 52 030 / 62 975 tokens, altså 54 % / 59 % / 59 % av ALLE prompt-tokens i kjøringen. Debattens tre kopier (11 738 / 31 376 / 37 943 = 33 % / 35 % / 36 %) kommer fra run.pys okf.bundle_context og er en EGEN beslutning (PM) — de er med i tabellen som nevner og som ETTER-kontroll, ikke som mål.

Nevneren for «hvor mange prompts»: 15 / 19 / 19 prompts i alt, hvorav 8 bærer konteksten. Tallet 5 gjelder DETTE manuset (én navigatør-tur, én hypotesiser-tur, tre manager-prompts). misjonsreview-v2 § 4 målte 7× på bibliotek-armen med et annet manus (to hypotesiser-turer, fire navigatør-prompts); begge er sanne om sitt eget manus, og ingen av dem er en påstand om hva en LEVENDE manager gjør. Multiplikatoren er en egenskap ved samtalehistorikken, ikke ved manuset: den er antall prompts etter kallet, og den er ≥ 1 uansett.

2. Hva basene faktisk inneholder — premisset for formen

Base Filer navigert Konseptfiler type: verdict Rot-index.md (body)
bygg-energi-mikro 6 4 1 1 884 tegn
veglys-fv-soer 7 5 1 3 646 tegn
tunnel-hauglia 7 5 1 4 763 tegn

Ett premiss felt før noe ble bygget på det: «basens indeks er selve navigasjonsprosaen, så den hører hjemme i read_bundle». Tunnelbasens rot-indeks er alene 4 763 tegn ≈ 1 400 o200k-tokens — altså nesten hele ordrens tak på 1 500 for HELE kallet. Å legge den inn ville brukt opp budsjettet på et felt katalogen alt gir et bundet utdrag av (_CATALOGUE_EXCERPT_CHARS), og som fortsatt er nøyaktig ett read_file(id, "index.md") unna. read_bundle bærer derfor konseptlista og ikke indeksbodyen.

3. Hva prefiks-caching ville kjøpt — notert, IKKE bygget

Ordren ber om notatet, ikke mekanismen. Regnet av prompt-prefiksene i misjonsreview-v2 § 4 (ikke av en fakturering — leverandørens faktiske cache-treff er UMÅLT): ≈ 60 % av debattens og ≈ 40 % av utforskningens prompt-tokens er prefiks-cachebart som koden står i dag, uten én kodeendring, fordi prefiksene allerede er byte-stabile. Caching virker på KOPIENE; et kontekstbudsjett virker på GRUNNTALLET de multipliserer. De to utelukker ikke hverandre, og denne ordren gjør bare det siste.

4. Verifiseringslogg (FØR-halvdelen)

# Påstand Kommando → resultat
1 Instrumentet treffer en kjent fasit uv run --with tiktoken python /tmp/claude-s2c/measure.py <repo> → 3 861 / 10 406 / 12 595 = commons' publiserte tall
2 Kjøringen ÅPNET basen (ikke bare listet den) measure-exploration.jsons tool_callslist_bundles, read_bundle(<base>)
3 Prompt-størrelse inkluderer verktøyresultatet wrapper på _inner_get_response serialiserer contents (function_call/function_result), ikke .text
4 Nevner 15 / 19 / 19 prompts per kjøring; 8 bærer konteksten i alle tre
5 Indeksbodyen alene sprenger nesten hele taket len(navigate_bundle(tunnel).index_summary) → 4 763 tegn ≈ 1 400 tok

5. ETTER — samme instrument, samme kommandoer, samme baser

read_bundle returnerer nå katalogformen: én oppføring per konseptdokument (name, type, title, chars), med read_file som neste trinn.

Base Nyttelast før → etter Kopier i utforsknings-prompts Nyttelast × kopier Utforsknings-prompts totalt Hele kjøringen
bygg-energi-mikro 3 861 → 163 tok 5 → 5 19 305 → 815 23 726 → 5 434 (77 %) 35 773 → 17 481 (51 %)
veglys-fv-soer 10 406 → 237 tok 5 → 5 52 030 → 1 185 56 314 → 5 742 (90 %) 88 881 → 38 309 (57 %)
tunnel-hauglia 12 595 → 259 tok 5 → 5 62 975 → 1 295 67 415 → 5 973 (91 %) 106 519 → 45 077 (58 %)

Ordrens eget kriterium, verifisert direkte: read_bundle over tunnelbasen er 748 tegn / 259 o200k-tokens — under taket på 1 500. Målt forhold 2,89 tegn/token for denne norske markdownen; det er dét som lar gaten bounde TEGN uten å gjette (se testens docstring, som uttaler avviket).

«Ikke utløs»-beviset er en MÅLING, ikke en forsikring. Ordren forbyr å røre okf.bundle_context, nav-goldenene og run.py. Debattens tre kontekstkopier kommer fra nettopp den sømmen, og de er byte-identiske før og etter i alle tre baser — 12 047 / 32 567 / 39 104 prompt-tokens, hver enkelt prompt uendret. Det er et sterkere utsagn enn «jeg rørte den ikke». I tillegg: hele suiten grønn (1 195 passed / 5 skipped, mot 1 189/5 før — supersett, 0 fjernet), tre byte-fasiter OK, og golden demo-transcript.stdout UENDRET (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Et fravær som var et instrumentfeil, ikke et faktum (Verifiseringsloven ansikt 4). Den første ETTER-kjøringen rapporterte 0 kopier på veglys og tunnel. Det var ikke sant: sonden var en 160-tegns skive fra MIDTEN av nyttelasten, og den nye nyttelasten er kort nok til at midten treffer norske tegn, som prompten serialiserer escaped (å) mens sonden holdt dem rå. Sonden ble byttet til et konseptfilnavn (ASCII, ordrett i begge), og svaret ble 5 — samme tall som før endringen. Et negativt resultat fra en ødelagt spørring er ikke en måling.

6. Ærlighetsgrenser, uttalt

  1. Dette er ikke «59 % kostnad». Det målte utsagnet er at read_bundles EGET bidrag faller fra 5 × 12 595 til 5 × 259 tokens på tunnelbasen. En navigatør som deretter åpner k dokumenter betaler k read_file-resultater, og en som åpner ALT betaler omtrent de samme bytene — bare per kall. Gevinsten er at den betaler for det den valgte, og at hvert resultat rir fra SITT eget kall og framover i stedet for at alt rir fra det første.
  2. At en LEVENDE modell velger BEDRE med en liste enn med hele konteksten er IKKE bevist. Samme klasse som structured-output-grensen: det som er bevist er formen og prisen, ikke dømmekraften.
  3. Multiplikatoren 5 gjelder dette manuset. Se § 1.
  4. Debattens 3× står urørt og er PM-ens beslutning, ikke denne ordrens.
  5. Prefiks-caching er notert (§ 3), ikke bygget — ordren ba om notatet.

7. Verifiseringslogg (ETTER-halvdelen)

# Påstand Kommando → resultat
6 Gaten er rød uten sømmen 7 mutasjoner mot HELE suiten, alle røde: se CLAUDE.md-raden
7 Grønn kontroll uv run pytest -q → 1 195 passed / 5 skipped (før: 1 189/5)
8 Golden uendret PYTHONIOENCODING=utf-8 uv run python -m portfolio_optimiser.simulation | shasumea8c5347…
9 Debatten urørt ETTER-kjøringens proposer/checker-prompts identiske med FØR-kjøringens, per prompt
10 Lint + typer uv run ruff check . → All checks passed; uv run mypy src → no issues, 35 files