feat: per-model context budgets — registry, provider truth, activation, recovery, observer, WebUI (campaign integration) - #277
Conversation
…ling migration Campaign phase 1 (contracts & configuration kernel; no runtime activation): - CODEX_MODEL_INPUT_BUDGETS: known-safe usable input floors per canonical model (each floor = that model's own probe-accepted observation; unknown slugs keep the 272K legacy math), plus canonical_codex_model() — the one canonicalizer (trim, alias codex-auto-review→gpt-5.6-luna, spelling preserved) behind every lookup. - openai_codex.context_budget_overrides (canonical keys, dup-alias rejection, 50_192–2_000_000 token bounds) and context_utilization (30–100, default 60). Both classified dormant in the apply registry until phase 3 wires the resolver. - src/llm/context_budget.py: pure total resolver — override ?? floor ?? unknown default, exact-evidence clamp, 60% utilization with the 272K legacy floor, 42K envelope reserve, exact integer arithmetic, positive monotonic deduped rescue ladder (0.7×primary, min 400K ceiling), frozen per-generation snapshot. Nothing consumes it at runtime yet. - context_compression.max_context_chars becomes int|None (null = auto); consumers read resolved_max_context_chars, which maps auto to the legacy 750_000 until phase 3 — behavior byte-identical today. One-time provenance-gated migration reinterprets a persisted legacy-default 750_000 as auto without rewriting config.yml: a data/-side marker is the provenance ledger, and a deliberate save of the compression section stamps operator provenance so any post-save value — including 750_000 — loads verbatim forever. Tests: resolver characterization at the plan-of-record numbers, totality boundary pins (0/42_000/50_192 clamps), ladder invariants, migration polarity (the save-then-reload round-trip), config validation; census pin updated 262→264; fakes upgraded to the real compression-config contract. Gates: suite 9,309 passed / 5 skipped; ruff clean; type-gate new=0; apply-registry findings=0; coverage-gate findings=0 (new files baselined).
…branch
Review round 2 (all four blockers):
1/2. The no-rewrite marker design left an ambiguous 750000 standing on
disk; every blocker traced to it. Now the gate performs the R2 primary
branch: a genuine one-time 750000→null rewrite of that single leaf through
patch_config_paths (comments, ordering, unrelated ${VAR} placeholders
preserved), so nothing remains for a later save to resurrect. Operator
provenance stamping in the persistence layer is deleted entirely —
saving enabled/keep_recent_iterations can no longer make a stale ceiling
explicit.
- Literality is judged on the UNSUBSTITUTED file text: a ${VAR}
placeholder resolving to 750000 is deliberate configuration and is
never migrated.
- Completion is recorded vacuously on non-legacy installs, closing the
fresh-null hole: a later hand-written 750000 is past the gate and
honored verbatim.
3. One truthful outcome: rewrite failure = auto for this boot only, one
warning, NO completion recorded, gate refires next boot; marker-write
failure after a successful rewrite self-heals via the vacuous branch.
4. Packaged-symlink split: the rewrite resolves the symlink and patches
the real target (the atomic replace would otherwise sever the
/opt→/etc link); the marker deliberately does NOT resolve, anchoring
in the durable data dir beside the link.
Tests: all three review reproductions pinned (fresh-null hand-edit,
env-placeholder 750000, unrelated-save resurrection), plus the symlink
case, rewrite-failure retry, marker self-heal, unparseable-text safety,
and the migrate-then-explicit-750000 round-trip. migrations.py at 100%
coverage. Suite 9,311 passed / 5 skipped; ruff clean; type-gate new=0;
coverage-gate findings=0.
Context-budget campaign phase 1: contracts & configuration kernel
Defaults ruling for the context-budget campaign: a fresh install now starts where the reference deployment runs instead of on legacy values. - openai_codex.model: gpt-4o (ancient) → gpt-5.6-sol - openai_codex.reasoning_effort: medium → xhigh - agent_model / agent_reasoning_effort: null (inherit) → "auto" (per-spawn Auto/Dynamic selection out of the box) - auxiliary: disabled/luna → enabled/gpt-5.6-terra (shares the primary OAuth; background jobs ride the mid-tier model) - config.yml template + docs/configuration.md aligned With the phase-1 budget registry this means fresh installs get the full sol-class working set immediately instead of a 272K-class ceiling. Test updates preserve intent: default-value assertions updated; tests ABOUT inherit semantics now declare the axis explicitly (None) instead of relying on the old default; PUT null/""-mean-inherit contract tests untouched. Suite 9,336 passed / 5 skipped; ruff clean; type-gate new=0; apply-registry findings=0; coverage-gate findings=0.
Review blocker on the defaults PR: the setup wizard's build_config() — the one supported first-boot writer — still emitted an explicit openai_codex.model: gpt-5.5, silently overriding the new schema default and pinning fresh installs to a 272K-class budget instead of sol's floor. The scaffold now emits gpt-5.6-sol, and the generated AND parsed codex default tuple (model, effort, both agent axes, auxiliary enabled/model) is pinned together in tests/test_setup_helpers.py so scaffold and schema can never drift apart silently again. Swept the tree for any other writers of the legacy default: none remain.
Campaign phase 2 (pure instrumentation; no behavior change): - LLMResponse.server_input_tokens: parsed STRICTLY from the response.completed usage echo (absent/malformed/bool/negative/non-int ⇒ None). The historical input_tokens client estimate keeps its exact meaning — never relabeled, never substituted. - The structural overflow exception (LLMRequestError, and the LLMError family generally) carries server_input_tokens from authoritative failure-event usage when the server provides one — a rejection without authoritative usage is an occurrence, not a numeric bound — plus the existing model provenance. - src/llm/account_key.py: opaque installation-local account keys — HMAC-SHA256 of the stable non-secret account id, keyed by a random 0600 key file under data/ (atomic first-use creation). Same account + same install ⇒ same key across restarts; no identity or no key material ⇒ None and the attempt is disqualified from account-scoped evidence. Key trouble degrades evidence, never requests. Weak/foreign key material is refused but never overwritten (replacing it would decorrelate all prior observations). - _send_with_retries stamps the account key PER ATTEMPT on both the successful LLMResponse and the overflow exception — the pool may rotate between attempts, and the observer's same-account clamp rule needs the account that served each one. Raw ids appear nowhere. - tests/conftest.py: account-key material isolated to tmp_path for every test (an auth fake returning a real id would otherwise materialize data/account_key.secret in the working tree). Tests: strict-parse matrix, key determinism/stability/0600/degradation/ temp-cleanup, failure-event usage parsing, completed-event stamping, end-to-end retry-engine stamping on success and overflow, no-raw-id pins. account_key.py at 100% coverage and baselined.
Context-budget campaign: out-of-the-box defaults mirror the reference deployment
The resolver becomes real: every logical generation works its EFFECTIVE model's budget instead of one stale constant. - agents/manager.py: the private emergency constants are gone. spawn() accepts budget_snapshot_provider, resolved per iteration (live config and a live model change reach the NEXT generation; retries and rescue rungs reuse the generation's snapshot). Soft compaction targets snapshot.primary_chars; _call_llm_with_recovery takes the snapshot's rescue_ladder; provider absence/failure falls back to the unknown-model snapshot — the exact pre-campaign conservative math. - Latch compaction targets min(learned ceiling, live primary): a silent budget drop is never out-waited by a stale larger latch. - agents_tasks: _make_budget_snapshot_provider resolves the effective agent model exactly like the iteration callback (_agent_llm_policy — override fixed for life, inherit tracks live config; non-codex clients use their own model name into the unknown-model math). Wired at the spawn_agent site and per-task through loop_bridge via budget_snapshot_provider_factory (mirrors iteration_callback_factory, so mixed-model fleets each compact against the right window). - Ceiling truth: the explicit max_context_chars is read from the boot-frozen compression object (restart-bound classification stays honest); overrides/utilization are live reads (live_for_new_work). - tool_loop._maybe_compress: the chat threshold follows the serving model via snapshot_for_codex_config. Fix-in-place: an attribute typo (llm_gateway vs _llm_gateway) would have been swallowed by the non-fatal catch and silently disabled chat compression — the new activation battery pins the fire paths so that class is visible. - apply_registry: context_budget_overrides + context_utilization flip dormant → live_for_new_work; max_context_chars copy now states the model-derived truth. Tests: tests/test_context_budget_activation.py (provider live-tracking / fixed-override / non-codex / frozen-ceiling; chat sol-headroom at 850K stays uncompressed, compresses past 1,277,400; unknown-model 575K; explicit-ceiling; iteration-0 guard) + snapshot_for_codex_config unit pins. v3.74.0 overflow suite updated by intent: the deleted constants' roles are played by the fallback ladder's rungs. Suite 9,349 passed / 5 skipped; ruff clean; type/apply-registry/coverage/lint gates all findings=0.
… contract Review round 2 (all three blockers): 1. First-use creation is now an exclusive-winner protocol: complete material is written and fsynced to a private temp file, published with os.link (atomic, fail-if-exists), and the parent directory fsynced. Exactly one process wins; losers read and use the winner's material, so every process MACs with the one durable secret. A crash can only leave a stray temp file, never a partial key. Pinned with the eight-process barrier test (all returned keys identical, one durable 0600 32-byte file, no temp debris) plus a deterministic single-process loser-branch pin. 2. Identity normalization is inside the failure boundary: an account id that cannot be UTF-8 encoded (unpaired surrogates pass json.loads) disqualifies with a warning instead of raising, and a blanket totality net makes the public function non-raising by construction. Pinned end-to-end on BOTH provider paths: the healthy response and the intended structural overflow each carry account_key=None rather than being replaced by UnicodeEncodeError. 3. Persisted material is accepted only on the exact generated shape: regular file opened O_NOFOLLOW (final-component symlinks refused), owned by this uid, mode exactly 0600, exactly 32 bytes. Anything else fails closed with a specific warning and the questionable material left untouched — replacing it would decorrelate all prior observations. Directory-at-path, 33-byte, 0644, foreign-owner, and read-failure branches all pinned. account_key.py at 100% coverage (baseline updated). Suite 9,377 passed / 5 skipped; ruff clean; type-gate new=0; coverage-gate findings=0.
…vider-truth Context-budget campaign phase 2: provider truth — server usage echoes + opaque account keys
…latch Review round 2 (all four blockers): 1. Frozen generation identity: iteration callbacks capture client, model, effort, and budget snapshot in ONE read on the first attempt, store the plan in a per-generation state channel the manager threads through, and every rescue retry reuses it verbatim — a live reload between attempts can no longer split the budget from the request it governs. The rescue ladder now comes from the OVERFLOWED REQUEST's own snapshot (the plan), with the spawn-time provider demoted to advisory soft targets and the no-plan fallback preserved for legacy/direct callers. Chat gets the model-granularity freeze: one capture per iteration drives both the compaction threshold and a model= pin on the outgoing request (a full client freeze across suspends is the phase-4 durability contract, named in place). Ladder exhaustion now falls through to the existing graceful failure handling instead of re-raising. 2. Collision gate: provider identity gates the budget registry on both paths — a non-Codex client NAMED like a Codex slug (Ollama tagged gpt-5.6-sol) gets conservative unknown-model math, never a Codex floor. 3. Latch deferral: the post-overflow ceiling is held as a pending candidate and published only after the retry actually receives a successful response — a local compressor fit is not provider acceptance. 4. apply_registry: both new fields move to live_read, whose generated copy truthfully matches per-generation rereads (live_for_new_work claimed running work keeps its values — false here). All three review reproductions pinned: rescue-uses-plan-ladder (5.5 plan rescues at 399,001 under a sol advisory), failed-retry-leaves-no-latch, and the frozen-plan reuse under a mid-generation config+client flip (plus fresh-generation pickup). Collision pins on chat and agents. The callback signature change (keyword-only generation_state) updated ~20 test fakes across seven files to the real contract; IterationCallback is Callable[...] since a positional Callable cannot express the keyword-only channel. Suite 9,403 passed / 5 skipped; ruff clean; type-gate new=0; apply-registry findings=0; coverage-gate findings=0.
…ivation Context-budget campaign phase 3: budget-policy activation & agent migration
…agent core Phase-4 unit 1 (contract §6). SurfaceBoundary(request_start, elided_replay) partitions a chat/loop-shaped list: replayed context before the current request envelope elides OLDEST-FIRST in whole messages behind a position-0 count marker regenerated from boundary STATE (recognition is never by text — an impostor message carrying the marker string is ordinary elidable history, pinned); the envelope is protected verbatim (honest failure when it alone exceeds a rung); everything after it rides the existing newest-first emergency core unchanged — the wrapper simply calls it on messages[request_start:], so a first-generation overflow with zero tool iterations recovers by replay elision alone (the round-1 structural gap). Iteration compression is spent before any history is: replay survives whenever the core alone reaches target. None boundary = agent semantics, byte-identical (existing suites untouched). Boundary state rides the report (boundary_request_start / boundary_elided_replay) for the surface to carry forward. 8-test battery.
Phase-4 unit 2 (contract §8). Loops previously had NO soft-compaction path and a structural overflow terminally failed the iteration. - Per-iteration serving-identity capture (the phase-3 chat contract, loop-side): one root read drives soft compaction, preflight, breaker admission (provider+model), and every physical attempt on the CAPTURED client with both codex axes pinned — the gateway bypass itself stays, per the RFC-001 policy asymmetry. - _maybe_compress_loop: the shared soft pass at the serving model's derived target once tool iterations exist, plus invocation-local latch compaction with the loop's surface boundary (prev_context replay elidable, current prompt protected). - _call_loop_llm rescue: structural context_length_exceeded compresses boundary-aware and retries the SAME frozen identity under ONE monotonic generation deadline — the first attempt runs the policy's own budget, rescue rungs pay for time already burned and never mint fresh windows. Ladder from the captured snapshot; authoritative-empty means no rescue. An exhausted ladder or unfit payload falls through to the existing failure path, so trajectory/reflection finalize exactly once. The latch publishes only after the retried request actually succeeds. - Boundary state (SurfaceBoundary) constructed at message assembly (request_start = 2 when prev_context rides, else 0) and carried/updated across passes; recovery evidence accumulates on the turn and rides the shared TrajectoryTurn.context_recoveries (serialized only when non-empty — on-disk schema unchanged for prior records). - LoopPolicy census gains the phase-4 asymmetries (overflow_recovery, durable_recovery_checkpointing chat-only, soft_compaction, latch_scope turn-vs-invocation) on both instances. - _serving_identity_for(): the narrow-test-gateway fallback extracted to ONE helper shared by chat and loop paths.
…uence Phase-4 unit 3 (contracts §7/§9). Chat turns now rescue a structural context overflow in-generation instead of dying with an LLM API error: - Rescue loop inside the existing exception chain: only the structural overflow class enters rescue; every other failure keeps its exact current path (cancel/capacity-suspend/terminal), so finalization stays single-path. Boundary-aware compression (session history elides behind the count marker; the request envelope is untouchable), rungs from the frozen serving identity's ladder, retries under the ONE persisted generation deadline — rescue never mints fresh budget. - The settled durability sequence: after a locally-fitting compression, the recovery record rides the turn trajectory (TrajectoryTurn.context_recoveries — restored with it on resume, so records can never double-append), and on_context_recovery checkpoints the mutated transcript + boundary + rung phase with progressed=False, no generation_seq bump, and the stored recovery_deadline_utc untouched (store semantics: None leaves it). A durability write failure PROPAGATES — the retry never runs ahead of what resume can reconstruct. - Codec v3: five persisted fields (_boundary_request_start/_boundary_ elided_replay/_char_latch/_rescue_passes/_gen_identity) with version-scoped normalization for v1/v2 payloads (request_start=0 = the pre-campaign whole-prefix protection), exact-type validation, and the census updated. A turn resumed MID-RECOVERY reuses its persisted identity FACTS (provider/model/effort/ladder) and continues at the NEXT rung via the persisted rescue phase — never re-arming rung one. - The durable-turn latch publishes only after the retried generation actually succeeds, then the generation's facts and rung phase reset. - _serving_identity_for relocated below the import block (E402).
Phase-4 unit 4. The recovery battery pins both new surfaces' contracts: overflow-only rescue entry (fast-fail classes keep their exact paths), boundary-aware history elision with the envelope intact on the retried wire, identity pins on both attempts, latch only on server acceptance (failed retry ⇒ no latch, facts survive for resume), the durability write-failure BLOCKING the retry (contract §7 step 6), and a resumed generation continuing at the NEXT rung with its persisted identity FACTS pinning the wire. Loop-side: rescue + acceptance latch + protected prompt. Report-truth fix the resume pin caught: the boundary wrapper now reports the RUNG the caller requested as target_chars, not the replay-reduced inner target the core happened to run with.
- SurfaceBoundary imported at module level and _LoopTurn._boundary typed SurfaceBoundary | None (the object annotation broke the compressor's signature at the type gate); redundant local imports deduped. - Non-fatal compaction guards pinned as behavior on both surfaces (an exploding compressor swallows, records nothing, keeps the payload). - Codec v2→v3 normalization pinned: a legacy payload without the five recovery fields validates and restores with pre-campaign semantics. - Stub census completed so snapshot_chat_turn works against the battery fixture. Suite 9,443/5; all gates findings=0.
1. Envelope integrity: the surface declares its envelope length structurally (chat 2, loop 1) via SurfaceBoundary.envelope_len; pinned mode takes the declared envelope verbatim and never runs content heuristics on it. Request text imitating '[Tool result:' or the emergency-summary marker is provably untouchable (immunity pins), while a PRIOR pass's summary stays in territory and is re-opened by later passes instead of ossifying (second-pass reopening pin). 2. Resume identity: _call_llm reconstructs the FULL serving identity from persisted facts BEFORE preflight/breaker selection (explicit per-provider client map); a missing frozen provider ends the generation honestly with zero physical attempts. The loop-head root config is threaded into _call_llm so budget policy and provider policy come from ONE read, and persisted facts now carry the budget snapshot plus per-attempt provenance (account_key, server_input_tokens). 3. Durable evidence: the codec round-trips context_recoveries with the trajectory, and _finish_loop copies loop recoveries onto the SAVED artifact. 4. Latch consumption: chat enforces min(latch, primary) pre-send with a boundary-aware emergency pass (trigger=latch); the loop latch is enforced even when the compressor object is absent. 5. Deadline honesty: both rescue paths pass the exact remaining budget and refuse the retry when it expires during compression or the durability write - generate_with_recovery admits one attempt regardless of budget, so refusal happens before the wire. 6. Entry census: real run() (durable Discord + nondurable web shapes over full ToolLoopDeps construction), run_resumed(), and run_autonomous() demonstrate the rescue machinery end-to-end, plus LoopPolicy four-dimension pins. Every blocker carries a reproduction pin from the review's exact scenario. Suite 9,466 collected; coverage/lint/type/apply-registry gates all clean.
…overy Context-budget campaign phase 4: chat/loop emergency recovery
Odin's normal work is the probe: every emergency rescue already carries the server's own numbers (phase-2 stamping) — the overflow's rejected input size and the compressed retry's accepted usage echo. The observer turns those pairs into per-account, per-model evidence and a temporary DOWNWARD clamp on budget resolution, so a silent serving-window regression stops costing repeated overflow round-trips. No probe traffic, no autonomous upward adjustment (plan of record R2 SS11). - src/llm/window_observer.py: versioned data/context_windows.json evidence store (opaque account keys only), one lock around the whole read-merge-atomic-write transaction, hostile-input-safe reads (O_NONBLOCK/O_NOFOLLOW, fstat shape, size cap), quarantine-never- repair on corrupt material, atomic publication with parent-dir fsync, explicit fd ownership across every failure window. Clamp qualifies only on a same-account, same-model, server-authoritative overflow->acceptance pair; the clamp value IS the acceptance, exact; 24h TTL judged lazily; downward-only merges; active clamp = minimum non-expired across accounts; every public entry point total - evidence-write failure forfeits durability, never the request. - Resolution: snapshot_for_codex_config(observed_clamp=) feeds the phase-1 clamp slot; chat (_maybe_compress + _call_llm ladder), loop (_maybe_compress_loop + _call_loop_llm ladder), and agent (_generation_budget_snapshot) surfaces all pass the active clamp. - Evidence capture at all three rescue-success sites: chat and loop record through the runner's total helper; agents thread an evidence_recorder callable (spawn -> _run_agent -> _call_llm_with_recovery) with a dict-shape adapter beside the callback that produces the shape. - API: GET /api/context/windows (canonical keys, floors, overrides, configured vs effective resolutions, provenance, raw evidence) + POST /api/context/windows/clear (account-scoped manual clear); route parity 188 -> 190. - 41-test battery: hostile store inputs (FIFO/symlink/directory/ oversize/corrupt/off-schema), atomicity under crashed writes, fd discipline under fdopen failure, the full clamp qualification matrix, downward-only + TTL semantics, forfeit invariant, broken- observer guard arms on every surface, resolver integration, all three surface hooks, and the management API.
…erver feat(context): passive window observer + downward-only clamps (phase 5)
feat(webui): add context budget controls
|
Integration review verdict at The phase-local batteries are green, but the complete
Validation evidence at the reviewed head: The green battery does not exercise the provider-switch rescue, split agent snapshot, stop-during-recovery, desired-vs-runtime ceiling, config-alias migration, or normalized-map persistence reproductions above. No files modified, merge performed, deployment made, or pipeline started. |
…ation-fixes fix: close context-budget integration seams
Final integration re-review —
|
…ation-fixes-r2 Fix remaining context-budget integration seams
Final integration re-review —
|
Integration PR for the per-model context-budget campaign (plan of record R2, settled 2026-08-17; gist: fbd9201e48d847d0f504ddff66d7773b). 30 commits, 67 files, six phases — each phase PR jointly reviewed to LGTM on the campaign branch (#270–#276 including two defaults/fix PRs), with role-swapped implementation rounds per Aaron's rules.
What the campaign delivers
CODEX_MODEL_INPUT_BUDGETSregistry (proven per-model usable input floors from the 2026-08-17 probe map), canonical model names, the pure total derivation chain (override→floor→clamp→utilization→envelope reserve→char targets→rescue ladder, exact integer math),max_context_chars→ int|None with a one-time provenance-gated 750K→null migration.server_input_tokens), opaque HMAC account keys, per-attempt stamping on successes and overflow errors.data/context_windows.jsonevidence store, downward-only 24h-TTL clamps qualified on same-account overflow→acceptance pairs, eligible-account scoping via dependency inversion, admin-gated management API.Behavioral invariants held throughout
Gates at
b090b4fSuite 9,539 passed / 5 skipped (grew from 9,257 at campaign start); coverage findings=0 (total 89.8%); lint/type new=0; apply-registry clean; npm run check green with byte-current dist; CI 7/7 on every phase head.
Merge is held for Aaron — as are local deploy and the release pipeline, each separately gated.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SHfEwTsEyuS8RUhwdoW66g