Skip to content

feat: per-model context budgets — registry, provider truth, activation, recovery, observer, WebUI (campaign integration) - #277

Merged
Calmingstorm merged 34 commits into
masterfrom
feat/context-budgets
Aug 19, 2026
Merged

feat: per-model context budgets — registry, provider truth, activation, recovery, observer, WebUI (campaign integration)#277
Calmingstorm merged 34 commits into
masterfrom
feat/context-budgets

Conversation

@Calmingstorm

Copy link
Copy Markdown
Owner

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

  1. Phase 1 — budget kernel (Context-budget campaign phase 1: contracts & configuration kernel #270): CODEX_MODEL_INPUT_BUDGETS registry (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.
  2. Defaults (Context-budget campaign: out-of-the-box defaults mirror the reference deployment #271): fresh installs scaffold and default to the reference deployment (sol/xhigh/auto/auto/aux-terra).
  3. Phase 2 — provider truth (Context-budget campaign phase 2: provider truth — server usage echoes + opaque account keys #272): strict server-usage parsing (server_input_tokens), opaque HMAC account keys, per-attempt stamping on successes and overflow errors.
  4. Phase 3 — activation (Context-budget campaign phase 3: budget-policy activation & agent migration #273): agents resolve per-generation budget snapshots (frozen with the generation identity); emergency constants deleted.
  5. Phase 4 — chat/loop recovery (Context-budget campaign phase 4: chat/loop emergency recovery #274): surface-boundary compression (declared envelopes, replay elision), in-iteration overflow rescue on both surfaces under one monotonic deadline, durable resume of mid-recovery generations (codec v4), latch-on-acceptance, LoopPolicy census.
  6. Phase 5 — passive observer (feat(context): passive window observer + downward-only clamps (phase 5) #275): data/context_windows.json evidence store, downward-only 24h-TTL clamps qualified on same-account overflow→acceptance pairs, eligible-account scoping via dependency inversion, admin-gated management API.
  7. Phase 6 — WebUI (feat(webui): add context budget controls #276): the Context budgets table in Codex Advanced — floors, editable overrides, effective budgets, char targets, provenance with clamp expiry, account-scoped clamp clear; fully server-derived.

Behavioral invariants held throughout

  • Protected material (system prompt, memory injection, session-history budget, request envelopes) untouched — only tool-iteration material compacts, now per-model.
  • No probe traffic; growth discovery stays manual. Clamps are downward-only, TTL-bounded, evidence-not-configuration.
  • Overrides/utilization apply to the NEXT logical generation; every generation freezes one identity + one budget snapshot.

Gates at b090b4f

Suite 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

Calmingstorm and others added 30 commits August 17, 2026 17:23
…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)
@Calmingstorm

Copy link
Copy Markdown
Owner Author

Integration review verdict at b090b4fc60b34ce17be9a22d71dda672830b0f47: not merge-ready.

The phase-local batteries are green, but the complete master...feat/context-budgets diff exposes six cross-phase seams that need closure.

  1. High — agent rescue keeps the frozen Codex client but admits/accounts it through the live provider's breaker.

    _capture_agent_generation_plan() freezes client/model/effort/snapshot but not provider (src/discord/native_tools/agents_tasks.py:217-259). _agent_generate() then calls capacity_breaker_for(resolved_model) without provider= (:697-710), which deliberately recaptures the current provider in src/discord/llm_gateway.py:182-198. If the active provider changes after a Codex overflow, the compressed retry still uses the frozen Codex client but can be blocked by, or poison, Ollama/Kimi's breaker.

    Freeze provider with the generation plan and pass the frozen provider into breaker lookup. Pin a provider switch between overflow and rescue, asserting client, provider breaker, model, and effort all remain the original serving identity.

  2. Medium — agent soft compaction and the request/rescue plan are separate live snapshots.

    The manager asks budget_snapshot_provider() and compacts at that result (src/agents/manager.py:1098-1139), but creates the generation state only at send time (:1198-1207); the callback then independently captures another root config/client/snapshot (src/discord/native_tools/agents_tasks.py:791-807). A live model/provider/config change between those points lets pre-send compaction use one model's target while the physical request and rescue ladder use another. This is the agent-side hole in the campaign's “one snapshot beside one serving identity” contract.

    Build the authoritative generation plan once before soft/latch compaction and thread that exact plan into every physical attempt. Pin an in-place/live identity change between compaction and send.

  3. High — stopping an autonomous loop does not cancel an in-flight recovery; tools may execute after the API reports stopped.

    LoopManager.stop_loop() only sets _cancel_event and status (src/tools/autonomous_loop.py:134-156). The callback remains awaited (:268-281), while loop recovery invokes generate_with_recovery() without that event (src/discord/tool_loop.py:2630-2638) and proceeds to _execute_loop_tools() after a later success (:2349-2355). This became operationally material when phase 4 added long in-iteration recovery.

    Thread the owning loop's cancellation event through run_autonomous() into recovery, and fence tool execution as well. Pin stop during a recovery wait: prompt task settlement, no later LLM attempt, and no tool dispatch.

  4. Medium — the Context budgets table reports desired restart-bound ceiling math as current runtime truth.

    /api/context/windows derives primary_chars from bot.config.openai_codex.context_compression.max_context_chars (src/web/api/llm_admin.py:957-998), while chat/loop requests derive from the boot-frozen compressor (src/discord/tool_loop.py:1019-1028; construction in src/discord/wiring.py:456-466). After saving 750000 -> 500000 without restart, status correctly says restart pending, but the table says “Resulting target 500,000” while the next generation still uses 750,000.

    Serve/render runtime-effective target separately from desired target, or derive the displayed effective resolution from the boot snapshot. Pin a desired/runtime split with context_compression_pending_restart=true.

  5. Medium — the one-time migration record is not bound to config identity.

    ceiling_marker_path() uses only the unresolved path's parent and a fixed filename (src/config/migrations.py:78-80), while the rewrite targets the resolved file (:359-365). Reproduced: migrate one real config through symlink A, explicitly restore 750000, then load the same target through symlink B; B has no marker and erases the intentional value again. Conversely, two config files in one directory share one marker, so one can suppress the other's migration.

    Preserve the packaged unresolved-marker durability requirement, but bind validated provenance to the actual config identity. Pin same-target/different-alias and same-directory/different-config cases.

  6. Medium — generic Config Admin can publish canonicalized overrides in memory while persisting none.

    The new validator canonicalizes map keys (src/config/schema.py:620-643), but submitted_leaves() walks the raw submitted map against the canonical validated map and drops unmatched keys (src/config/persistence.py:136-151). Reproduced with POST/PUT /api/config payload equivalent to context_budget_overrides: {codex-auto-review: 600000}: validation produces {gpt-5.6-luna: 600000}, submitted_leaves() returns [], the route publishes the canonical value and returns success, disk remains {}, and restart loses it.

    Teach leaf derivation how to persist normalized keys inside schema-owned maps, or reject alias keys on that generic path. Pin successful response, runtime value, disk value, and reload value as identical.

Validation evidence at the reviewed head:

Independent full suite: 9,537 passed, 7 skipped
npm run check:         passed
Ruff:                  clean
Lint gate:             0 findings
Type gate:             0 findings
Apply-registry gate:   0 findings
Coverage gate:         0 findings (89.8% report-only total)
git diff --check:      clean
GitHub checks:         7/7 passed
Mergeability:          CLEAN

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
@Calmingstorm

Copy link
Copy Markdown
Owner Author

Final integration re-review — 2167e11

Verdict: not merge-ready. The integration-fix delta closes blockers 1, 2, and 6 as originally reproduced, but blockers 3, 4, and 5 are not closed across their full production surfaces.

Original six-blocker status

  1. Frozen agent breaker — closed for the reported Codex/provider-switch reproduction. The generation plan captures provider/client/model/effort/snapshot once; _agent_generate() admits through capacity_breaker_for(resolved_model, provider=provider), and every rescue reuses that plan.
  2. Single agent budget snapshot — closed. The manager captures one plan before soft/latch compaction and passes its exact snapshot and ladder through the physical request and rescue, including loop-spawned agents.
  3. Loop stop cancellation — still open (high). External stop now cancels and awaits the manager task correctly. But a loop invoking stop_loop through its real native-tool path runs the handler in an asyncio.gather() child task, not info._task. LoopManager.stop_loop() therefore cancels and awaits the parent manager task while that parent is awaiting the child. This creates a cancellation cycle: Python recurses in Task.cancel(), the stop call never returns, and the loop remains running. Relevant path: src/discord/tool_loop.py:2362,2785-2800,2875-2880src/discord/native_tools/agents_tasks.py:649-666src/tools/autonomous_loop.py:171-180. My production-shaped reproduction emitted RecursionError: maximum recursion depth exceeded, timed out, and left the manager task pending.
  4. Runtime/configured ceiling truth — still open (medium). The enabled-at-boot mismatch is fixed, but disabled-at-boot compression is reported falsely. Runtime creates no compressor when boot config has enabled: false (src/discord/wiring.py:456-466), so generation snapshots apply no explicit ceiling (src/discord/tool_loop.py:1019-1027). The API nevertheless takes the boot snapshot's max_context_chars without considering boot-effective enabled (src/web/api/llm_admin.py:968-977,1021-1026). Reproduction with boot enabled=false, max_context_chars=500000: API runtime target 500000; actual generation target 1277400. Also, the pending-restart copy renders an automatic null ceiling as 0 characters via ...max_context_chars || 0 (ui/js/pages/llm-config.js:236-237).
  5. Migration config identity — still open (medium). Canonical-path IDs correctly converge aliases and separate steady-state sibling configs, but adoption of the old directory-wide marker is a check-then-bind race (src/config/migrations.py:447-497). Two sibling configs concurrently loading one v2 not_applicable marker can both adopt it before either publishes an identity marker. I barrier-reproduced both retaining 750000 with distinct v3 identity records and no errors. Only one config may inherit the old ambiguous provenance; the other must perform its own migration. The same arbitration treats any directory entry as proof an identity marker exists (:454-462), so unrelated debris can change legacy-upgrade behavior.
  6. Canonical override persistence — closed. Schema-owned maps persist as one validated normalized leaf; the alias-to-canonical override survives YAML persistence and reload.

Validation at the reviewed head

Head:                   2167e11f11ea16ab7deb073fcd2c4a2736a52fb2
Independent full suite: 9,561 passed, 6 skipped
Focused integration:      385 passed
npm run check:           passed; rebuilt dist byte-clean
Ruff changed files:      clean
Lint gate:               0 findings
Type gate:               0 findings
Apply-registry gate:     0 findings
Coverage gate:           0 findings; 89.9% reported
GitHub checks:           7/7 passed
Mergeability:            CLEAN
Worktree/diff-check:     clean

The green battery does not exercise the self-stop task topology, disabled-at-boot runtime-target truth, or concurrent legacy-marker adoption. No merge, deploy, or release/pipeline action was performed.

…ation-fixes-r2

Fix remaining context-budget integration seams
@Calmingstorm

Copy link
Copy Markdown
Owner Author

Final integration re-review — 78caf756

LGTM. PR #277 is merge-ready for master.

The three blockers reopened at 2167e11 are closed at the whole-campaign integration level, and I found no new merge-blocking cross-phase seam in the refreshed master...feat/context-budgets diff.

Reopened blocker status

  1. Loop self-stop cancellation cycle — closed. Logical loop ownership is carried through the real gathered native-tool child with _current_loop. A self-stop sets the cooperative cancellation event and returns control so the owning loop task can unwind and publish stopped; it never cancels/awaits its parent from below. External stop still cancels and awaits settlement. The real start_loop → run_autonomous → stop_loop characterization path settles with one LLM attempt and no recursive cancellation, timeout, later retry, or later tool effect. Removing logical-owner recognition makes that exact pin time out again.

  2. Disabled-at-boot runtime ceiling truth — closed. /api/context/windows now treats a numeric saved ceiling as configured-only when the authoritative boot snapshot says compression is disabled, reports runtime_max_context_chars: null, and derives the effective target from the model budget actually used by generation. Configured and runtime resolutions remain separate. The UI renders null as automatic (model-derived), never 0 characters. Regressing either backend selection or UI formatting makes its pin fail.

  3. Concurrent legacy-marker adoption — closed. Ambiguous pre-identity provenance is now granted through one fsynced-temp, fail-if-exists hard-link claim bound to the canonical config ID. A sibling loser reads the winner and performs its own lexical migration; aliases of the same canonical config can rendezvous. The two-sibling barrier pin produces exactly one adoption and one migration. Making EEXIST grant both claimants reproduces [750000, 750000] and fails the pin.

The original blockers 1, 2, and 6 remain closed as previously verified. Therefore all six integration blockers are closed.

Validation at the reviewed head

Head:                    78caf756bc264e4e561270328515b43bc905671d
Independent full suite:  9,572 passed, 6 skipped
Focused blockers 3/4/5: 19 passed
npm run check:           passed; 39 templates, 223 bindings, rebuilt dist byte-clean
Ruff changed files:      clean
Lint gate:               0 findings
Type gate:               0 findings
Apply-registry gate:     0 findings
Coverage gate:           0 findings; 89.9% reported
GitHub checks:           7/7 passed
Mergeability:            MERGEABLE
Worktree/diff-check:     clean

Campaign verdict: merge-ready for master. Merge, deployment, and release/pipeline execution remain Aaron-gated and were not performed.

@Calmingstorm
Calmingstorm merged commit 05b3875 into master Aug 19, 2026
7 checks passed
@Calmingstorm
Calmingstorm deleted the feat/context-budgets branch August 19, 2026 00:40
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.

1 participant