Skip to content

Enforce context-window admission check on report requests (#5157) - #5210

Merged
realmarcin merged 17 commits into
mainfrom
claude/5157-report-admission
Oct 30, 2026
Merged

realmarcin merged 17 commits into
mainfrom
claude/5157-report-admission

Conversation

@realmarcin

Copy link
Copy Markdown
Collaborator

Summary

Implements #5157 (D33) report-family context-window admission check on claude/5157-report-admission (0674193f60ca895c7c2acff5b6bc7b4274be9c94, stacked on claude/dc-a13-receipt-floor-v3 01264370e2ca51312cb534d42a4c7b9f2721b1c6) and resolves all findings from independent review R9 (pr5157_225a3cce4_review_2026-10-26.md, F1–F8):

  • Adds src/data_sheets_schema/report_admission.py enforcing count_tokens + max_tokens <= context_limit prior to provider dispatch across report, report_after_repair, report_regate, and reconcile_full (plus pre-full sizing verification).
  • Persists structured admission check records in usage_ledger.py and provenance.admission_checks.
  • Refuses oversized requests before billing any output tokens.

Verification

  • pytest tests/test_report_admission.py: 43 passed.

realmarcin and others added 17 commits October 25, 2026 12:45
Owner decision D33 (2026-10-25): admission check now. At the 128k report
cap (D28) a 1M window leaves 872k tokens of input. Kids First's measured
regate input was already 880,265 tokens at the 96k cap, and the regate
also carries the report it rejected. The receipt completion and the typed
audit stages count before they send; the report family did not.

Why: a request that cannot fit should be refused before it is sent, with
its numbers, and not discovered as a provider error after the usage call
has begun. On the regate path that error was caught by
`_regenerate_report`, so the run completed with the unregated report and
no recorded reason.

On shared generation 1, `_call_with_usage` now counts the exact request
`_call_with_retry` will send for reconcile_full, report, report_regate and
report_after_repair. It uses the receipt completion's helpers: the
single-attempt client, `messages.count_tokens` and `_context_check`,
against the registration's `audit_limits` window, as the typed stages do.
When count + max_tokens exceeds context_limit_tokens it raises
UsageLedgerError before the usage call begins. Nothing is sent, and there
is no pending call, usage row, abandoned attempt or consumed regate
allowance. `_generate_phase` and `_regenerate_report` both re-raise it, so
MAX_ATTEMPTS is not spent, and a resume recounts the same request and
refuses it again. The batch canary records it as it records the other
admission refusals ("it raised UsageLedgerError"). The dollar readout is
unchanged and prices nothing for a refused call. An admitted count is
pinned as `{project}_{phase}_context.json` in the snapshot index and the
provenance record's intermediates. The receipt completion module gains
`_measure_context`, the count before its verdict, and a `label` on its
messages so the refusal names the phase and its numbers; its own messages
are unchanged. Shared generation 2 already counts every ordinary call and
is untouched.

reconcile_full is included although #5157 names only the report family.
D28 raised it to 128k with the report (`SHARED_PHASE_MAX_TOKENS`), and it
carries the same records, audit findings and schema context, so the same
872k room applies. An over-window reconcile_full already ended the run
with a provider error; the check changes only that it stops before
sending, with the count, at the cost of one unbilled count call.

A refused regate or report refresh after repair now stops the run, where
before the request was sent and any error it drew left the stale report
in place. Re-sizing the AI_READI and KIDS_FIRST inputs, #5157's other
proposed fix, is still open.

No model-facing bytes change. The CHORUS fixture's reconcile_full, report
and regate requests hash the same at the base f8c5ac7 and here, and the
same with the admission bypassed (tests/test_report_admission.py).

Refs #5157, #5112

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview)

Why: review R9 of 225a3cc (verdict "fix first") found four problems.
A refused regate or refresh after repair raised out of the run with
nothing recorded. The regate re-refused on every resume, so the run
could never publish, and the refresh left a resume that died on the
receipt-completion boundary. The CHORUS parity test hashed the author's
checkout path, so it failed in every other checkout and in CI. A single
transient error on the unbilled count stopped a run whose dispatch
would have retried it.

A refused request now raises ContextNotAdmitted, a UsageLedgerError
that carries what the refusal rests on: the phase, the reason, the
count, max_tokens, the window and its basis, and the request hash. The
refused count is pinned as {project}_{phase}_context_refused.json.
reconcile_full and the first report still stop the run before dispatch,
because there is no artifact to keep.

A refused regate is now treated as a regate that failed (#967). The
report as written stays, and report_gate records regenerated false,
regeneration_attempted false (the allowance is unspent), the reason and
a regate_refused block. The run then writes its record before the
post-record checks decide (#1818).

A refused refresh after repair is now latched as a terminal evidence
refusal, report_refresh_not_admitted with its numbers. A resume
re-raises it before any drift logic (#1820). The report as written
describes the records before repair, and the gate's source review
refuses it in any case, which is how the parent ended the same request
when the provider refused it. report_regenerated_after_repair also
states whether the refresh was written.

The count now uses the same transport as the call it admits: the
caller's client, with the transient errors _call_with_retry retries
retried up to MAX_ATTEMPTS, with the same backoff and rate-limit pause.
Both now use one _transient classification. Non-transient errors and
unusable counts still refuse at once.

The parity test now normalises the checkout the schema resolved from as
well as the run root. The constants were re-derived from f8c5ac7 in a
separate tree and match the reviewer's. Typed stage counts now carry
their stage's label, and an over-window count states its numbers. New
tests cover the regate, refresh, count-retry and resume paths.

Not done. Progress is not pinned between repair and refresh: the
refresh trigger and the repair log live in that invocation's memory, so
a pin alone would let a resumed run skip the refresh and drop the log.
A refused regate is not kept in the ledger; a resumed gate recounts the
same request, unbilled, and reaches the same recorded outcome.

Refs #5157, #967

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #5210; review finding M1: retain the owner-first-parent packet and compose the current #5158 head before aligning admission tests with #5212.
PR #5210 / #5157, finding M1: use the same-tree admission-on/off wire test from #5212 head 6041adf. Omit only the D57 monitor opt-in because this packet has no latch-journal fixture; #5212 retains that opt-in when refreshed. The exact-wire canary passes without changing protected production code or prompt pins.
Base automatically changed from claude/dc-a13-receipt-floor-v3 to main October 29, 2026 04:38
realmarcin added a commit that referenced this pull request Oct 29, 2026
PR #5212, finding M1: merge the recomposed #5210 head after its 237-test full green run. Preserve owner-first-parent history and reviewed D55-D57 protections.
@realmarcin
realmarcin marked this pull request as ready for review October 30, 2026 16:40
@realmarcin
realmarcin merged commit fa304cd into main Oct 30, 2026
16 checks passed
@realmarcin
realmarcin deleted the claude/5157-report-admission branch October 30, 2026 16: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