diff --git a/docs/decisions/0007-context-admission.md b/docs/decisions/0007-context-admission.md new file mode 100644 index 000000000..7ab69544e --- /dev/null +++ b/docs/decisions/0007-context-admission.md @@ -0,0 +1,418 @@ +# ADR-0007: Cumulative Context Admission + +**Status:** Accepted + +**Date:** 2026-07-23 + +**Issue:** [#4333](https://github.com/TalonT-Org/AutoSkillit/issues/4333) + +**Work package:** C1 + +## Context + +AutoSkillit has lossless spill controls and raw per-result byte limits, but those controls do not +answer the cumulative question: whether the final request assembled for a model still fits the +active context window. Local history insertion, handler JSON, terminal billing usage, rollout +files, byte counts, and content digests each observe a different representation or a different +time. None proves both provider acceptance and remaining token capacity at admission time. + +This decision freezes C1's implementation-independent contract before C2-C8 add persistence, +adapters, enforcement, or reuse. Its executable form is +`CONTEXT_ADMISSION_PROTOCOL_VERSION`, the pure `reduce_context_admission` reducer, +`replay_context_admission`, and the immutable `CONTEXT_ADMISSION_COVERAGE` registry. + +## Decision + +AutoSkillit adopts protocol version 1 as the shared accounting contract for every model-visible +producer. A debit is authorized only from an authoritative window snapshot and an exact, +representation-bound admission chain. Every transition consumes the complete immutable prior +state and returns the complete next state, a typed decision, and content-free publication effects +as one atomic unit. + +This ADR defines a contract, not enforcement. When the required authority is absent, the only +valid result is a typed unavailable or upstream-gated decision; local estimates never acquire +authority by being conservative. + +## Admission boundary and authority + +The model-visible admission boundary is the exact incremental active-context growth of the final +canonical provider-request representation after all shaping, envelopes, spill markers, tool +arguments and results, hook feedback, retrieval, instructions, schemas, context injection, and +ordered batching. + +Authority requires all of the following to refer to that same immutable representation and +window epoch: + +1. an authoritative remaining-capacity snapshot; +2. an authoritative count for the proposed final input; +3. a binding between that count, the complete representation manifest, and the dispatched + request; and +4. a durable/queryable provider-acceptance or explicit non-admission witness. + +Local history insertion is not request inclusion. Request inclusion is not provider acceptance. +Terminal usage is post-hoc evidence, not admission authority. Bytes are emergency containment, +not token capacity. A digest is integrity metadata, not acceptance, access authority, or +occurrence identity. + +## Protocol version 1 + +Protocol version 1 is identified by `CONTEXT_ADMISSION_PROTOCOL_VERSION = 1`. The static +`CONTEXT_ADMISSION_COVERAGE` registry has exactly one row for every `ProducerSurface`. +`reduce_context_admission(state, event)` is a pure, exhaustive transition function, and +`replay_context_admission(initial_state, events)` deterministically folds a recorded stream. +Neither function performs I/O, persistence, enforcement, response mutation, or estimation. + +The lifecycle is: + +```text +PROPOSED + -> RESERVED + -> PREPARED + -> HISTORY_STAGED + -> REQUEST_DISPATCHED + -> COMMITTED +``` + +Before acceptance, an explicit non-admission or rollback witness may produce `RELEASED` or +`ROLLED_BACK`. A safe epoch rollover may produce `INVALIDATED` for work that was not dispatched. +An ambiguous crash produces `INDETERMINATE`, while inconsistent stored or provider facts produce +`QUARANTINED`; neither state silently releases capacity. + +An uninitialized state has no spendable capacity. Only an authoritative epoch-open snapshot +creates capacity. Commands carry a protocol version, event ID, idempotency namespace, and expected +aggregate revision. Exact event-ID replay and equivalent idempotent retry are resolved before +stale-revision rejection. + +## State, witnesses, and atomic batches + +One occurrence describes one immutable producer contribution and representation revision. An +`AdmissionBatch` owns a request ID, batch ID, ordered unique members, one reserve class, one +complete `CanonicalRepresentationManifest`, and, for protected work, one authorized pool owner. +The batch is reserved, prepared, staged, dispatched, committed, released, rolled back, invalidated, +or quarantined atomically. A provider's exact whole-batch charge is never invented as independent +per-member charges. + +Witnesses are typed and distinct: + +| Stage | Required evidence | +|---|---| +| Epoch open | authoritative snapshot sequence, model/tokenizer identity, and window ID/number | +| Input measured | exact counted representation and complete canonical representation manifest | +| Prepared | representation-binding witness for the counted and dispatch revisions | +| History staged | local history-staging witness | +| Request dispatched | exact request-inclusion witness and receiver fence | +| Committed | durable/queryable provider-acceptance witness for the request, batch, revision, epoch, and snapshot | +| Released/rolled back | explicit non-admission or rollback witness | +| Reconciled | authoritative reconciliation or exact output-usage witness | +| Epoch replaced | truncation/compaction replacement attestation and an authoritative new snapshot | + +Commit reconciles the reservation and exact authoritative charge in the same capacity +transaction. Timeouts, process exit, missing telemetry, and local rendered equality are not +release witnesses. A deadline may request or escalate reconciliation only. + +The reducer's `next_state`, decision, and effects are an atomic publication unit. C2 may persist +that unit with compare-and-swap, but may not split its semantic result. + +## Accounting and identity invariants + +1. An occurrence ID identifies one immutable descriptor. Reuse with changed lineage, surface, + epoch, representation, or batch membership is a conflict or corruption. +2. Admission occurrence identity, storage identity, integrity identity, and access authority are + separate domains. Canonical span IDs are opaque representation-local identities, not hashes. +3. The final manifest owns every model-visible span exactly once. Overlap or omission is rejected; + spans are provenance and duplicate-insertion controls, not additive token truth. +4. Input-context charge and output-generation allowance are separate domains. Generated-output + usage, including invisible tokens, reconciles its maximum; text later added to history is + measured again in the next final input representation. +5. Numeric values enter as authoritative inputs. There are no runtime numeric defaults and no + byte-to-token conversion. +6. Reservations, committed input, unresolved input, and generation reservations have exactly one + canonical accounting owner. They cannot be reconstructed from effects or logs. +7. Retry and resume are idempotent only for the same descriptor, representation revision, key, and + authoritative epoch. The attempt ID is not part of the reservation key. +8. A same-key retry with changed intent returns `CONFLICT` without corrupting valid state. Explicit + witnessed expiry creates a tombstone; wall-clock age neither expires identity nor releases + capacity. +9. Forked or child work has distinct lineage and epoch identity. It affects the parent only when + the parent-visible child delivery is accepted at the parent's admission boundary. +10. Serializable values contain bounded reason codes, counts, versions, timestamps, and opaque + non-secret IDs only. They never contain captured payloads. + +## Protected reserve and epoch isolation + +For one authoritative snapshot, protocol v1 computes global unallocated capacity from snapshot +remaining minus committed input, outstanding input reservations, unresolved input charge, and +outstanding generation reservations. Each protected pool separately subtracts its own committed, +outstanding, unresolved, and generation charge from its injected capacity. Ordinary availability +is global unallocated minus all unused protected capacity; a protected owner can spend only the +non-negative unused capacity of its own class, bounded by global unallocated. + +Protected `SYNTHESIS` and `FINAL_RESPONSE` pools have explicit capability owners, priority, and +release-witness rules. Version 1 forbids borrowing and mixed-reserve-class batches. Every input and +output reservation for one request has the same owner/class, and total injected protected +capacity cannot exceed authoritative remaining capacity at epoch open. This preserves reserve +without subtracting protected use twice. + +A compaction, model, tokenizer, or window change closes the old epoch. Undispatched reservations +are invalidated and cannot transfer. Dispatched or indeterminate work and its conservative charge +remain in immutable closed-epoch audit state. A new identity is not itself a fence: rollover +requires a receiver-validated epoch fence, proof that all old dispatches resolved, or a new +authoritative snapshot that already deducts every unresolved old-epoch charge. + +## Producer coverage matrix + +The states have these meanings: + +- `VERIFIED`: primary source evidence proves the named local observation/control point, but never + implies a global token watermark. +- `PARTIAL`: a useful observation/control point exists, but the final representation, complete + identity, or capacity authority is missing. +- `UPSTREAM_GATED`: no local boundary can observe or control the required fact; Codex/provider + participation is required. + +Every authority state is `UPSTREAM_GATED` for the tested configuration. The following is the +complete, version-pinned projection of `CONTEXT_ADMISSION_COVERAGE`; each row has a stable +`COV-` claim ID and deterministically degrades on version or configuration mismatch. + +| Producer surface | Control-point owner | Observation | Authority | +|---|---|---|---| +| `NATIVE_SHELL` | `shell_capture_hook` | `VERIFIED` | `UPSTREAM_GATED` | +| `UNIFIED_EXEC_AND_WRITE_STDIN` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `APPLY_PATCH` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `AUTOSKILLIT_MCP` | `track_response_size` | `VERIFIED` | `UPSTREAM_GATED` | +| `EXTERNAL_MCP` | `fastmcp_client` | `PARTIAL` | `UPSTREAM_GATED` | +| `AUTOSKILLIT_LOCAL_FUNCTION` | `local_function_dispatch` | `VERIFIED` | `UPSTREAM_GATED` | +| `OTHER_LOCAL_FUNCTION` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `MCP_RESOURCE` | `fastmcp_client` | `PARTIAL` | `UPSTREAM_GATED` | +| `CLIENT_PROVIDER_RETRIEVAL` | `codex_host` | `UPSTREAM_GATED` | `UPSTREAM_GATED` | +| `CODE_MODE_AGGREGATE` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `HOSTED_SPECIALIZED_TOOL` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `HOOK_FEEDBACK` | `hook_registry` | `VERIFIED` | `UPSTREAM_GATED` | +| `TOOL_ARGUMENT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `TOOL_RESULT_ENVELOPE` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `USER_PROMPT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `ASSISTANT_OUTPUT_HISTORY` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `SKILL_PLUGIN_CONTEXT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `OTHER_CONTEXT_INJECTION` | `final_request_assembler` | `UPSTREAM_GATED` | `UPSTREAM_GATED` | +| `HEADLESS_CHILD_PROMPT` | `headless_prompt_builder` | `VERIFIED` | `UPSTREAM_GATED` | +| `PARENT_VISIBLE_CHILD_DELIVERY` | `child_delivery_receipt` | `VERIFIED` | `UPSTREAM_GATED` | +| `COMPACTION_MODEL_WINDOW_TRANSITION` | `compaction_receiver` | `PARTIAL` | `UPSTREAM_GATED` | + +`VERIFIED` rows use AutoSkillit source at revision +`ac8f653a00d24b6be50ef285958cfb0e1b7a351b`. Codex-backed rows use +codex-cli `0.145.0` at revision `25af12f7e61572b0bc18ddb1008be543b91519b0`. +`CLIENT_PROVIDER_RETRIEVAL` and `OTHER_CONTEXT_INJECTION` are explicitly inference-backed gap +claims, not verified source claims. Static source pins are documentation provenance, not runtime +lineage. + +## Authority unavailable and byte ceilings + +If the watermark, exact final measurement, receiver fence, or provider witness is absent, +`reduce_context_admission` returns a typed `watermark_unavailable` or `upstream_gated` decision. +Uninitialized and authority-unavailable states are non-spendable. Estimates provide **no numeric** +authorization for cumulative admission or enforcement. + +Existing lossless spill controls and raw-byte emergency ceilings remain independent and +unchanged. They continue to bound transport and result risk even when token authority is +unavailable, but cannot authorize a token debit. Likewise, token authority must not weaken any +existing raw per-producer byte ceiling. + +## Upstream authority request + +Codex/provider support must expose all three parts as one authority contract: + +1. An **atomic snapshot/reservation** operation for interceptable input producers, plus a separate + **generated-output maximum** reservation. +2. A **synchronous blocking** operation on the **final ordered batch**, after every + transformation, that performs exact measurement and admission using the provider's input-token + count operation when available. It must bind the result to an immutable + canonical representation manifest and a receiver fence immediately before provider request + assembly. +3. A **durable/queryable journal** that records distinct facts for **history staging**, exact + **request inclusion**, **provider acceptance**, **output-usage reconciliation**, **rollback**, + **truncation/compaction replacement**, and **authoritative reconciliation**. + +The minimum request and event fields are: `request_id`, `batch_id`, **ordered members**, +**reservation IDs**, **thread/turn/agent lineage**, **admission sequence**, **window ID/number**, +**model/tokenizer identity**, **snapshot sequence**, **measurement kind/source**, +**active/hard-limit/remaining/proposed/max-output counts**, **reserve class**, and +**representation revision**. A compaction, model, window, or tokenizer transition additionally +carries old and new identities and before/after authority status. + +The OpenAI input-token endpoint can measure one resolved representation but does not reserve +capacity, attest that later mutable conversation state is identical, fence a receiver, or prove +provider acceptance. Post-hoc usage likewise cannot substitute for this three-part contract. + +## Privacy and observability + +Runtime IDs are random, opaque, non-secret values and are never derived from personal data. +Runtime/audit records and Aggregate telemetry are governed separately by field. Each field below +maps to a concrete protocol dataclass attribute (`protocol_version`, `claim_id`, `reason_code`, +etc.) and is governed by an explicit maximum length/cardinality, retention, access, deletion, and +export rule. + +### Runtime/audit fields (per-record, persisted with the session) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| `protocol_version` | pinned reducer semantics | 1 non-negative integer | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; ADR header is the static anchor | content-free structured records only | +| `aggregate_revision` / `admission_sequence` | monotonic per-session counters | 64-bit non-negative | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `event_id`, `reservation_id`, `witness_id`, `batch_id`, `request_id`, `reservation_key`, `occurrence_id`, `attempt_id`, `delivery_occurrence_id`, `generation_reservation_id` | identity | opaque 96 ASCII chars (`[A-Za-z0-9_.:-]`); no `-` leading/trailing segments | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; tombstone keys preserve identity until retention | content-free structured records only | +| `reason_code` (event/decision) | typed provenance | 64 ASCII chars; kebab-case `^[a-z][a-z0-9-]*$`; no `bearer`, `sha256:`, `blake2:`, `content:` prefix | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `count` / `requested_count` / `available_ordinary_count` / `available_protected_count` / `reserved_count` / `committed_input_count` / `unresolved_input_count` / `retained_unresolved_count` / `maximum_allowance` / `exact_terminal_usage` / `injected_count` / `priority` / `predicted_authoritative_maximum` / `active_count` / `hard_limit` / `remaining_count` / `highest_admitted_dispatch_sequence` | exact accounting counts | 64-bit non-negative integer | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `version` / `representation_revision` / `tested_version` / `tested_revision` / `publication_revision` | binding identity | opaque 96 ASCII chars; tested_revision is a pinned git SHA (40 hex chars) | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; static ADR pins preserve release provenance | content-free structured records only | +| `checked_at` | evidence freshness date | ISO-8601 date (10 ASCII chars: `YYYY-MM-DD`) | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | +| `witness_ids` / `span_owners` / `occurrence_ids` / `input_reservations` / `generation_reservations` / `protected_pools` / `idempotency_records` / `expired_idempotency_tombstones` / `closed_epochs` / `processed_events` / `occurrence_records` / `batch_records` / `reservations` / `terminal_occurrence_records` / `terminal_reservations` / `processed_event_tombstones` | ordered collection of typed records | bounded by one epoch (≤ 1 active + ≤ N closed audits, each audit ≤ 10⁴ occurrence records) | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; closed audits survive until audit retention | content-free structured records only | +| `freshness_policy` | typed degradation policy | 128 ASCII chars; literal enum (`verify_on_version_or_configuration_change`, `verify_on_revision_change`, `infer_only`) | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | +| `verifier`, `configuration_mode`, `backend`, `control_point_owner` | evidence metadata | 64 ASCII chars (verifier, configuration_mode, backend); 96 ASCII chars (control_point_owner); no secrets or paths | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | + +### Lineage and source locator fields (per-record) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| `root_session_id`, `current_session_id`, `root_agent_id`, `current_agent_id`, `parent_agent_id`, `root_thread_id`, `current_thread_id`, `parent_thread_id`, `fork_occurrence_id`, `turn_id`, `producer_surface`, `producer_instance_id`, `tool_call_id`, `model_item_id`, `dispatch_identity`, `delivery_occurrence_id` | correlate a request, turn, agent, source claim, and authority witness | opaque 96 ASCII chars; `producer_surface` is a closed enum; `dispatch_identity` is a single opaque dispatch ID plus derived sentinels (no per-call secrets) | same as the owning session or coverage record | authorized operators and maintainers | delete runtime lineage with its session; retain static provenance only with the released contract | export only to access-controlled audit channels; never to aggregate telemetry | +| `source_locator` (coverage evidence) | static relative source path under `src/` | 256 ASCII chars; forward-slash relative path only (no `/` or `~` prefix, no `\`); no absolute paths, no home-directory paths, no URLs, or secrets | coverage record lifetime (static) | maintainers with release-provenance access | dropped with the coverage record; release pins remain in the ADR | export only to access-controlled audit channels; never to aggregate telemetry | + +### Aggregate telemetry fields (population-level, never per-session) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| `state`, `reason_code` counters (privacy-only aggregates) | measure coverage state, unavailable decisions, and protocol health | fixed enum dimensions; counter buckets ≤ 10⁴ distinct labels per dimension | configured aggregate-metrics retention (≤ 30 days) | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only; no opaque IDs, lineage, source locator, or user-controlled values | +| `version` counters | track pinned protocol version usage | one bucket per released protocol version | configured aggregate-metrics retention (≤ 30 days) | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only | + +### Forbidden content (zero cardinality, rejected at the validator) + +The following are forbidden from every serialized contract value, `repr`, and exception message: + +- model content, payloads, prompts, tool results, tool arguments, retrieval bodies, or + `system_message` text; +- absolute paths (any string starting with `/` or `~`, or containing `\\`); +- bearer tokens, credentials, API keys, session cookies, or any string starting with `bearer`, + `sha256:`, `blake2:`, or `content:`; +- content/artifact hashes (any string matching `sha256:[hex]`, `blake2:[hex]`, `content:[hex]`, + or other digest prefixes); +- newlines or carriage returns (`\n`, `\r`) anywhere in a serialized field. + +Validator behavior: each contract type carries a `__post_init__` check that rejects the +construction outright before reduction. Rejection raises `ContextAdmissionValidationError` with a +bounded reason code; the rejected value never reaches storage, telemetry, or the reducer. Tests +in `tests/core/types/test_context_admission_contract.py` and +`tests/core/test_context_admission_coverage.py` freeze this contract by parameterizing against +canary values for every forbidden category. + +Serialization, `repr`, and exception paths must remain content-free. Contract tests use canaries to +prove that content, absolute paths, bearer tokens, and content/artifact hashes cannot escape. +Static commit pins in coverage evidence document provenance only and are not runtime identities. + +## Capability decision for Codex 0.145.0 + +The tested runtime is **codex-cli 0.145.0**, tag `rust-v0.145.0`, peeled commit +`25af12f7e61572b0bc18ddb1008be543b91519b0`. + +Primary source evidence at that exact commit: + +- [context status and window fields](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/session/context_window.rs) +- [history accounting and estimation](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/context_manager/history.rs) +- [the disabled experimental `token_budget` feature](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/features/src/lib.rs) +- [`get_context_remaining` tool registration](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/tools/spec_plan.rs) +- [`get_context_remaining` polling handler](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/tools/handlers/get_context_remaining.rs) +- [App Server usage fields](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/app-server-protocol/src/protocol/v2/thread.rs) +- [App Server raw-response documentation](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/app-server/README.md) +- [compaction identity](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/protocol/src/protocol.rs) +- [four-byte truncation heuristic](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/utils/string/src/truncate.rs) +- [generated hook schemas](https://github.com/openai/codex/tree/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/hooks/schema/generated) + +Official provider documentation: [input-token-count API +reference](https://platform.openai.com/docs/api-reference/responses/input-tokens), +[token-counting guide](https://platform.openai.com/docs/guides/token-counting), and +[compaction guide](https://platform.openai.com/docs/guides/compaction). + +Current hook coverage is documented separately by the +[App Server hook-discovery and `hooks/list` contract](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#hooks). +That moving documentation describes current client-visible coverage; it is not substituted for +the immutable 0.145.0 source pins above. + +The `token_budget` capability is disabled and experimental. `get_context_remaining` is a +model-callable/polling observation, not an atomic reservation available synchronously to every +producer. Internal `base_window_tokens_remaining`, terminal usage, and rollout evidence are +estimated or post-hoc and do not bind the final request to provider acceptance. App Server usage +may be null and is reconciliation evidence only. The pinned hook schemas expose `PreCompact` and +`PostCompact` notification identities, but no atomic hook-visible admission watermark that fences +all producers and the final representation. + +**Inference (clearly labeled):** taken together, these pinned source facts imply that a plugin +cannot close the count-then-mutate interval or prove that the provider accepted the exact +representation it counted. + +**Verdict:** Codex 0.145.0 cannot supply authoritative admission capacity. Its observations may +support diagnostics and reconciliation, but every cumulative admission authority claim remains +`UPSTREAM_GATED`. + +## Protocol evolution + +Version 1 event semantic fields, state meanings, and witness requirements are frozen. Unknown +protocol versions fail closed; they are never silently coerced to version 1. Replay selects +semantics from each stream's recorded protocol version. + +Any semantic change requires a new protocol version and explicit versioned conformance vectors. +C2 (#4334) owns durable encoding, stored-event migration, and upcasting. Upcasting must preserve +the recorded meaning and audit identity; it cannot manufacture an authority witness or reinterpret +an old unavailable decision as admitted. + +## Downstream dependency graph + +This exact graph identifies contract consumers and owners; it does not authorize their +implementation in C1: + +```text +#4333 C1 -> #4334 C2 +#4333 C1 + #4334 C2 + #4335 C3 -> #4336 C4 +#4333 C1 + #4334 C2 + #4335 C3 -> #4337 C5 +#4333 C1 + #4334 C2 -> #4338 C8 + +#4319/#4320/#4321/#4322/#4325/#4326/#4327 -> #4335 C3 artifact authority +#4334 C2 + #4336 C4 + #4337 C5 + #4324 + #4338 C8 -> #4339 C6 +#4334 C2 + #4335 C3 + #4271 + #4338 C8 -> #4340 C7 +``` + +Thus C2 is #4334, C3 is #4335, C4 is #4336, C5 is #4337, C6 is #4339, C7 is #4340, and C8 is +#4338. C2-C8 must use this shared accounting contract rather than invent per-adapter semantics. + +## Non-goals + +- This decision adds no enforcement and chooses no numeric budget defaults. +- Existing raw per-producer ceilings remain in force and are not replaced or relaxed. +- Bytes are not an exact token proxy and are not converted into admission authority. +- A digest is not an access capability or deduplication identity. +- C1 adds no journal, store, producer adapter, provider integration, response mutation, fleet + schema, deduplication store, artifact reuse, or compaction rehydration. +- This decision does not claim that a local observation is provider acceptance or that post-hoc + billing usage can reserve capacity. + +## Traceability + +| ID | Requirement | Contract or verification target | +|---|---|---| +| INV-1 | model-visible admission boundary | Admission boundary and authority | +| INV-2 | stable identities | Accounting and identity invariants | +| INV-3 | atomic reserve/commit/release protocol | State, witnesses, and atomic batches | +| INV-4 | version-pinned coverage matrix | CONTEXT_ADMISSION_COVERAGE | +| INV-5 | token_budget/get_context_remaining | Capability decision for Codex 0.145.0 | +| INV-6 | upstream Codex contract | Upstream authority request | +| INV-7 | privacy-safe observability | Privacy and observability | +| OUT-1 | versioned admission protocol and state machine | CONTEXT_ADMISSION_PROTOCOL_VERSION | +| OUT-2 | producer/control-point coverage matrix | CONTEXT_ADMISSION_COVERAGE | +| OUT-3 | accounting and identity invariants | Accounting and identity invariants | +| OUT-4 | failure and reconciliation semantics | reduce_context_admission | +| OUT-5 | authoritative token accounting | Admission boundary and authority | +| OUT-6 | upstream Codex request | Upstream authority request | +| OUT-7 | implementation dependency graph | Downstream dependency graph | +| NG-1 | no enforcement or numeric budget defaults | Non-goals | +| NG-2 | retain existing raw per-producer ceilings | Non-goals | +| NG-3 | bytes are not an exact token proxy | Non-goals | +| NG-4 | digest is not an access capability or deduplication identity | Non-goals | +| AC-1 | every model-visible producer | Producer coverage matrix | +| AC-2 | idempotent reservation keys and compaction/window reset rules | Accounting and identity invariants | +| AC-3 | outstanding concurrent calls and protected reserve | Protected reserve and epoch isolation | +| AC-4 | Codex claims cite tested version and primary sources | Capability decision for Codex 0.145.0 | +| AC-5 | C2-C8 use the shared accounting contract | Downstream dependency graph | diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 64fc00742..3894e5996 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -6,3 +6,4 @@ - [0004-recipe-redelivery.md](0004-recipe-redelivery.md) — Sanctioned `load_recipe` channel for recipe knowledge re-delivery after Codex context compaction - [0005-output-budget-protocol.md](0005-output-budget-protocol.md) — Bound per-response model-context output with lossless artifacts, pre-spend guards, and derived transport ceilings - [0006-output-containment.md](0006-output-containment.md) — Retire pre-execution command-shape classification in favor of per-backend output-boundary bounding on measured bytes +- [0007-context-admission.md](0007-context-admission.md) — Freeze the versioned cumulative context-admission boundary, authority contract, producer coverage, and privacy rules diff --git a/src/autoskillit/core/AGENTS.md b/src/autoskillit/core/AGENTS.md index 876911047..dff29aa69 100644 --- a/src/autoskillit/core/AGENTS.md +++ b/src/autoskillit/core/AGENTS.md @@ -32,6 +32,7 @@ Sub-packages: types/ (see types/AGENTS.md) and runtime/ (see runtime/AGENTS.md). | `closure_hashing.py` | Canonical hash utilities for closure-mode verdict verification (stdlib-only, IL-0) | | `path_containment.py` | Path containment guards — symlink/hardlink check, TOCTOU guard (stdlib-only, IL-0) | | `closure_verifier.py` | Independent verifier for closure-mode reports (stdlib-only, IL-0) | +| `context_admission.py` | Pure protocol-v1 cumulative context-admission reducer, replay, and coverage resolution | ## Architecture Notes diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index 7186cc0fc..ada25c4a5 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -48,6 +48,21 @@ from .claude_conventions import validate_add_dir as validate_add_dir from .claude_conventions import validate_worktree_path as validate_worktree_path from .closure_verifier import VerificationResult as VerificationResult from .closure_verifier import verify_closure_report as verify_closure_report +from .context_admission import ( + ContextAdmissionValidationError as ContextAdmissionValidationError, +) +from .context_admission import ( + UnsupportedContextAdmissionProtocolError as UnsupportedContextAdmissionProtocolError, +) +from .context_admission import ( + reduce_context_admission as reduce_context_admission, +) +from .context_admission import ( + replay_context_admission as replay_context_admission, +) +from .context_admission import ( + resolve_context_admission_coverage as resolve_context_admission_coverage, +) from .feature_flags import _collect_disabled_feature_tags as _collect_disabled_feature_tags from .feature_flags import is_feature_enabled as is_feature_enabled from .git_remote import REMOTE_PRECEDENCE as REMOTE_PRECEDENCE @@ -165,6 +180,10 @@ from .types import CODEX_SCHEMA_VERSION as CODEX_SCHEMA_VERSION from .types import CODEX_SESSIONS_SUBDIR as CODEX_SESSIONS_SUBDIR from .types import CODEX_VALID_MODEL_IDS as CODEX_VALID_MODEL_IDS from .types import CONFIG_AUTHORITY_KEYS as CONFIG_AUTHORITY_KEYS +from .types import CONTEXT_ADMISSION_COVERAGE as CONTEXT_ADMISSION_COVERAGE +from .types import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION as CONTEXT_ADMISSION_PROTOCOL_VERSION, +) from .types import CONTEXT_EXHAUSTION_MARKER as CONTEXT_EXHAUSTION_MARKER from .types import CORE_PACKS as CORE_PACKS from .types import DATA_MANIFEST_SOURCE_TYPES as DATA_MANIFEST_SOURCE_TYPES @@ -271,16 +290,47 @@ from .types import UNGATED_TOOLS as UNGATED_TOOLS from .types import VALID_INPUT_SPEC_TYPES as VALID_INPUT_SPEC_TYPES from .types import VARIADIC_CLAUDE_FLAGS as VARIADIC_CLAUDE_FLAGS from .types import WORKTREE_SKILLS as WORKTREE_SKILLS +from .types import AcceptInputEvent as AcceptInputEvent +from .types import ActiveContextAdmissionState as ActiveContextAdmissionState +from .types import AdmissionAttemptId as AdmissionAttemptId +from .types import AdmissionBatch as AdmissionBatch +from .types import AdmissionBatchId as AdmissionBatchId +from .types import AdmissionBatchRecord as AdmissionBatchRecord +from .types import AdmissionDecision as AdmissionDecision +from .types import AdmissionDecisionKind as AdmissionDecisionKind +from .types import AdmissionEffect as AdmissionEffect +from .types import AdmissionEventId as AdmissionEventId +from .types import AdmissionOccurrence as AdmissionOccurrence +from .types import AdmissionOccurrenceId as AdmissionOccurrenceId +from .types import AdmissionOccurrenceRecord as AdmissionOccurrenceRecord +from .types import AdmissionReplay as AdmissionReplay +from .types import AdmissionRequestId as AdmissionRequestId +from .types import AdmissionReservation as AdmissionReservation +from .types import AdmissionReservationId as AdmissionReservationId +from .types import AdmissionReservationKey as AdmissionReservationKey +from .types import AdmissionSequence as AdmissionSequence +from .types import AdmissionState as AdmissionState +from .types import AdmissionTransition as AdmissionTransition +from .types import AdmissionWitness as AdmissionWitness +from .types import AdmissionWitnessId as AdmissionWitnessId +from .types import AgentInstanceId as AgentInstanceId from .types import AgentPackDef as AgentPackDef from .types import AgentSessionResult as AgentSessionResult +from .types import AggregateRevision as AggregateRevision from .types import ApiRetryOutcome as ApiRetryOutcome from .types import AuditLog as AuditLog +from .types import AuthoritySourceId as AuthoritySourceId +from .types import AuthorityUnavailableEffect as AuthorityUnavailableEffect +from .types import AuthorityUnavailableEvent as AuthorityUnavailableEvent from .types import BackendCapabilities as BackendCapabilities from .types import BackendConventions as BackendConventions from .types import BackendEventKind as BackendEventKind from .types import BackgroundSupervisor as BackgroundSupervisor from .types import BareResume as BareResume from .types import CampaignProtector as CampaignProtector +from .types import CanonicalRepresentationManifest as CanonicalRepresentationManifest +from .types import CanonicalSpanId as CanonicalSpanId +from .types import CanonicalSpanOwner as CanonicalSpanOwner from .types import CanonicalTokenUsage as CanonicalTokenUsage from .types import CapabilityNotSupportedError as CapabilityNotSupportedError from .types import CapabilityResolutionDetail as CapabilityResolutionDetail @@ -290,6 +340,8 @@ from .types import CaptureValueType as CaptureValueType from .types import CaptureValueTypeError as CaptureValueTypeError from .types import ChannelBStatus as ChannelBStatus from .types import ChannelConfirmation as ChannelConfirmation +from .types import ChargeCommittedEffect as ChargeCommittedEffect +from .types import ChargeDomain as ChargeDomain from .types import CIRunScope as CIRunScope from .types import CIWatcher as CIWatcher from .types import ClaudeContentBlockType as ClaudeContentBlockType @@ -302,6 +354,7 @@ from .types import CloneGateUnpublished as CloneGateUnpublished from .types import CloneManager as CloneManager from .types import CloneResult as CloneResult from .types import CloneSuccessResult as CloneSuccessResult +from .types import ClosedEpochAudit as ClosedEpochAudit from .types import ClosureAuthoritySpec as ClosureAuthoritySpec from .types import ClosureReport as ClosureReport from .types import ClosureRow as ClosureRow @@ -312,17 +365,33 @@ from .types import CodexEventType as CodexEventType from .types import CodexItemType as CodexItemType from .types import CodingAgentBackend as CodingAgentBackend from .types import CompletionRequiredResolver as CompletionRequiredResolver +from .types import ConflictRejectedEffect as ConflictRejectedEffect from .types import ContaminationOutcome as ContaminationOutcome +from .types import ContextAdmissionEvent as ContextAdmissionEvent +from .types import ContextAdmissionState as ContextAdmissionState +from .types import ContextLineage as ContextLineage +from .types import ContextSessionId as ContextSessionId +from .types import ContextThreadId as ContextThreadId +from .types import ContextWindowSnapshot as ContextWindowSnapshot +from .types import CoverageEvidence as CoverageEvidence +from .types import CoverageEvidenceKind as CoverageEvidenceKind +from .types import CoverageState as CoverageState from .types import CrossDomainAssessment as CrossDomainAssessment from .types import CrossDomainPrescription as CrossDomainPrescription from .types import DatabaseReader as DatabaseReader +from .types import DeliveryOccurrenceId as DeliveryOccurrenceId from .types import DialingConfig as DialingConfig from .types import DirectInstall as DirectInstall from .types import DispatchGateType as DispatchGateType from .types import DispatchIdentity as DispatchIdentity +from .types import DispatchRequestEvent as DispatchRequestEvent from .types import EffectiveSkillCatalogAuthority as EffectiveSkillCatalogAuthority from .types import EffectiveSkillInvocationAuthority as EffectiveSkillInvocationAuthority from .types import EnvPolicy as EnvPolicy +from .types import EpochClosedEffect as EpochClosedEffect +from .types import EpochFenceProof as EpochFenceProof +from .types import ExpiredIdempotencyTombstone as ExpiredIdempotencyTombstone +from .types import ExpireIdempotencyKeyEvent as ExpireIdempotencyKeyEvent from .types import FailureRecord as FailureRecord from .types import FeatureDef as FeatureDef from .types import FeatureLifecycle as FeatureLifecycle @@ -330,12 +399,23 @@ from .types import FigureSpec as FigureSpec from .types import FleetErrorCode as FleetErrorCode from .types import FleetLock as FleetLock from .types import FleetSessionEnv as FleetSessionEnv +from .types import ForkOccurrenceId as ForkOccurrenceId from .types import GateState as GateState +from .types import GenerationReconciledEffect as GenerationReconciledEffect +from .types import GenerationReservationId as GenerationReservationId +from .types import GenerationReservationRecord as GenerationReservationRecord +from .types import ( + GenerationReservationRecordedEffect as GenerationReservationRecordedEffect, +) +from .types import GenerationState as GenerationState from .types import GitHubApiLog as GitHubApiLog from .types import GitHubFetcher as GitHubFetcher from .types import HardCapabilityMismatch as HardCapabilityMismatch from .types import HeadlessExecutor as HeadlessExecutor from .types import HeadlessSkillDispatchContract as HeadlessSkillDispatchContract +from .types import IdempotencyExpiredEffect as IdempotencyExpiredEffect +from .types import IdempotencyNamespace as IdempotencyNamespace +from .types import IdempotencyRecord as IdempotencyRecord from .types import InfraExitCategory as InfraExitCategory from .types import InfraOutcome as InfraOutcome from .types import InputContractResolver as InputContractResolver @@ -352,27 +432,42 @@ from .types import LensEntry as LensEntry from .types import LoadReport as LoadReport from .types import LoadResult as LoadResult from .types import MarketplaceInstall as MarketplaceInstall +from .types import MarkGenerationIndeterminateEvent as MarkGenerationIndeterminateEvent +from .types import MarkIndeterminateEvent as MarkIndeterminateEvent from .types import McpResponseLog as McpResponseLog +from .types import MeasurementKind as MeasurementKind from .types import MergeFailedStep as MergeFailedStep from .types import MergeQueueWatcher as MergeQueueWatcher from .types import MergeState as MergeState from .types import MigrationService as MigrationService from .types import ModelIdentity as ModelIdentity +from .types import ModelItemId as ModelItemId from .types import ModelTotalEntry as ModelTotalEntry from .types import ModelTranslation as ModelTranslation from .types import NamedResume as NamedResume from .types import NdjsonDriftOutcome as NdjsonDriftOutcome from .types import NoResume as NoResume +from .types import OccurrenceStateChangedEffect as OccurrenceStateChangedEffect +from .types import OpenEpochEvent as OpenEpochEvent from .types import OutputFormat as OutputFormat from .types import OutputPatternResolver as OutputPatternResolver from .types import PackDef as PackDef from .types import PhoropterPhaseSkip as PhoropterPhaseSkip from .types import PhoropterPrescription as PhoropterPrescription from .types import PluginSource as PluginSource +from .types import PrepareBatchEvent as PrepareBatchEvent +from .types import ProcessedEventRecord as ProcessedEventRecord from .types import ProcessStaleError as ProcessStaleError +from .types import ProducerCoverageDef as ProducerCoverageDef +from .types import ProducerInstanceId as ProducerInstanceId +from .types import ProducerSurface as ProducerSurface from .types import PromptContractError as PromptContractError +from .types import ProposeOccurrenceEvent as ProposeOccurrenceEvent +from .types import ProtectedPoolOwnerId as ProtectedPoolOwnerId +from .types import ProtectedPoolSpec as ProtectedPoolSpec from .types import ProviderOutcome as ProviderOutcome from .types import PRState as PRState +from .types import QuarantineRecordedEffect as QuarantineRecordedEffect from .types import QuotaPolicy as QuotaPolicy from .types import QuotaRefreshTask as QuotaRefreshTask from .types import ReadingToken as ReadingToken @@ -393,12 +488,40 @@ from .types import RecipeSectionContentFormatDef as RecipeSectionContentFormatDe from .types import RecipeSectionDef as RecipeSectionDef from .types import RecipeSectionValidationFinding as RecipeSectionValidationFinding from .types import RecipeSource as RecipeSource +from .types import ReconcileGenerationEvent as ReconcileGenerationEvent +from .types import ( + ReconciliationEscalationEffect as ReconciliationEscalationEffect, +) +from .types import ( + ReconciliationQueryRequestedEffect as ReconciliationQueryRequestedEffect, +) +from .types import ReleaseNonAdmissionEvent as ReleaseNonAdmissionEvent +from .types import RepresentationBindingId as RepresentationBindingId +from .types import RepresentationBindingWitness as RepresentationBindingWitness +from .types import RepresentationRevision as RepresentationRevision +from .types import RequestReconciliationEvent as RequestReconciliationEvent +from .types import ReservationInvalidatedEffect as ReservationInvalidatedEffect +from .types import ReservationRecordedEffect as ReservationRecordedEffect +from .types import ReservationReleasedEffect as ReservationReleasedEffect +from .types import ReserveClass as ReserveClass +from .types import ReserveRequestEvent as ReserveRequestEvent from .types import ResolvedSkillAuthority as ResolvedSkillAuthority +from .types import ( + ResolveIndeterminateAcceptedEvent as ResolveIndeterminateAcceptedEvent, +) +from .types import ( + ResolveIndeterminateNonAdmissionEvent as ResolveIndeterminateNonAdmissionEvent, +) +from .types import ( + ResolveIndeterminateRollbackEvent as ResolveIndeterminateRollbackEvent, +) from .types import ResponseBackstopExemptionDef as ResponseBackstopExemptionDef from .types import RestartScope as RestartScope from .types import ResultParser as ResultParser from .types import ResumeSpec as ResumeSpec from .types import RetryReason as RetryReason +from .types import RollbackAdmissionEvent as RollbackAdmissionEvent +from .types import RolloverEpochEvent as RolloverEpochEvent from .types import ServeOverridesSnapshot as ServeOverridesSnapshot from .types import SessionCheckpoint as SessionCheckpoint from .types import SessionEvent as SessionEvent @@ -428,6 +551,8 @@ from .types import SkillSourceRef as SkillSourceRef from .types import SkillVisibilitySpec as SkillVisibilitySpec from .types import SpilledOutput as SpilledOutput from .types import SpillSpec as SpillSpec +from .types import StageHistoryEvent as StageHistoryEvent +from .types import StartGenerationEvent as StartGenerationEvent from .types import StoredSkillSessionContract as StoredSkillSessionContract from .types import StreamParser as StreamParser from .types import SubprocessResult as SubprocessResult @@ -441,10 +566,18 @@ from .types import TestResult as TestResult from .types import TestRunner as TestRunner from .types import TimingLog as TimingLog from .types import TokenFactory as TokenFactory +from .types import TokenizerIdentity as TokenizerIdentity from .types import TokenLog as TokenLog +from .types import ToolCallId as ToolCallId from .types import TraditionManifest as TraditionManifest +from .types import TurnId as TurnId +from .types import ( + UninitializedContextAdmissionState as UninitializedContextAdmissionState, +) from .types import ValidatedAddDir as ValidatedAddDir from .types import ValidatedWorktreePath as ValidatedWorktreePath +from .types import WindowEpochId as WindowEpochId +from .types import WitnessKind as WitnessKind from .types import WorkspaceManager as WorkspaceManager from .types import WriteBehaviorSpec as WriteBehaviorSpec from .types import WriteEvidence as WriteEvidence diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py new file mode 100644 index 000000000..fadc91dc3 --- /dev/null +++ b/src/autoskillit/core/context_admission.py @@ -0,0 +1,2950 @@ +"""Pure reducer and coverage resolver for cumulative context admission.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import assert_never + +from .types._type_context_admission import ( + CONTEXT_ADMISSION_COVERAGE, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionBatch, + AdmissionBatchId, + AdmissionBatchRecord, + AdmissionDecision, + AdmissionEffect, + AdmissionOccurrenceId, + AdmissionOccurrenceRecord, + AdmissionReplay, + AdmissionReservation, + AdmissionReservationId, + AdmissionSequence, + AdmissionTransition, + AdmissionWitness, + AdmissionWitnessId, + AggregateRevision, + AuthorityUnavailableEffect, + AuthorityUnavailableEvent, + CanonicalSpanId, + ChargeCommittedEffect, + ClosedEpochAudit, + ContextAdmissionEvent, + ContextAdmissionState, + ContextAdmissionValidationError, + ContextWindowSnapshot, + DispatchRequestEvent, + EpochClosedEffect, + ExpiredIdempotencyTombstone, + ExpireIdempotencyKeyEvent, + GenerationReconciledEffect, + GenerationReservationId, + GenerationReservationRecord, + GenerationReservationRecordedEffect, + IdempotencyExpiredEffect, + IdempotencyRecord, + MarkGenerationIndeterminateEvent, + MarkIndeterminateEvent, + OccurrenceStateChangedEffect, + OpenEpochEvent, + PrepareBatchEvent, + ProcessedEventRecord, + ProducerCoverageDef, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + QuarantineRecordedEffect, + ReconcileGenerationEvent, + ReconciliationEscalationEffect, + ReconciliationQueryRequestedEffect, + ReleaseNonAdmissionEvent, + RequestReconciliationEvent, + ReservationInvalidatedEffect, + ReservationRecordedEffect, + ReservationReleasedEffect, + ReserveRequestEvent, + ResolveIndeterminateAcceptedEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, + RolloverEpochEvent, + StageHistoryEvent, + StartGenerationEvent, + UninitializedContextAdmissionState, + UnsupportedContextAdmissionProtocolError, +) +from .types._type_enums import ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageState, + GenerationState, + MeasurementKind, + ReserveClass, + WitnessKind, +) +from .types._type_helpers import _reconciled_snapshot_counts + +__all__ = [ + "ContextAdmissionValidationError", + "UnsupportedContextAdmissionProtocolError", + "reduce_context_admission", + "replay_context_admission", + "resolve_context_admission_coverage", +] + + +def _effect_coordinates( + state: ContextAdmissionState, + *, + capacity_changed: bool, +) -> tuple[AggregateRevision, AdmissionSequence]: + return ( + AggregateRevision(state.aggregate_revision.value + 1), + AdmissionSequence(state.admission_sequence.value + (1 if capacity_changed else 0)), + ) + + +def _occurrence_effects( + state: ContextAdmissionState, + event: ContextAdmissionEvent, + batch: AdmissionBatch, + previous_state: AdmissionState, + next_state: AdmissionState, + *, + capacity_changed: bool, +) -> tuple[AdmissionEffect, ...]: + revision, sequence = _effect_coordinates( + state, + capacity_changed=capacity_changed, + ) + return tuple( + OccurrenceStateChangedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=occurrence_id, + previous_state=previous_state, + next_state=next_state, + ) + for occurrence_id in batch.occurrence_ids + ) + + +def _acceptance_effects( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + record: AdmissionBatchRecord, + exact_charge: int, + witness: AdmissionWitness, + *, + quarantine_reason_code: str | None, +) -> tuple[AdmissionEffect, ...]: + reservation = _reservation_for(state, record) + if reservation is None: + return () + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ChargeCommittedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + count=exact_charge, + window_epoch_id=state.snapshot.window_epoch_id, + snapshot_sequence=state.snapshot.snapshot_sequence, + witness_ids=(witness.witness_id,), + ), + *_occurrence_effects( + state, + event, + record.batch, + record.state, + ( + AdmissionState.QUARANTINED + if quarantine_reason_code is not None + else AdmissionState.COMMITTED + ), + capacity_changed=True, + ), + ) + if quarantine_reason_code is not None: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + reason_code=quarantine_reason_code, + ), + ) + return effects + + +def _accepted_effects( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + record: AdmissionBatchRecord, + exact_charge: int, + witness: AdmissionWitness, +) -> tuple[AdmissionEffect, ...]: + reservation = _reservation_for(state, record) + quarantined = reservation is not None and ( + exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit + ) + return _acceptance_effects( + state, + event, + record, + exact_charge, + witness, + quarantine_reason_code=("provider-charge-exceeds-reservation" if quarantined else None), + ) + + +def _reservation_for( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, +) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in state.reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + +def _capacity( + state: ActiveContextAdmissionState, +) -> tuple[ + int, + int, + dict[tuple[ReserveClass, ProtectedPoolOwnerId], int], +]: + charged_by_pool: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} + global_charged = 0 + + for record in state.batch_records: + reservation = _reservation_for(state, record) + charged = record.charged_input_count(reservation) + global_charged += charged + owner = record.batch.protected_pool_owner_id + if owner is not None: + key = (record.batch.reserve_class, owner) + charged_by_pool[key] = charged_by_pool.get(key, 0) + charged + + for generation in state.generation_reservations: + generation_charge = generation.charged_output_count() + if generation_charge > 0: + global_charged += generation_charge + owner = generation.protected_pool_owner_id + if owner is not None: + key = (generation.reserve_class, owner) + charged_by_pool[key] = charged_by_pool.get(key, 0) + generation_charge + + global_unallocated = max(state.snapshot.remaining_count - global_charged, 0) + pool_available: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} + for pool in state.protected_pools: + key = (pool.reserve_class, pool.capability_owner_id) + unused = max(pool.injected_count - charged_by_pool.get(key, 0), 0) + pool_available[key] = min(unused, global_unallocated) + ordinary_available = max( + global_unallocated - sum(pool_available.values()), + 0, + ) + return global_unallocated, ordinary_available, pool_available + + +def _decision( + state: ContextAdmissionState, + kind: AdmissionDecisionKind, + reason_code: str, + *, + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, +) -> AdmissionDecision: + if isinstance(state, UninitializedContextAdmissionState): + return AdmissionDecision( + kind=kind, + reason_code=reason_code, + window_epoch_id=None, + snapshot_sequence=None, + requested_count=requested_count, + available_ordinary_count=0, + available_protected_count=0, + ) + _, ordinary_available, pool_available = _capacity(state) + protected_available = 0 + if protected_pool_owner_id is not None: + protected_available = pool_available.get( + (reserve_class, protected_pool_owner_id), + 0, + ) + return AdmissionDecision( + kind=kind, + reason_code=reason_code, + window_epoch_id=state.snapshot.window_epoch_id, + snapshot_sequence=state.snapshot.snapshot_sequence, + requested_count=requested_count, + available_ordinary_count=ordinary_available, + available_protected_count=protected_available, + ) + + +def _reject( + state: ContextAdmissionState, + event: ContextAdmissionEvent, + reason_code: str, + *, + kind: AdmissionDecisionKind = AdmissionDecisionKind.WOULD_REJECT, + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, +) -> AdmissionTransition: + decision = _decision( + state, + kind, + reason_code, + requested_count=requested_count, + reserve_class=reserve_class, + protected_pool_owner_id=protected_pool_owner_id, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + ) + next_state = replace( + state, + processed_events=tuple( + sorted( + state.processed_events + (processed,), + key=lambda record: ( + record.aggregate_revision.value, + record.event_id.value, + ), + ) + ), + ) + return AdmissionTransition( + next_state=next_state, + decision=decision, + effects=(), + ) + + +def _publish( + prior_state: ContextAdmissionState, + next_state: ContextAdmissionState, + event: ContextAdmissionEvent, + *, + kind: AdmissionDecisionKind = AdmissionDecisionKind.WOULD_ADMIT, + reason_code: str = "accepted", + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, + capacity_changed: bool = False, + effects: tuple[AdmissionEffect, ...] = (), + idempotency_record: IdempotencyRecord | None = None, +) -> AdmissionTransition: + aggregate_revision = type(prior_state.aggregate_revision)( + prior_state.aggregate_revision.value + 1 + ) + admission_sequence = type(prior_state.admission_sequence)( + prior_state.admission_sequence.value + (1 if capacity_changed else 0) + ) + published = replace( + next_state, + aggregate_revision=aggregate_revision, + admission_sequence=admission_sequence, + ) + decision = _decision( + published, + kind, + reason_code, + requested_count=requested_count, + reserve_class=reserve_class, + protected_pool_owner_id=protected_pool_owner_id, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=aggregate_revision, + admission_sequence=admission_sequence, + ) + idempotency_records = published.idempotency_records + if idempotency_record is not None: + idempotency_records = tuple( + sorted( + idempotency_records + (idempotency_record,), + key=lambda item: ( + item.publication_revision.value, + item.owning_event_id.value, + ), + ) + ) + published = replace( + published, + processed_events=tuple( + sorted( + published.processed_events + (processed,), + key=lambda record: ( + record.aggregate_revision.value, + record.event_id.value, + ), + ) + ), + idempotency_records=idempotency_records, + ) + return AdmissionTransition( + next_state=published, + decision=decision, + effects=effects, + ) + + +def _preflight( + state: ContextAdmissionState, + event: ContextAdmissionEvent, +) -> AdmissionTransition | None: + prior = next( + (record for record in state.processed_events if record.event_id == event.event_id), + None, + ) + if prior is not None: + same_event = prior.event == event + if same_event: + original = prior.original_decision + return AdmissionTransition( + next_state=state, + decision=AdmissionDecision( + kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, + reason_code="event-replay", + window_epoch_id=original.window_epoch_id, + snapshot_sequence=original.snapshot_sequence, + requested_count=original.requested_count, + available_ordinary_count=original.available_ordinary_count, + available_protected_count=original.available_protected_count, + ), + effects=(), + ) + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.CONFLICT, + "event-id-conflict", + ), + effects=(), + ) + if isinstance(event, ReserveRequestEvent) and event.input_reservations: + reservation_key = event.input_reservations[0].key + if any( + tombstone.reservation_key == reservation_key + for tombstone in state.expired_idempotency_tombstones + ): + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.IDEMPOTENCY_EXPIRED, + "idempotency-expired", + ), + effects=(), + ) + idempotency_record = next( + ( + record + for record in state.idempotency_records + if record.reservation_key == reservation_key + ), + None, + ) + if idempotency_record is not None: + stored_descriptor = idempotency_record.original_descriptor + same_intent = ( + stored_descriptor.protocol_version == event.protocol_version + and stored_descriptor.idempotency_namespace == event.idempotency_namespace + and stored_descriptor.batch == event.batch + and stored_descriptor.snapshot_sequence == event.snapshot_sequence + and stored_descriptor.input_reservations == event.input_reservations + and stored_descriptor.generation_reservation == event.generation_reservation + ) + if same_intent: + original_decision = idempotency_record.original_reserve_decision + replay_decision = AdmissionDecision( + kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, + reason_code="reservation-key-replay", + window_epoch_id=original_decision.window_epoch_id, + snapshot_sequence=original_decision.snapshot_sequence, + requested_count=original_decision.requested_count, + available_ordinary_count=original_decision.available_ordinary_count, + available_protected_count=original_decision.available_protected_count, + ) + return AdmissionTransition( + next_state=state, + decision=replay_decision, + effects=(), + ) + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.CONFLICT, + "reservation-key-conflict", + ), + effects=(), + ) + if event.expected_aggregate_revision != state.aggregate_revision: + return _reject(state, event, "stale-revision") + return None + + +def _batch_record( + state: ActiveContextAdmissionState, + batch_id: AdmissionBatchId, +) -> AdmissionBatchRecord | None: + return next( + (record for record in state.batch_records if record.batch.batch_id == batch_id), + None, + ) + + +def _highest_dispatch_sequence(state: ActiveContextAdmissionState) -> int: + return sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == state.snapshot.window_epoch_id + and record.event.witness.window_epoch_number == state.snapshot.window_epoch_number + for record in state.processed_events + ) + + +def _required_release_witness_kind( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + snapshot: ContextWindowSnapshot | None = None, +) -> WitnessKind | None: + if batch.protected_pool_owner_id is None: + return None + policy_snapshot = snapshot or state.snapshot + if ( + policy_snapshot.window_epoch_id == state.snapshot.window_epoch_id + and policy_snapshot.window_epoch_number == state.snapshot.window_epoch_number + ): + protected_pools = state.protected_pools + else: + policy_event = next( + ( + record.event + for record in state.processed_events + if ( + isinstance(record.event, OpenEpochEvent) + and record.event.snapshot.window_epoch_id == policy_snapshot.window_epoch_id + and record.event.snapshot.window_epoch_number + == policy_snapshot.window_epoch_number + ) + or ( + isinstance(record.event, RolloverEpochEvent) + and record.event.new_snapshot.window_epoch_id + == policy_snapshot.window_epoch_id + and record.event.new_snapshot.window_epoch_number + == policy_snapshot.window_epoch_number + ) + ), + None, + ) + protected_pools = policy_event.protected_pools if policy_event is not None else () + pool = next( + ( + item + for item in protected_pools + if item.reserve_class is batch.reserve_class + and item.capability_owner_id == batch.protected_pool_owner_id + ), + None, + ) + return pool.required_release_witness_kind if pool is not None else None + + +def _append_witness_ids( + witness_ids: tuple[AdmissionWitnessId, ...], + *new_witness_ids: AdmissionWitnessId, +) -> tuple[AdmissionWitnessId, ...]: + return tuple( + sorted( + {*witness_ids, *new_witness_ids}, + key=lambda witness_id: witness_id.value, + ) + ) + + +def _generation_record( + state: ActiveContextAdmissionState, + reservation_id: GenerationReservationId, +) -> GenerationReservationRecord | None: + return next( + ( + record + for record in state.generation_reservations + if record.generation_reservation_id == reservation_id + ), + None, + ) + + +def _replace_batch_record( + state: ActiveContextAdmissionState, + updated: AdmissionBatchRecord, +) -> ActiveContextAdmissionState: + member_ids = set(updated.batch.occurrence_ids) + return replace( + state, + batch_records=tuple( + updated if record.batch.batch_id == updated.batch.batch_id else record + for record in state.batch_records + ), + occurrence_records=tuple( + replace( + record, + state=updated.state, + batch_id=updated.batch.batch_id, + reservation_id=updated.reservation_id, + ) + if record.occurrence.occurrence_id in member_ids + else record + for record in state.occurrence_records + ), + ) + + +def _quarantined_acceptance_state( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, + witness: AdmissionWitness, + exact_charge: int, + reason_code: str, +) -> ActiveContextAdmissionState: + quarantined = replace( + record, + state=AdmissionState.QUARANTINED, + witness_ids=_append_witness_ids(record.witness_ids, witness.witness_id), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, quarantined) + return _set_occurrence_state( + next_state, + record.batch, + AdmissionState.QUARANTINED, + witness=witness, + quarantine_reason_code=reason_code, + ) + + +def _set_occurrence_state( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + lifecycle_state: AdmissionState, + *, + reservation_id: AdmissionReservationId | None = None, + witness: AdmissionWitness | None = None, + indeterminate_reason_code: str | None = None, + quarantine_reason_code: str | None = None, +) -> ActiveContextAdmissionState: + member_ids = set(batch.occurrence_ids) + records: list[AdmissionOccurrenceRecord] = [] + for record in state.occurrence_records: + if record.occurrence.occurrence_id not in member_ids: + records.append(record) + continue + witness_ids = record.accepted_witness_ids + if witness is not None and witness.witness_id not in witness_ids: + witness_ids = _append_witness_ids(witness_ids, witness.witness_id) + records.append( + replace( + record, + state=lifecycle_state, + batch_id=batch.batch_id, + reservation_id=( + reservation_id if reservation_id is not None else record.reservation_id + ), + accepted_witness_ids=witness_ids, + indeterminate_reason_code=indeterminate_reason_code, + quarantine_reason_code=quarantine_reason_code, + ) + ) + return replace(state, occurrence_records=tuple(records)) + + +def _validate_witness( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + witness: AdmissionWitness, + expected_kind: WitnessKind, +) -> bool: + return _validate_witness_for_snapshot( + state.snapshot, + batch, + witness, + expected_kind, + ) + + +def _validate_witness_for_snapshot( + snapshot: ContextWindowSnapshot, + batch: AdmissionBatch, + witness: AdmissionWitness, + expected_kind: WitnessKind, +) -> bool: + return ( + witness.kind is expected_kind + and witness.window_epoch_id == snapshot.window_epoch_id + and witness.window_epoch_number == snapshot.window_epoch_number + and witness.snapshot_sequence == snapshot.snapshot_sequence + and witness.request_id == batch.request_id + and witness.batch_id == batch.batch_id + and witness.representation_revision == batch.manifest.representation_revision + and witness.representation_binding_id == batch.manifest.representation_binding_id + and witness.occurrence_ids == batch.occurrence_ids + ) + + +def _closed_batch_location( + state: ActiveContextAdmissionState, + batch_id: AdmissionBatchId, +) -> tuple[int, ClosedEpochAudit, AdmissionBatchRecord] | None: + for index, audit in enumerate(state.closed_epochs): + for record in audit.terminal_batch_records: + if record.batch.batch_id == batch_id: + return index, audit, record + return None + + +def _closed_generation_location( + state: ActiveContextAdmissionState, + reservation_id: GenerationReservationId, +) -> tuple[int, ClosedEpochAudit, GenerationReservationRecord] | None: + for index, audit in enumerate(state.closed_epochs): + for record in audit.terminal_generation_reservations: + if record.generation_reservation_id == reservation_id: + return index, audit, record + return None + + +def _replace_closed_audit( + state: ActiveContextAdmissionState, + index: int, + audit: ClosedEpochAudit, +) -> ActiveContextAdmissionState: + return replace( + state, + closed_epochs=tuple( + audit if item_index == index else item + for item_index, item in enumerate(state.closed_epochs) + ), + ) + + +def _reconcile_deducted_closed_charge( + state: ActiveContextAdmissionState, + audit: ClosedEpochAudit, + *, + deducted_charge: int, + terminal_charge: int, +) -> ActiveContextAdmissionState: + if audit.fence_proof is not None or deducted_charge == terminal_charge: + return state + snapshot = state.snapshot + active_count, remaining_count = _reconciled_snapshot_counts( + snapshot.active_count, + snapshot.remaining_count, + snapshot.hard_limit, + deducted_charge, + terminal_charge, + ) + return replace( + state, + snapshot=replace(snapshot, active_count=active_count, remaining_count=remaining_count), + ) + + +def _open_epoch( + state: ContextAdmissionState, + event: OpenEpochEvent, +) -> AdmissionTransition: + if not isinstance(state, UninitializedContextAdmissionState): + return _reject(state, event, "epoch-already-active") + try: + active = ActiveContextAdmissionState( + protocol_version=state.protocol_version, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + snapshot=event.snapshot, + protected_pools=event.protected_pools, + occurrence_records=(), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=state.processed_events, + idempotency_records=state.idempotency_records, + expired_idempotency_tombstones=state.expired_idempotency_tombstones, + closed_epochs=state.closed_epochs, + ) + except ContextAdmissionValidationError: + return _reject(state, event, "invalid-epoch-snapshot") + return _publish(state, active, event) + + +def _propose( + state: ContextAdmissionState, + event: ProposeOccurrenceEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + occurrence = event.occurrence + lineage = occurrence.lineage + is_fork_work = ( + lineage.current_session_id != lineage.root_session_id + or lineage.current_agent_id != lineage.root_agent_id + or lineage.current_thread_id != lineage.root_thread_id + or lineage.parent_agent_id is not None + or lineage.parent_thread_id is not None + or lineage.fork_occurrence_id is not None + ) + is_parent_delivery = ( + occurrence.producer_surface is ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + and lineage.delivery_occurrence_id is not None + ) + existing = next( + ( + record + for record in state.occurrence_records + if record.occurrence.occurrence_id == occurrence.occurrence_id + ), + None, + ) + if existing is None: + existing = next( + ( + record + for audit in state.closed_epochs + for record in audit.terminal_occurrence_records + if record.occurrence.occurrence_id == occurrence.occurrence_id + ), + None, + ) + if existing is not None: + if existing.occurrence == occurrence: + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.NOOP_IDEMPOTENT, + "occurrence-replay", + ), + effects=(), + ) + return _reject( + state, + event, + "occurrence-identity-corruption", + kind=AdmissionDecisionKind.QUARANTINED, + ) + if ( + occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id + or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number + ): + return _reject(state, event, "occurrence-epoch-mismatch") + if is_fork_work and not is_parent_delivery: + return _reject(state, event, "fork-requires-distinct-epoch") + record = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + return _publish( + state, + replace( + state, + occurrence_records=tuple( + sorted( + state.occurrence_records + (record,), + key=lambda item: item.occurrence.occurrence_id.value, + ) + ), + ), + event, + ) + + +def _reserve( + state: ContextAdmissionState, + event: ReserveRequestEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + if event.snapshot_sequence != state.snapshot.snapshot_sequence: + return _reject(state, event, "snapshot-sequence-mismatch") + if ( + _batch_record(state, event.batch.batch_id) is not None + or _closed_batch_location(state, event.batch.batch_id) is not None + ): + return _reject(state, event, "batch-already-reserved") + reservation = event.input_reservations[0] + if any( + existing.reservation_id == reservation.reservation_id for existing in state.reservations + ): + return _reject(state, event, "reservation-id-reuse-with-changed-descriptor") + member_records = tuple( + record + for record in state.occurrence_records + if record.occurrence.occurrence_id in set(event.batch.occurrence_ids) + ) + if ( + len(member_records) != len(event.batch.occurrence_ids) + or tuple(record.occurrence.occurrence_id for record in member_records) + != event.batch.occurrence_ids + or any(record.state is not AdmissionState.PROPOSED for record in member_records) + or any( + record.occurrence.reserve_class is not event.batch.reserve_class + for record in member_records + ) + ): + return _reject(state, event, "batch-members-not-all-proposed") + owned_pairs = tuple( + (span_id, member.occurrence.occurrence_id) + for member in member_records + for span_id in member.occurrence.owned_span_ids + ) + owned_span_ids = tuple(span_id for span_id, _ in owned_pairs) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.batch.manifest.span_owners + ) + if ( + len(owned_span_ids) != len(set(owned_span_ids)) + or set(owned_pairs) != set(manifest_pairs) + or len(owned_pairs) != len(manifest_pairs) + ): + return _reject(state, event, "inconsistent-span-ownership") + if len(event.input_reservations) != 1: + return _reject(state, event, "atomic-input-reservation-required") + if ( + reservation.key.batch_id != event.batch.batch_id + or reservation.occurrence_ids != event.batch.occurrence_ids + or reservation.snapshot_sequence != state.snapshot.snapshot_sequence + or reservation.window_epoch_id != state.snapshot.window_epoch_id + or reservation.window_epoch_number != state.snapshot.window_epoch_number + or reservation.reserve_class is not event.batch.reserve_class + or reservation.protected_pool_owner_id != event.batch.protected_pool_owner_id + ): + return _reject(state, event, "reservation-descriptor-mismatch") + expected_revisions = tuple( + ( + record.occurrence.occurrence_id, + record.occurrence.representation_revision, + ) + for record in member_records + ) + if reservation.key.occurrence_revisions != expected_revisions: + return _reject(state, event, "reservation-revision-mismatch") + generation = event.generation_reservation + generation_count = generation.maximum_allowance if generation is not None else 0 + if generation is not None and ( + any( + existing.generation_reservation_id == generation.generation_reservation_id + for existing in state.generation_reservations + ) + or any( + existing.generation_reservation_id == generation.generation_reservation_id + for audit in state.closed_epochs + for existing in audit.terminal_generation_reservations + ) + ): + return _reject( + state, + event, + "generation-reservation-id-reuse-with-changed-descriptor", + ) + if generation is not None and ( + generation.request_id != event.batch.request_id + or generation.batch_id != event.batch.batch_id + or generation.representation_revision != event.batch.manifest.representation_revision + or generation.occurrence_ids != event.batch.occurrence_ids + or generation.window_epoch_id != state.snapshot.window_epoch_id + or generation.window_epoch_number != state.snapshot.window_epoch_number + or generation.snapshot_sequence != state.snapshot.snapshot_sequence + or generation.reserve_class is not event.batch.reserve_class + or generation.protected_pool_owner_id != event.batch.protected_pool_owner_id + ): + return _reject(state, event, "generation-descriptor-mismatch") + requested = reservation.reserved_count + generation_count + global_available, ordinary_available, pool_available = _capacity(state) + if event.batch.protected_pool_owner_id is None: + available = ordinary_available + else: + available = min( + global_available, + pool_available.get( + ( + event.batch.reserve_class, + event.batch.protected_pool_owner_id, + ), + 0, + ), + ) + if requested > available: + return _reject( + state, + event, + "insufficient-capacity", + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + ) + if event.batch.protected_pool_owner_id is not None: + pool = next( + ( + item + for item in state.protected_pools + if item.reserve_class is event.batch.reserve_class + and item.capability_owner_id == event.batch.protected_pool_owner_id + ), + None, + ) + if pool is None: + return _reject(state, event, "unknown-protected-pool") + batch_record = AdmissionBatchRecord( + batch=event.batch, + state=AdmissionState.RESERVED, + reservation_id=reservation.reservation_id, + witness_ids=(), + committed_input_count=0, + unresolved_input_count=0, + ) + member_ids = set(event.batch.occurrence_ids) + reserved_occurrence_records = tuple( + replace( + record, + state=AdmissionState.RESERVED, + batch_id=event.batch.batch_id, + reservation_id=reservation.reservation_id, + ) + if record.occurrence.occurrence_id in member_ids + else record + for record in state.occurrence_records + ) + next_state = replace( + state, + occurrence_records=reserved_occurrence_records, + batch_records=tuple( + sorted( + state.batch_records + (batch_record,), + key=lambda item: item.batch.batch_id.value, + ) + ), + reservations=tuple( + sorted( + state.reservations + event.input_reservations, + key=lambda item: item.reservation_id.value, + ) + ), + generation_reservations=tuple( + sorted( + state.generation_reservations + + ((generation,) if generation is not None and generation_count > 0 else ()), + key=lambda item: item.generation_reservation_id.value, + ) + ), + ) + reserve_decision = _decision( + next_state, + AdmissionDecisionKind.WOULD_ADMIT, + "accepted", + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + ) + idempotency_record = IdempotencyRecord( + namespace=event.idempotency_namespace, + reservation_key=reservation.key, + original_descriptor=event, + original_reserve_decision=reserve_decision, + owning_event_id=event.event_id, + publication_revision=type(state.aggregate_revision)(state.aggregate_revision.value + 1), + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ReservationRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(), + ), + *_occurrence_effects( + state, + event, + event.batch, + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + capacity_changed=True, + ), + ) + if generation is not None and generation_count > 0: + effects += ( + GenerationReservationRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation_count, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(), + ), + ) + return _publish( + state, + next_state, + event, + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + capacity_changed=True, + effects=effects, + idempotency_record=idempotency_record, + ) + + +def _prepare( + state: ContextAdmissionState, + event: PrepareBatchEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if record is None or record.state is not AdmissionState.RESERVED: + return _reject(state, event, "illegal-prepare-order") + if event.representation_revision != record.batch.manifest.representation_revision: + return _reject(state, event, "representation-revision-mismatch") + if event.representation_binding_id != record.batch.manifest.representation_binding_id: + return _reject(state, event, "representation-binding-mismatch") + reservation = _reservation_for(state, record) + if reservation is None or event.proposed_charge != reservation.reserved_count: + return _reject(state, event, "prepared-charge-mismatch") + if event.measurement_kind not in { + MeasurementKind.PROVIDER_EXACT, + MeasurementKind.TOKENIZER_EXACT, + }: + return _reject(state, event, "non-authoritative-measurement") + updated = replace( + record, + state=AdmissionState.PREPARED, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.PREPARED, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + capacity_changed=False, + ), + ) + + +def _stage( + state: ContextAdmissionState, + event: StageHistoryEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if ( + record is None + or record.state is not AdmissionState.PREPARED + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.HISTORY_STAGED, + ) + ): + return _reject(state, event, "invalid-history-stage-witness") + updated = replace( + record, + state=AdmissionState.HISTORY_STAGED, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.HISTORY_STAGED, + witness=event.witness, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + capacity_changed=False, + ), + ) + + +def _dispatch( + state: ContextAdmissionState, + event: DispatchRequestEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if ( + record is None + or record.state is not AdmissionState.HISTORY_STAGED + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.REQUEST_INCLUDED, + ) + ): + return _reject(state, event, "invalid-request-inclusion-witness") + updated = replace( + record, + state=AdmissionState.REQUEST_DISPATCHED, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.REQUEST_DISPATCHED, + witness=event.witness, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + record.state, + AdmissionState.REQUEST_DISPATCHED, + capacity_changed=False, + ), + ) + + +def _accepted_state( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, + witness: AdmissionWitness, + exact_charge: int, +) -> tuple[ActiveContextAdmissionState, AdmissionDecisionKind, str]: + reservation = _reservation_for(state, record) + if reservation is None: + return state, AdmissionDecisionKind.QUARANTINED, "missing-reservation" + quarantined = ( + exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit + ) + lifecycle = AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED + updated = replace( + record, + state=lifecycle, + witness_ids=_append_witness_ids(record.witness_ids, witness.witness_id), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + lifecycle, + witness=witness, + quarantine_reason_code=("provider-charge-exceeds-reservation" if quarantined else None), + ) + return ( + next_state, + (AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT), + "provider-charge-exceeds-reservation" if quarantined else "accepted", + ) + + +def _accept_closed_input( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], +) -> AdmissionTransition: + index, audit, record = location + expected_state = ( + AdmissionState.REQUEST_DISPATCHED + if isinstance(event, AcceptInputEvent) + else AdmissionState.INDETERMINATE + ) + binding = event.representation_binding_witness + expected_revision = record.batch.manifest.representation_revision + exact_charge = ( + event.exact_input_charge if isinstance(event, AcceptInputEvent) else event.exact_charge + ) + if ( + record.state is not expected_state + or not _validate_witness_for_snapshot( + audit.snapshot, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ) + or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT + or event.final_manifest_revision != expected_revision + or event.final_manifest != record.batch.manifest + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.representation_binding_id != record.batch.manifest.representation_binding_id + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + ): + return _reject(state, event, "invalid-closed-epoch-acceptance") + reservation = audit.reservation_for(record) + if reservation is None: + return _reject(state, event, "missing-closed-epoch-reservation") + member_ids = set(record.batch.occurrence_ids) + expected_owned_pairs = tuple( + (span_id, item.occurrence.occurrence_id) + for item in audit.terminal_occurrence_records + if item.occurrence.occurrence_id in member_ids + for span_id in item.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) + manifest_invalid = ( + len({span_id for span_id, _ in expected_owned_pairs}) != len(expected_owned_pairs) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) + ) + authority_mismatch = ( + event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id + ) + quarantined = ( + authority_mismatch + or exact_charge > reservation.reserved_count + or exact_charge > audit.snapshot.hard_limit + or manifest_invalid + ) + reason_code = ( + "authority-source-mismatch" + if authority_mismatch + else "incomplete-canonical-span-ownership" + if manifest_invalid + else "provider-charge-exceeds-reservation" + if quarantined + else "accepted" + ) + lifecycle = AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED + updated_record = replace( + record, + state=lifecycle, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + batch_records = tuple( + updated_record if item.batch.batch_id == record.batch.batch_id else item + for item in audit.terminal_batch_records + ) + occurrence_records = tuple( + replace( + item, + state=lifecycle, + accepted_witness_ids=_append_witness_ids( + item.accepted_witness_ids, + event.witness.witness_id, + ), + quarantine_reason_code=(reason_code if quarantined else None), + ) + if item.occurrence.occurrence_id in member_ids + else item + for item in audit.terminal_occurrence_records + ) + updated_audit = replace( + audit, + terminal_occurrence_records=occurrence_records, + terminal_batch_records=batch_records, + retained_unresolved_count=audit.retained_input_count(batch_records), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=audit.retained_input_count((record,)), + terminal_charge=exact_charge, + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ChargeCommittedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + count=exact_charge, + window_epoch_id=audit.snapshot.window_epoch_id, + snapshot_sequence=audit.snapshot.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + *_occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ), + ) + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + reason_code=reason_code, + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=reason_code, + requested_count=exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=effects, + ) + + +def _accept( + state: ContextAdmissionState, + event: AcceptInputEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if record is None: + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _accept_closed_input(state, event, location) + if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: + return _reject(state, event, "illegal-accept-order") + if not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ): + return _reject(state, event, "invalid-provider-acceptance-witness") + binding = event.representation_binding_witness + expected_revision = record.batch.manifest.representation_revision + if ( + event.final_manifest != record.batch.manifest + or binding.representation_binding_id != record.batch.manifest.representation_binding_id + ): + return _reject(state, event, "representation-binding-mismatch") + if ( + event.final_manifest_revision != expected_revision + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + ): + return _reject(state, event, "representation-revision-mismatch") + if event.measurement_kind is not MeasurementKind.PROVIDER_EXACT: + return _reject(state, event, "non-authoritative-measurement") + if event.exact_input_charge < 0: + return _reject(state, event, "invalid-exact-charge") + expected_owned_spans: list[CanonicalSpanId] = [] + expected_owned_pairs: list[tuple[CanonicalSpanId, AdmissionOccurrenceId]] = [] + for occurrence in state.occurrence_records: + if occurrence.occurrence.occurrence_id in set(record.batch.occurrence_ids): + expected_owned_spans.extend(occurrence.occurrence.owned_span_ids) + expected_owned_pairs.extend( + (span_id, occurrence.occurrence.occurrence_id) + for span_id in occurrence.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) + if ( + event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id + ): + reason_code = "authority-source-mismatch" + next_state = _quarantined_acceptance_state( + state, + record, + event.witness, + event.exact_input_charge, + reason_code, + ) + return _publish( + state, + next_state, + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code=reason_code, + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_acceptance_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + quarantine_reason_code=reason_code, + ), + ) + if ( + len(expected_owned_spans) != len(set(expected_owned_spans)) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) + ): + reason_code = "incomplete-canonical-span-ownership" + next_state = _quarantined_acceptance_state( + state, + record, + event.witness, + event.exact_input_charge, + reason_code, + ) + return _publish( + state, + next_state, + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code=reason_code, + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_acceptance_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + quarantine_reason_code=reason_code, + ), + ) + next_state, kind, reason = _accepted_state( + state, + record, + event.witness, + event.exact_input_charge, + ) + return _publish( + state, + next_state, + event, + kind=kind, + reason_code=reason, + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_accepted_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + ), + ) + + +def _release_closed_batch( + state: ActiveContextAdmissionState, + event: ( + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + ), + location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], +) -> AdmissionTransition: + index, audit, record = location + released_input_count = audit.retained_input_count((record,)) + is_release = isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + is_resolution = isinstance( + event, + ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, + ) + expected_state = ( + AdmissionState.INDETERMINATE if is_resolution else AdmissionState.REQUEST_DISPATCHED + ) + expected_kind = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK + required_release_kind = _required_release_witness_kind( + state, + record.batch, + snapshot=audit.snapshot, + ) + if ( + record.state is not expected_state + or ( + record.batch.protected_pool_owner_id is not None + and required_release_kind is not expected_kind + ) + or not _validate_witness_for_snapshot( + audit.snapshot, + record.batch, + event.witness, + expected_kind, + ) + ): + return _reject(state, event, "invalid-closed-epoch-resolution") + lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK + witness_ids = _append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ) + updated_record = replace( + record, + state=lifecycle, + witness_ids=witness_ids, + unresolved_input_count=0, + ) + batch_records = tuple( + updated_record if item.batch.batch_id == record.batch.batch_id else item + for item in audit.terminal_batch_records + ) + member_ids = set(record.batch.occurrence_ids) + occurrence_records = tuple( + replace( + item, + state=lifecycle, + accepted_witness_ids=_append_witness_ids( + item.accepted_witness_ids, + event.witness.witness_id, + ), + indeterminate_reason_code=None, + ) + if item.occurrence.occurrence_id in member_ids + else item + for item in audit.terminal_occurrence_records + ) + updated_audit = replace( + audit, + terminal_occurrence_records=occurrence_records, + terminal_batch_records=batch_records, + retained_unresolved_count=audit.retained_input_count(batch_records), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + reservation = audit.reservation_for(record) + effects: tuple[AdmissionEffect, ...] = _occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ) + if reservation is not None: + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects = ( + ReservationReleasedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + *effects, + ) + generation_effects: tuple[AdmissionEffect, ...] = () + generation_records: list[GenerationReservationRecord] = [] + invalidated_generation_count = 0 + revision, sequence = _effect_coordinates(state, capacity_changed=True) + for generation in audit.terminal_generation_reservations: + if generation.batch_id == record.batch.batch_id and generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + generation_effects += ( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + ) + invalidated_generation_count += generation.maximum_allowance + else: + generation_records.append(generation) + if generation_effects: + updated_audit = replace( + updated_audit, + terminal_generation_reservations=tuple(generation_records), + retained_generation_count=sum( + generation.maximum_allowance + for generation in generation_records + if generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + effects += generation_effects + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=released_input_count + invalidated_generation_count, + terminal_charge=0, + ) + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + +def _release_or_rollback( + state: ContextAdmissionState, + event: ( + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + ), +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if record is None: + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _release_closed_batch(state, event, location) + return _reject(state, event, "unknown-batch") + is_release = isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + is_resolution = isinstance( + event, + ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, + ) + if is_resolution: + allowed_states = {AdmissionState.INDETERMINATE} + elif is_release: + allowed_states = { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + } + else: + allowed_states = { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + } + expected_witness = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK + required_release_kind = _required_release_witness_kind(state, record.batch) + if ( + record.batch.protected_pool_owner_id is not None + and required_release_kind is not expected_witness + ): + return _reject(state, event, "protected-release-policy-mismatch") + if record.state not in allowed_states or not _validate_witness( + state, + record.batch, + event.witness, + expected_witness, + ): + return _reject(state, event, "invalid-release-or-rollback-witness") + lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK + updated = replace( + record, + state=lifecycle, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + lifecycle, + witness=event.witness, + ) + effects: tuple[AdmissionEffect, ...] = _occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ) + reservation = _reservation_for(state, record) + if reservation is not None: + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects = ( + ReservationReleasedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + *effects, + ) + generation_effects: tuple[AdmissionEffect, ...] = () + generation_records: list[GenerationReservationRecord] = [] + revision, sequence = _effect_coordinates(state, capacity_changed=True) + for generation in next_state.generation_reservations: + if generation.batch_id == record.batch.batch_id and generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + generation_effects += ( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + ) + else: + generation_records.append(generation) + if generation_effects: + next_state = replace( + next_state, + generation_reservations=tuple(generation_records), + ) + effects += generation_effects + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + +def _mark_indeterminate( + state: ContextAdmissionState, + event: MarkIndeterminateEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if record is None or record.state not in { + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + return _reject(state, event, "illegal-indeterminate-order") + reservation = _reservation_for(state, record) + unresolved = reservation.reserved_count if reservation is not None else 0 + updated = replace( + record, + state=AdmissionState.INDETERMINATE, + unresolved_input_count=unresolved, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.INDETERMINATE, + indeterminate_reason_code=event.reason_code, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + record.state, + AdmissionState.INDETERMINATE, + capacity_changed=False, + ), + ) + + +def _resolve_indeterminate_accepted( + state: ContextAdmissionState, + event: ResolveIndeterminateAcceptedEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + record = _batch_record(state, event.batch_id) + if record is None: + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _accept_closed_input(state, event, location) + binding = event.representation_binding_witness + expected_revision = ( + record.batch.manifest.representation_revision if record is not None else None + ) + if ( + record is None + or record.state is not AdmissionState.INDETERMINATE + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ) + or event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id + or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT + or event.final_manifest_revision != expected_revision + or event.final_manifest != record.batch.manifest + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.representation_binding_id != record.batch.manifest.representation_binding_id + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + ): + return _reject(state, event, "invalid-indeterminate-acceptance") + member_ids = set(record.batch.occurrence_ids) + expected_owned_pairs = tuple( + (span_id, item.occurrence.occurrence_id) + for item in state.occurrence_records + if item.occurrence.occurrence_id in member_ids + for span_id in item.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) + if ( + len({span_id for span_id, _ in expected_owned_pairs}) != len(expected_owned_pairs) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) + ): + reason_code = "incomplete-canonical-span-ownership" + next_state = _quarantined_acceptance_state( + state, + record, + event.witness, + event.exact_charge, + reason_code, + ) + return _publish( + state, + next_state, + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code=reason_code, + requested_count=event.exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_acceptance_effects( + state, + event, + record, + event.exact_charge, + event.witness, + quarantine_reason_code=reason_code, + ), + ) + next_state, kind, reason = _accepted_state( + state, + record, + event.witness, + event.exact_charge, + ) + return _publish( + state, + next_state, + event, + kind=kind, + reason_code=reason, + requested_count=event.exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_accepted_effects( + state, + event, + record, + event.exact_charge, + event.witness, + ), + ) + + +def _start_generation( + state: ContextAdmissionState, + event: StartGenerationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None or generation.state is not GenerationState.RESERVED: + return _reject(state, event, "illegal-generation-start") + batch_record = _batch_record(state, generation.batch_id) + batch = batch_record.batch if batch_record is not None else None + if ( + batch is None + or batch_record is None + or batch_record.state is not AdmissionState.REQUEST_DISPATCHED + or not _validate_witness( + state, + batch, + event.witness, + WitnessKind.REQUEST_INCLUDED, + ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.witness.authority_source_id + ) + ): + return _reject(state, event, "invalid-generation-start-witness") + updated = replace( + generation, + state=GenerationState.STREAMING, + authority_source_id=event.witness.authority_source_id, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.witness.witness_id, + ), + ) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + return _publish(state, next_state, event) + + +def _reconcile_closed_generation( + state: ActiveContextAdmissionState, + event: ReconcileGenerationEvent, + location: tuple[int, ClosedEpochAudit, GenerationReservationRecord], +) -> AdmissionTransition: + index, audit, generation = location + batch_record = next( + ( + record + for record in audit.terminal_batch_records + if record.batch.batch_id == generation.batch_id + ), + None, + ) + if ( + generation.state + not in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + or batch_record is None + or not _validate_witness_for_snapshot( + audit.snapshot, + batch_record.batch, + event.output_usage_witness, + WitnessKind.OUTPUT_USAGE, + ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.output_usage_witness.authority_source_id + ) + ): + return _reject(state, event, "invalid-closed-generation-witness") + quarantined = event.exact_output_usage > generation.maximum_allowance + updated = replace( + generation, + state=(GenerationState.QUARANTINED if quarantined else GenerationState.RECONCILED), + exact_terminal_usage=event.exact_output_usage, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.output_usage_witness.witness_id, + ), + authority_source_id=event.output_usage_witness.authority_source_id, + ) + generation_records = tuple( + updated if item.generation_reservation_id == generation.generation_reservation_id else item + for item in audit.terminal_generation_reservations + ) + retained_generation_count = sum( + item.maximum_allowance + for item in generation_records + if item.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + updated_audit = replace( + audit, + terminal_generation_reservations=generation_records, + retained_generation_count=retained_generation_count, + ) + next_state = _replace_closed_audit(state, index, updated_audit) + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=generation.maximum_allowance, + terminal_charge=event.exact_output_usage, + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + GenerationReconciledEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=event.exact_output_usage, + window_epoch_id=audit.snapshot.window_epoch_id, + snapshot_sequence=audit.snapshot.snapshot_sequence, + witness_ids=(event.output_usage_witness.witness_id,), + ), + ) + reason_code = "generation-usage-exceeds-allowance" if quarantined else "accepted" + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + reason_code=reason_code, + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=reason_code, + capacity_changed=True, + effects=effects, + ) + + +def _reconcile_generation( + state: ContextAdmissionState, + event: ReconcileGenerationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None: + location = _closed_generation_location( + state, + event.generation_reservation_id, + ) + if location is not None: + return _reconcile_closed_generation(state, event, location) + if generation is None or generation.state not in { + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + return _reject(state, event, "illegal-generation-reconciliation") + batch_record = _batch_record(state, generation.batch_id) + if ( + batch_record is None + or not _validate_witness( + state, + batch_record.batch, + event.output_usage_witness, + WitnessKind.OUTPUT_USAGE, + ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.output_usage_witness.authority_source_id + ) + ): + return _reject(state, event, "invalid-output-usage-witness") + quarantined = event.exact_output_usage > generation.maximum_allowance + updated = replace( + generation, + state=(GenerationState.QUARANTINED if quarantined else GenerationState.RECONCILED), + exact_terminal_usage=event.exact_output_usage, + authority_source_id=event.output_usage_witness.authority_source_id, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.output_usage_witness.witness_id, + ), + ) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + GenerationReconciledEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=event.exact_output_usage, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.output_usage_witness.witness_id,), + ), + ) + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + reason_code="generation-usage-exceeds-allowance", + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=("generation-usage-exceeds-allowance" if quarantined else "accepted"), + capacity_changed=True, + effects=effects, + ) + + +def _mark_generation_indeterminate( + state: ContextAdmissionState, + event: MarkGenerationIndeterminateEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None or generation.state not in { + GenerationState.RESERVED, + GenerationState.STREAMING, + }: + return _reject(state, event, "illegal-generation-indeterminate") + updated = replace(generation, state=GenerationState.INDETERMINATE) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + return _publish(state, next_state, event) + + +def _request_reconciliation( + state: ContextAdmissionState, + event: RequestReconciliationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + batch = ( + _batch_record(state, event.target_id) + if isinstance(event.target_id, AdmissionBatchId) + else None + ) + generation = ( + _generation_record(state, event.target_id) + if isinstance(event.target_id, GenerationReservationId) + else None + ) + closed_batch = ( + _closed_batch_location(state, event.target_id) + if isinstance(event.target_id, AdmissionBatchId) + else None + ) + closed_generation = ( + _closed_generation_location(state, event.target_id) + if isinstance(event.target_id, GenerationReservationId) + else None + ) + if not ( + ( + batch is not None + and batch.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + or ( + generation is not None + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + or ( + closed_batch is not None + and closed_batch[2].state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + or ( + closed_generation is not None + and closed_generation[2].state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + ): + return _reject(state, event, "reconciliation-target-not-unresolved") + revision, sequence = _effect_coordinates(state, capacity_changed=False) + effect_type = ( + ReconciliationEscalationEffect + if "deadline" in event.reason_code.casefold() + else ReconciliationQueryRequestedEffect + ) + return _publish( + state, + state, + event, + effects=( + effect_type( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=event.target_id, + reason_code=event.reason_code, + ), + ), + ) + + +def _expire_idempotency( + state: ContextAdmissionState, + event: ExpireIdempotencyKeyEvent, +) -> AdmissionTransition: + record = next( + ( + item + for item in state.idempotency_records + if item.reservation_key == event.reservation_key + ), + None, + ) + if record is None: + return _reject(state, event, "idempotency-key-not-terminal") + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "idempotency-key-not-terminal") + if any( + tombstone.namespace == record.namespace + and tombstone.reservation_key == event.reservation_key + for tombstone in state.expired_idempotency_tombstones + ): + return _reject(state, event, "idempotency-key-expired") + batch_record: AdmissionBatchRecord | None + snapshot: ContextWindowSnapshot + generation_records: tuple[GenerationReservationRecord, ...] + if ( + event.reservation_key.window_epoch_id == state.snapshot.window_epoch_id + and event.reservation_key.window_epoch_number == state.snapshot.window_epoch_number + ): + batch_record = _batch_record( + state, + record.original_descriptor.batch.batch_id, + ) + snapshot = state.snapshot + generation_records = state.generation_reservations + else: + audit = next( + ( + item + for item in state.closed_epochs + if item.snapshot.window_epoch_id == event.reservation_key.window_epoch_id + and item.snapshot.window_epoch_number == event.reservation_key.window_epoch_number + ), + None, + ) + if audit is None: + return _reject(state, event, "idempotency-key-not-terminal") + batch_record = next( + ( + item + for item in audit.terminal_batch_records + if item.batch.batch_id == record.original_descriptor.batch.batch_id + ), + None, + ) + snapshot = audit.snapshot + generation_records = audit.terminal_generation_reservations + if batch_record is None or batch_record.state not in { + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.INVALIDATED, + AdmissionState.QUARANTINED, + }: + return _reject(state, event, "idempotency-key-not-terminal") + if any( + generation.batch_id == batch_record.batch.batch_id + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + for generation in generation_records + ): + return _reject(state, event, "idempotency-key-not-terminal") + if not _validate_witness_for_snapshot( + snapshot, + batch_record.batch, + event.expiry_witness, + WitnessKind.IDEMPOTENCY_EXPIRY, + ): + return _reject(state, event, "invalid-expiry-witness") + tombstone = ExpiredIdempotencyTombstone( + namespace=record.namespace, + reservation_key=record.reservation_key, + original_descriptor=record.original_descriptor, + expiry_witness=event.expiry_witness, + original_terminal_decision=record.original_reserve_decision, + ) + next_state = replace( + state, + expired_idempotency_tombstones=tuple( + sorted( + state.expired_idempotency_tombstones + (tombstone,), + key=lambda item: ( + item.reservation_key.window_epoch_number, + item.reservation_key.batch_id.value, + ), + ) + ), + ) + revision, sequence = _effect_coordinates(state, capacity_changed=False) + reservation = record.original_descriptor.input_reservations[0] + return _publish( + state, + next_state, + event, + effects=( + IdempotencyExpiredEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + reservation_key=event.reservation_key, + expiry_witness_id=event.expiry_witness.witness_id, + ), + ), + ) + + +def _rollover( + state: ContextAdmissionState, + event: RolloverEpochEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "epoch-uninitialized") + proof = event.fence_proof + if ( + event.witness.kind is not WitnessKind.EPOCH_ROLLOVER + or event.witness.window_epoch_id != state.snapshot.window_epoch_id + or event.witness.window_epoch_number != state.snapshot.window_epoch_number + or event.witness.snapshot_sequence != state.snapshot.snapshot_sequence + or event.new_snapshot.window_epoch_number <= state.snapshot.window_epoch_number + or event.new_snapshot.window_epoch_id == state.snapshot.window_epoch_id + ): + return _reject(state, event, "invalid-rollover-witness") + unresolved_batch_records = tuple( + record + for record in state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + retained_generation_batch_ids = { + record.batch.batch_id + for record in state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + } + retained_unresolved_count = sum( + record.unresolved_input_count + or ( + reservation.reserved_count + if (reservation := _reservation_for(state, record)) is not None + else 0 + ) + for record in unresolved_batch_records + ) + retained_generation_count = sum( + generation.maximum_allowance + for generation in state.generation_reservations + if generation.batch_id in retained_generation_batch_ids + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + retained_total = retained_unresolved_count + retained_generation_count + receiver_fence_valid = proof is not None and ( + proof.old_window_epoch_id == state.snapshot.window_epoch_id + and proof.old_window_epoch_number == state.snapshot.window_epoch_number + and proof.new_window_epoch_id == event.new_snapshot.window_epoch_id + and proof.new_window_epoch_number == event.new_snapshot.window_epoch_number + and proof.receiver_authority_source_id == event.witness.authority_source_id + and proof.highest_admitted_dispatch_sequence == _highest_dispatch_sequence(state) + ) + fully_resolved = retained_total == 0 + snapshot_deducts_unresolved = ( + retained_total > 0 + and event.new_snapshot.remaining_count <= state.snapshot.remaining_count - retained_total + ) + authority_alternative_valid = ( + receiver_fence_valid + if proof is not None + else (fully_resolved or snapshot_deducts_unresolved) + ) + if not authority_alternative_valid: + return _reject(state, event, "stale-receiver-fence") + terminal_occurrences = tuple( + replace( + record, + state=( + AdmissionState.INVALIDATED + if record.state + in { + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + else record.state + ), + ) + for record in state.occurrence_records + ) + terminal_batch_records = tuple( + replace( + record, + state=( + AdmissionState.INVALIDATED + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + else record.state + ), + ) + for record in state.batch_records + ) + invalidated_generation_reservations = tuple( + generation + for generation in state.generation_reservations + if generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + and generation.batch_id not in retained_generation_batch_ids + ) + invalidated_generation_ids = { + generation.generation_reservation_id for generation in invalidated_generation_reservations + } + terminal_generation_reservations = tuple( + generation + for generation in state.generation_reservations + if generation.generation_reservation_id not in invalidated_generation_ids + ) + audit = ClosedEpochAudit( + snapshot=state.snapshot, + terminal_occurrence_records=terminal_occurrences, + terminal_batch_records=terminal_batch_records, + terminal_reservations=state.reservations, + terminal_generation_reservations=terminal_generation_reservations, + closure_witness_id=event.witness.witness_id, + fence_proof=proof, + processed_event_tombstones=tuple( + sorted( + (record.event_id for record in state.processed_events), + key=lambda event_id: event_id.value, + ) + ), + retained_unresolved_count=retained_unresolved_count, + retained_generation_count=retained_generation_count, + ) + try: + next_state = ActiveContextAdmissionState( + protocol_version=state.protocol_version, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + snapshot=event.new_snapshot, + protected_pools=event.protected_pools, + occurrence_records=(), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=state.processed_events, + idempotency_records=state.idempotency_records, + expired_idempotency_tombstones=state.expired_idempotency_tombstones, + closed_epochs=tuple( + sorted( + state.closed_epochs + (audit,), + key=lambda item: item.snapshot.window_epoch_number, + ) + ), + ) + except ContextAdmissionValidationError: + return _reject(state, event, "invalid-rollover-snapshot") + revision, sequence = _effect_coordinates(state, capacity_changed=True) + rollover_witness_ids = _append_witness_ids( + (), + event.witness.witness_id, + *((proof.fence_witness_id,) if proof is not None else ()), + ) + invalidation_effects = tuple( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=rollover_witness_ids, + ) + for record in state.batch_records + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + for reservation in state.reservations + if reservation.reservation_id == record.reservation_id + ) + generation_invalidation_effects = tuple( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=rollover_witness_ids, + ) + for generation in invalidated_generation_reservations + ) + occurrence_effects = tuple( + OccurrenceStateChangedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=prior.occurrence.occurrence_id, + previous_state=prior.state, + next_state=terminal.state, + ) + for prior, terminal in zip( + state.occurrence_records, + terminal_occurrences, + strict=True, + ) + if prior.state is not terminal.state + ) + effects: tuple[AdmissionEffect, ...] = ( + *invalidation_effects, + *generation_invalidation_effects, + *occurrence_effects, + EpochClosedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=state.snapshot.window_epoch_id, + fence_proof=proof, + deducted_unresolved_count=( + retained_total if snapshot_deducts_unresolved and proof is None else 0 + ), + ), + ) + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + +def reduce_context_admission( + state: ContextAdmissionState, + event: ContextAdmissionEvent, +) -> AdmissionTransition: + """Apply one protocol event to the complete prior immutable state.""" + preflight = _preflight(state, event) + if preflight is not None: + return preflight + match event: + case OpenEpochEvent(): + return _open_epoch(state, event) + case AuthorityUnavailableEvent(): + kind = ( + AdmissionDecisionKind.UPSTREAM_GATED + if event.authority_state is CoverageState.UPSTREAM_GATED + else AdmissionDecisionKind.WATERMARK_UNAVAILABLE + ) + if isinstance(state, ActiveContextAdmissionState): + revision, sequence = _effect_coordinates( + state, + capacity_changed=False, + ) + return _publish( + state, + state, + event, + kind=kind, + reason_code=event.reason_code, + effects=( + AuthorityUnavailableEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=state.snapshot.window_epoch_id, + reason_code=event.reason_code, + authority_state=event.authority_state, + ), + ), + ) + return _publish( + state, + state, + event, + kind=kind, + reason_code=event.reason_code, + ) + case ProposeOccurrenceEvent(): + return _propose(state, event) + case ReserveRequestEvent(): + return _reserve(state, event) + case PrepareBatchEvent(): + return _prepare(state, event) + case StageHistoryEvent(): + return _stage(state, event) + case DispatchRequestEvent(): + return _dispatch(state, event) + case AcceptInputEvent(): + return _accept(state, event) + case ReleaseNonAdmissionEvent() | RollbackAdmissionEvent(): + return _release_or_rollback(state, event) + case MarkIndeterminateEvent(): + return _mark_indeterminate(state, event) + case ResolveIndeterminateAcceptedEvent(): + return _resolve_indeterminate_accepted(state, event) + case ResolveIndeterminateNonAdmissionEvent() | ResolveIndeterminateRollbackEvent(): + return _release_or_rollback(state, event) + case StartGenerationEvent(): + return _start_generation(state, event) + case ReconcileGenerationEvent(): + return _reconcile_generation(state, event) + case MarkGenerationIndeterminateEvent(): + return _mark_generation_indeterminate(state, event) + case RequestReconciliationEvent(): + return _request_reconciliation(state, event) + case ExpireIdempotencyKeyEvent(): + return _expire_idempotency(state, event) + case RolloverEpochEvent(): + return _rollover(state, event) + case _ as unreachable: + assert_never(unreachable) + + +def replay_context_admission( + initial_state: ContextAdmissionState, + events: tuple[ContextAdmissionEvent, ...], +) -> AdmissionReplay: + """Replay a full stream, feeding each complete next state into the next event.""" + state = initial_state + transitions: list[AdmissionTransition] = [] + for event in events: + transition = reduce_context_admission(state, event) + transitions.append(transition) + state = transition.next_state + return AdmissionReplay(final_state=state, transitions=tuple(transitions)) + + +def resolve_context_admission_coverage( + surface: ProducerSurface, + backend: str, + configuration_mode: str, + source_version: str, + as_of: str, +) -> ProducerCoverageDef: + """Resolve one static coverage row against runtime lineage inputs.""" + row = next( + (item for item in CONTEXT_ADMISSION_COVERAGE if item.surface is surface), + None, + ) + if row is None: + raise ContextAdmissionValidationError("unknown_producer_surface") + evidence = row.evidence[0] + matches = ( + evidence.backend == backend + and evidence.configuration_mode == configuration_mode + and evidence.tested_version == source_version + and evidence.checked_at == as_of + ) + if matches: + return row + return replace( + row, + observation_state=CoverageState.UPSTREAM_GATED, + authority_state=CoverageState.UPSTREAM_GATED, + reason_code="coverage-runtime-mismatch", + ) diff --git a/src/autoskillit/core/types/AGENTS.md b/src/autoskillit/core/types/AGENTS.md index 73e748b61..793775b66 100644 --- a/src/autoskillit/core/types/AGENTS.md +++ b/src/autoskillit/core/types/AGENTS.md @@ -34,13 +34,14 @@ Type re-export hub and all typed building blocks for the autoskillit package (IL | `_type_recipe_sections.py` | Recipe-section schema validation plus canonical section, element, and plan digest helpers | | `_type_capture.py` | `CaptureEntrySpec` and `CaptureValueTypeError` for typed capture contract enforcement | | `_type_dispatch_identity.py` | `DispatchIdentity` frozen value object, `PromptContractError`, and `assert_prompt_sentinel` for sentinel contract enforcement | -| `_type_helpers.py` | Text processing and skill-name extraction utilities | +| `_type_helpers.py` | Text processing, skill-name extraction, and shared content-free validation utilities | | `_type_inspector.py` | Health Inspector types: `InspectorEvidence`, `InspectorVerdict`, `InspectorCallback` (issue #3533) | | `_type_invariant_registry.py` | Invariant registry: `InvariantDef` dataclass and `INVARIANT_REGISTRY` mapping prose prohibitions to runtime gates | | `_type_phoropter.py` | Phoropter family/phase types: `PhoropterPrescription`, `ReadingToken`, `READING_TOKEN_PATTERN`, `PhoropterPhaseSkip`, `CrossDomainPrescription`, `CrossDomainAssessment` | | `_type_resume.py` | `ResumeSpec` discriminated union: `NoResume | BareResume | NamedResume` | | `_type_plugin_source.py` | `PluginSource` discriminated union: `DirectInstall | MarketplaceInstall` | | `_type_tradition_manifest.py` | `TraditionManifest`, `LensEntry`, `DialingConfig` frozen dataclasses with `from_dict`/`from_yaml_path` loaders | +| `_type_context_admission.py` | Frozen content-free identities, events, records, effects, states, and coverage definitions for context admission | ## Architecture Notes diff --git a/src/autoskillit/core/types/__init__.py b/src/autoskillit/core/types/__init__.py index 94708e2ed..e97ee409a 100644 --- a/src/autoskillit/core/types/__init__.py +++ b/src/autoskillit/core/types/__init__.py @@ -22,6 +22,8 @@ from ._type_constants_features import __all__ as _constants_features_all from ._type_constants_registries import * # noqa: F401, F403 from ._type_constants_registries import __all__ as _constants_registries_all +from ._type_context_admission import * # noqa: F401, F403 +from ._type_context_admission import __all__ as _context_admission_all from ._type_dispatch_identity import * # noqa: F401, F403 from ._type_dispatch_identity import __all__ as _dispatch_identity_all from ._type_enums import * # noqa: F401, F403 @@ -84,6 +86,7 @@ + _constants_env_all + _constants_features_all + _constants_registries_all + + _context_admission_all + _dispatch_identity_all + _enums_all + _exceptions_all diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py new file mode 100644 index 000000000..588469c92 --- /dev/null +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -0,0 +1,2292 @@ +"""Pure protocol-v1 values for cumulative context admission. + +The contract is intentionally content-free and implementation-independent. It +contains immutable commands, records, decisions, and declarative publication +effects; persistence and producer integration belong to downstream layers. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field, fields +from enum import StrEnum +from typing import Any, ClassVar, TypeAlias, get_type_hints + +from ._type_dispatch_identity import DispatchIdentity +from ._type_enums import ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageEvidenceKind, + CoverageState, + GenerationState, + MeasurementKind, + ProducerSurface, + ReserveClass, + WitnessKind, +) +from ._type_helpers import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + ContextAdmissionValidationError, + UnsupportedContextAdmissionProtocolError, + _matches_declared_type, + _raise_invalid, + _validate_bounded_text, + _validate_canonical_tuple, + _validate_context_admission_state_metadata, + _validate_expired_idempotency_tombstone, + _validate_freshness_policy, + _validate_git_revision, + _validate_iso_date, + _validate_non_negative, + _validate_protocol_version, + _validate_reason_code, +) +from ._type_results import ModelIdentity + +_TYPE_REGISTRY: dict[str, type[_ContractValue]] = {} +_MAX_CLOSED_EPOCH_OCCURRENCES = 10_000 +_ENUM_REGISTRY: dict[str, type[StrEnum]] = { + enum_type.__name__: enum_type + for enum_type in ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageEvidenceKind, + CoverageState, + GenerationState, + MeasurementKind, + ProducerSurface, + ReserveClass, + WitnessKind, + ) +} + + +def _encode(value: object) -> object: + if isinstance(value, DispatchIdentity): + try: + validated = DispatchIdentity( + dispatch_id=value.dispatch_id, + completion_marker=value.completion_marker, + sentinel_open=value.sentinel_open, + sentinel_close=value.sentinel_close, + sentinel_contract=value.sentinel_contract, + ) + except ValueError: + _raise_invalid("invalid_dispatch_identity") + return {"dispatch_id": validated.dispatch_id} + if isinstance(value, ModelIdentity): + return { + "__type__": "ModelIdentity", + "configured_model": value.configured_model, + "effective_model": value.effective_model, + "profile_name": value.profile_name, + } + if isinstance(value, StrEnum): + return {"__enum__": type(value).__name__, "value": value.value} + if isinstance(value, tuple): + return {"__tuple__": [_encode(item) for item in value]} + if isinstance(value, frozenset): + encoded = [_encode(item) for item in value] + encoded.sort(key=repr) + return {"__frozenset__": encoded} + if isinstance(value, _ContractValue): + result: dict[str, object] = {"__type__": type(value).__name__} + for field_name in value.__dataclass_fields__: + result[field_name] = _encode(getattr(value, field_name)) + return result + if value is None or isinstance(value, bool | int | str): + return value + _raise_invalid("unsupported_serialization_value") + + +def _decode(value: object) -> object: + if isinstance(value, list): + return tuple(_decode(item) for item in value) + if not isinstance(value, Mapping): + return value + if set(value) == {"dispatch_id"}: + dispatch_id = value["dispatch_id"] + if not isinstance(dispatch_id, str): + _raise_invalid("invalid_dispatch_identity") + return DispatchIdentity.from_dispatch_id(dispatch_id) + if "__enum__" in value: + if set(value) != {"__enum__", "value"}: + _raise_invalid("unknown_serialized_enum") + enum_name = value.get("__enum__") + enum_value = value.get("value") + if ( + not isinstance(enum_name, str) + or enum_name not in _ENUM_REGISTRY + or not isinstance(enum_value, str) + ): + _raise_invalid("unknown_serialized_enum") + try: + return _ENUM_REGISTRY[enum_name](enum_value) + except (TypeError, ValueError): + raise ContextAdmissionValidationError("invalid_serialized_enum") from None + if "__tuple__" in value: + if set(value) != {"__tuple__"}: + _raise_invalid("invalid_serialized_tuple") + raw = value["__tuple__"] + if not isinstance(raw, list): + _raise_invalid("invalid_serialized_tuple") + return tuple(_decode(item) for item in raw) + if "__frozenset__" in value: + if set(value) != {"__frozenset__"}: + _raise_invalid("invalid_serialized_frozenset") + raw = value["__frozenset__"] + if not isinstance(raw, list): + _raise_invalid("invalid_serialized_frozenset") + return frozenset(_decode(item) for item in raw) + type_name = value.get("__type__") + if type_name == "ModelIdentity": + if set(value) != { + "__type__", + "configured_model", + "effective_model", + "profile_name", + }: + _raise_invalid("invalid_model_identity") + configured_model = value["configured_model"] + effective_model = value["effective_model"] + profile_name = value["profile_name"] + if not all( + isinstance(item, str) for item in (configured_model, effective_model, profile_name) + ): + _raise_invalid("invalid_model_identity") + return ModelIdentity( + configured_model=configured_model, + effective_model=effective_model, + profile_name=profile_name, + ) + if not isinstance(type_name, str) or type_name not in _TYPE_REGISTRY: + _raise_invalid("unknown_serialized_contract_type") + contract_type = _TYPE_REGISTRY[type_name] + kwargs = {key: _decode(item) for key, item in value.items() if key != "__type__"} + try: + return contract_type(**kwargs) + except TypeError: + raise ContextAdmissionValidationError("invalid_serialized_contract") from None + + +class _ContractMeta(type): + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + try: + instance = super().__call__(*args, **kwargs) + except ContextAdmissionValidationError: + raise + except (AttributeError, TypeError, ValueError): + raise ContextAdmissionValidationError("invalid_contract_field_type") from None + _validate_declared_field_types(instance) + _validate_deep_immutability(instance) + return instance + + +class _ContractValue(metaclass=_ContractMeta): + """Canonical content-free serialization shared by all protocol values.""" + + _registry: ClassVar[dict[str, type[_ContractValue]]] = _TYPE_REGISTRY + __dataclass_fields__: ClassVar[dict[str, Any]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + _TYPE_REGISTRY[cls.__name__] = cls + + def to_dict(self) -> dict[str, object]: + encoded = _encode(self) + if not isinstance(encoded, dict): + _raise_invalid("invalid_contract_serialization") + encoded.pop("__type__", None) + return encoded + + @classmethod + def from_dict(cls, data: Mapping[str, object]) -> Any: + if not isinstance(data, Mapping): + _raise_invalid("invalid_serialized_contract") + tagged = {"__type__": cls.__name__, **dict(data)} + decoded = _decode(tagged) + if not isinstance(decoded, cls): + _raise_invalid("serialized_contract_type_mismatch") + return decoded + + +def _validate_deep_immutability(value: object) -> None: + if isinstance(value, list | dict | set): + _raise_invalid("mutable_contract_collection") + if isinstance(value, tuple | frozenset): + for item in value: + _validate_deep_immutability(item) + elif isinstance(value, _ContractValue): + for field_name in value.__dataclass_fields__: + _validate_deep_immutability(getattr(value, field_name)) + + +def _validate_declared_field_types(value: _ContractValue) -> None: + declared_types = get_type_hints(type(value)) + for declared_field in fields(value): + declared_type = declared_types.get(declared_field.name) + if declared_type is None or not _matches_declared_type( + getattr(value, declared_field.name), + declared_type, + ): + _raise_invalid("invalid_contract_field_type") + + +@dataclass(frozen=True, slots=True) +class _OpaqueString(_ContractValue): + value: str + + def __post_init__(self) -> None: + _validate_bounded_text( + self.value, + "invalid_opaque_identifier", + maximum=96, + ) + allowed = "-_.:" + if ( + len(self.value) in {40, 64, 128} + and all(character in "0123456789abcdefABCDEF" for character in self.value) + ) or ( + self.value.startswith("-") + or self.value.endswith("-") + or any( + not (character.isascii() and (character.isalnum() or character in allowed)) + for character in self.value + ) + ): + _raise_invalid("invalid_opaque_identifier") + + +@dataclass(frozen=True, slots=True) +class _NonNegativeInteger(_ContractValue): + value: int + + def __post_init__(self) -> None: + _validate_non_negative(self.value, "invalid_non_negative_integer") + + +@dataclass(frozen=True, slots=True) +class ContextSessionId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AgentInstanceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ContextThreadId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ForkOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class TurnId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ProducerInstanceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ToolCallId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ModelItemId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionRequestId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionBatchId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class WindowEpochId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class TokenizerIdentity(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class CanonicalSpanId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionAttemptId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class DeliveryOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionEventId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionReservationId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionWitnessId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AuthoritySourceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class GenerationReservationId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ProtectedPoolOwnerId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class RepresentationRevision(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class RepresentationBindingId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AggregateRevision(_NonNegativeInteger): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionSequence(_NonNegativeInteger): + pass + + +@dataclass(frozen=True, slots=True) +class IdempotencyNamespace(_ContractValue): + caller_scope: str + operation_kind: str + + def __post_init__(self) -> None: + _validate_bounded_text(self.caller_scope, "invalid_idempotency_namespace") + _validate_bounded_text(self.operation_kind, "invalid_idempotency_operation") + + +@dataclass(frozen=True, slots=True) +class ContextLineage(_ContractValue): + root_session_id: ContextSessionId + current_session_id: ContextSessionId + root_agent_id: AgentInstanceId + current_agent_id: AgentInstanceId + parent_agent_id: AgentInstanceId | None + root_thread_id: ContextThreadId + current_thread_id: ContextThreadId + parent_thread_id: ContextThreadId | None + fork_occurrence_id: ForkOccurrenceId | None + turn_id: TurnId + producer_surface: ProducerSurface + producer_instance_id: ProducerInstanceId + tool_call_id: ToolCallId | None + model_item_id: ModelItemId | None + dispatch_identity: DispatchIdentity | None = field(repr=False) + attempt_id: AdmissionAttemptId + delivery_occurrence_id: DeliveryOccurrenceId | None + window_epoch_id: WindowEpochId + window_epoch_number: int + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + if self.dispatch_identity is not None: + expected = DispatchIdentity.from_dispatch_id(self.dispatch_identity.dispatch_id) + if self.dispatch_identity != expected: + _raise_invalid("invalid_dispatch_identity") + if self.producer_surface in _NON_DISPATCH_PRODUCER_SURFACES: + _raise_invalid("dispatch_identity_on_non_dispatch_surface") + is_parent_delivery = self.producer_surface is ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + if is_parent_delivery != (self.delivery_occurrence_id is not None): + _raise_invalid("invalid_parent_delivery_lineage") + if is_parent_delivery and ( + self.parent_agent_id is None + or self.parent_thread_id is None + or self.fork_occurrence_id is None + ): + _raise_invalid("incomplete_parent_delivery_lineage") + + +@dataclass(frozen=True, slots=True) +class ContextWindowSnapshot(_ContractValue): + protocol_version: int + window_epoch_id: WindowEpochId + window_epoch_number: int + model_identity: ModelIdentity + tokenizer_identity: TokenizerIdentity + snapshot_sequence: int + active_count: int + hard_limit: int + remaining_count: int + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + for value, reason in ( + (self.window_epoch_number, "invalid_window_epoch_number"), + (self.snapshot_sequence, "invalid_snapshot_sequence"), + (self.active_count, "invalid_active_count"), + (self.hard_limit, "invalid_hard_limit"), + (self.remaining_count, "invalid_remaining_count"), + ): + _validate_non_negative(value, reason) + if ( + not self.model_identity.configured_model + or not self.model_identity.effective_model + or self.active_count + self.remaining_count > self.hard_limit + ): + _raise_invalid("invalid_authoritative_snapshot") + for model_value in ( + self.model_identity.configured_model, + self.model_identity.effective_model, + ): + _validate_bounded_text( + model_value, + "invalid_model_identity", + maximum=128, + ) + if self.model_identity.profile_name: + _validate_bounded_text( + self.model_identity.profile_name, + "invalid_model_identity", + maximum=64, + ) + + +@dataclass(frozen=True, slots=True) +class CanonicalSpanOwner(_ContractValue): + span_id: CanonicalSpanId + occurrence_id: AdmissionOccurrenceId + + +@dataclass(frozen=True, slots=True) +class CanonicalRepresentationManifest(_ContractValue): + request_id: AdmissionRequestId + representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId + span_owners: tuple[CanonicalSpanOwner, ...] + assembler_identity: ProducerInstanceId + assembler_witness_id: AdmissionWitnessId + + def __post_init__(self) -> None: + _validate_canonical_tuple( + self.span_owners, + "noncanonical_span_owners", + key=lambda owner: (owner.span_id.value, owner.occurrence_id.value), + ) + if not self.span_owners: + _raise_invalid("incomplete_representation_manifest") + span_ids = tuple(owner.span_id for owner in self.span_owners) + if len(span_ids) != len(set(span_ids)): + _raise_invalid("overlapping_canonical_span_ownership") + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrence(_ContractValue): + occurrence_id: AdmissionOccurrenceId + lineage: ContextLineage + reserve_class: ReserveClass + producer_surface: ProducerSurface + predicted_authoritative_maximum: int + representation_revision: RepresentationRevision + owned_span_ids: tuple[CanonicalSpanId, ...] + + def __post_init__(self) -> None: + _validate_non_negative( + self.predicted_authoritative_maximum, + "invalid_predicted_authoritative_maximum", + ) + _validate_canonical_tuple( + self.owned_span_ids, + "noncanonical_owned_span_ids", + key=lambda span_id: span_id.value, + ) + if not self.owned_span_ids or len(self.owned_span_ids) != len(set(self.owned_span_ids)): + _raise_invalid("invalid_owned_span_ids") + if self.producer_surface is not self.lineage.producer_surface: + _raise_invalid("producer_surface_mismatch") + + +@dataclass(frozen=True, slots=True) +class AdmissionBatch(_ContractValue): + batch_id: AdmissionBatchId + request_id: AdmissionRequestId + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + manifest: CanonicalRepresentationManifest + + def __post_init__(self) -> None: + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_batch_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) + if not self.occurrence_ids or len(self.occurrence_ids) != len(set(self.occurrence_ids)): + _raise_invalid("invalid_batch_occurrences") + if self.manifest.request_id != self.request_id: + _raise_invalid("batch_manifest_request_mismatch") + manifest_occurrences = tuple(owner.occurrence_id for owner in self.manifest.span_owners) + if set(manifest_occurrences) != set(self.occurrence_ids): + _raise_invalid("incomplete_representation_manifest") + if (self.reserve_class is ReserveClass.ORDINARY) != (self.protected_pool_owner_id is None): + _raise_invalid("invalid_protected_pool_owner") + + +@dataclass(frozen=True, slots=True) +class AdmissionReservationKey(_ContractValue): + idempotency_namespace: IdempotencyNamespace + protocol_version: int + window_epoch_id: WindowEpochId + window_epoch_number: int + batch_id: AdmissionBatchId + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + occurrence_revisions: tuple[tuple[AdmissionOccurrenceId, RepresentationRevision], ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_canonical_tuple( + self.occurrence_revisions, + "noncanonical_occurrence_revisions", + key=lambda pair: (pair[0].value, pair[1].value), + ) + occurrence_ids = tuple(pair[0] for pair in self.occurrence_revisions) + if ( + not occurrence_ids + or len(occurrence_ids) != len(set(occurrence_ids)) + or (self.reserve_class is ReserveClass.ORDINARY) + != (self.protected_pool_owner_id is None) + ): + _raise_invalid("invalid_reservation_key") + + +@dataclass(frozen=True, slots=True) +class AdmissionReservation(_ContractValue): + reservation_id: AdmissionReservationId + key: AdmissionReservationKey + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + reserved_count: int + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_non_negative(self.reserved_count, "invalid_reserved_count") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_reservation_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) + if ( + self.window_epoch_id != self.key.window_epoch_id + or self.window_epoch_number != self.key.window_epoch_number + or self.reserve_class is not self.key.reserve_class + or self.protected_pool_owner_id != self.key.protected_pool_owner_id + or self.occurrence_ids + != tuple(occurrence_id for occurrence_id, _ in self.key.occurrence_revisions) + ): + _raise_invalid("reservation_key_mismatch") + + +@dataclass(frozen=True, slots=True) +class AdmissionWitness(_ContractValue): + witness_id: AdmissionWitnessId + kind: WitnessKind + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + request_id: AdmissionRequestId + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + authority_source_id: AuthoritySourceId + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_witness_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) + if len(self.occurrence_ids) != len(set(self.occurrence_ids)) or ( + not self.occurrence_ids and self.kind is not WitnessKind.EPOCH_ROLLOVER + ): + _raise_invalid("invalid_witness_occurrences") + + +@dataclass(frozen=True, slots=True) +class RepresentationBindingWitness(_ContractValue): + counted_representation_revision: RepresentationRevision + dispatched_representation_revision: RepresentationRevision + final_manifest_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId + request_id: AdmissionRequestId + batch_id: AdmissionBatchId + authority_source_id: AuthoritySourceId + + +@dataclass(frozen=True, slots=True) +class EpochFenceProof(_ContractValue): + old_window_epoch_id: WindowEpochId + old_window_epoch_number: int + new_window_epoch_id: WindowEpochId + new_window_epoch_number: int + receiver_authority_source_id: AuthoritySourceId + fence_witness_id: AdmissionWitnessId + highest_admitted_dispatch_sequence: int + + def __post_init__(self) -> None: + _validate_non_negative(self.old_window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.new_window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative( + self.highest_admitted_dispatch_sequence, + "invalid_dispatch_sequence", + ) + + +@dataclass(frozen=True, slots=True) +class ProtectedPoolSpec(_ContractValue): + reserve_class: ReserveClass + capability_owner_id: ProtectedPoolOwnerId + injected_count: int + priority: int + required_release_witness_kind: WitnessKind + + def __post_init__(self) -> None: + _validate_non_negative(self.injected_count, "invalid_protected_pool_count") + _validate_non_negative(self.priority, "invalid_protected_pool_priority") + if self.reserve_class is ReserveClass.ORDINARY: + _raise_invalid("ordinary_pool_forbidden") + if self.required_release_witness_kind not in { + WitnessKind.NON_ADMISSION, + WitnessKind.ROLLBACK, + }: + _raise_invalid("invalid_protected_release_witness_kind") + + +@dataclass(frozen=True, slots=True) +class AdmissionDecision(_ContractValue): + kind: AdmissionDecisionKind + reason_code: str + window_epoch_id: WindowEpochId | None + snapshot_sequence: int | None + requested_count: int + available_ordinary_count: int + available_protected_count: int + + def __post_init__(self) -> None: + _validate_reason_code(self.reason_code) + for value in ( + self.requested_count, + self.available_ordinary_count, + self.available_protected_count, + ): + _validate_non_negative(value, "invalid_decision_count") + if self.snapshot_sequence is not None: + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrenceRecord(_ContractValue): + occurrence: AdmissionOccurrence + state: AdmissionState + batch_id: AdmissionBatchId | None + reservation_id: AdmissionReservationId | None + accepted_witness_ids: tuple[AdmissionWitnessId, ...] + indeterminate_reason_code: str | None + quarantine_reason_code: str | None + + def __post_init__(self) -> None: + _validate_canonical_tuple( + self.accepted_witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(self.accepted_witness_ids) != len(set(self.accepted_witness_ids)): + _raise_invalid("duplicate_witness_id") + if self.indeterminate_reason_code is not None: + _validate_reason_code( + self.indeterminate_reason_code, + "invalid_indeterminate_reason", + ) + if self.quarantine_reason_code is not None: + _validate_reason_code( + self.quarantine_reason_code, + "invalid_quarantine_reason", + ) + + +@dataclass(frozen=True, slots=True) +class AdmissionBatchRecord(_ContractValue): + batch: AdmissionBatch + state: AdmissionState + reservation_id: AdmissionReservationId | None + witness_ids: tuple[AdmissionWitnessId, ...] + committed_input_count: int + unresolved_input_count: int + + def charged_input_count(self, reservation: AdmissionReservation | None) -> int: + """Return the capacity charge represented by this lifecycle record.""" + if self.state in {AdmissionState.COMMITTED, AdmissionState.QUARANTINED}: + return self.committed_input_count + if self.state is AdmissionState.INDETERMINATE: + return self.unresolved_input_count or ( + reservation.reserved_count if reservation is not None else 0 + ) + if self.state in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + return reservation.reserved_count if reservation is not None else 0 + return 0 + + def __post_init__(self) -> None: + _validate_canonical_tuple( + self.witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(self.witness_ids) != len(set(self.witness_ids)): + _raise_invalid("duplicate_witness_id") + _validate_non_negative(self.committed_input_count, "invalid_committed_count") + _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") + if self.committed_input_count > 0 and self.unresolved_input_count > 0: + _raise_invalid("committed_and_unresolved_simultaneously") + if self.committed_input_count > 0 and self.state not in { + AdmissionState.COMMITTED, + AdmissionState.QUARANTINED, + }: + _raise_invalid("committed_count_for_nonterminal_batch") + if self.unresolved_input_count > 0 and self.state is not AdmissionState.INDETERMINATE: + _raise_invalid("unresolved_count_for_resolved_batch") + + +@dataclass(frozen=True, slots=True) +class GenerationReservationRecord(_ContractValue): + generation_reservation_id: GenerationReservationId + request_id: AdmissionRequestId + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + response_id: ModelItemId + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + maximum_allowance: int + state: GenerationState + exact_terminal_usage: int | None + witness_ids: tuple[AdmissionWitnessId, ...] + authority_source_id: AuthoritySourceId | None + + def charged_output_count(self) -> int: + """Return the generation capacity charge for this lifecycle record.""" + if self.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + GenerationState.QUARANTINED, + }: + return self.maximum_allowance + return 0 + + def __post_init__(self) -> None: + for value in ( + self.window_epoch_number, + self.snapshot_sequence, + self.maximum_allowance, + ): + _validate_non_negative(value, "invalid_generation_reservation") + if self.exact_terminal_usage is not None: + _validate_non_negative(self.exact_terminal_usage, "invalid_generation_usage") + if (self.reserve_class is ReserveClass.ORDINARY) != (self.protected_pool_owner_id is None): + _raise_invalid("invalid_generation_owner") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_generation_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) + if not self.occurrence_ids or len(self.occurrence_ids) != len(set(self.occurrence_ids)): + _raise_invalid("invalid_generation_occurrences") + _validate_canonical_tuple( + self.witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(self.witness_ids) != len(set(self.witness_ids)): + _raise_invalid("duplicate_witness_id") + + +@dataclass(frozen=True, slots=True) +class ExpiredIdempotencyTombstone(_ContractValue): + namespace: IdempotencyNamespace + reservation_key: AdmissionReservationKey + original_descriptor: ReserveRequestEvent + expiry_witness: AdmissionWitness + original_terminal_decision: AdmissionDecision + + def __post_init__(self) -> None: + _validate_expired_idempotency_tombstone(self) + + +@dataclass(frozen=True, slots=True) +class ClosedEpochAudit(_ContractValue): + snapshot: ContextWindowSnapshot + terminal_occurrence_records: tuple[AdmissionOccurrenceRecord, ...] + terminal_batch_records: tuple[AdmissionBatchRecord, ...] + terminal_reservations: tuple[AdmissionReservation, ...] + terminal_generation_reservations: tuple[GenerationReservationRecord, ...] + closure_witness_id: AdmissionWitnessId + fence_proof: EpochFenceProof | None + processed_event_tombstones: tuple[AdmissionEventId, ...] + retained_unresolved_count: int + retained_generation_count: int + + def reservation_for(self, record: AdmissionBatchRecord) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in self.terminal_reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + def retained_input_count(self, records: tuple[AdmissionBatchRecord, ...]) -> int: + return sum( + record.charged_input_count(self.reservation_for(record)) + for record in records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + + def __post_init__(self) -> None: + _validate_non_negative(self.retained_unresolved_count, "invalid_retained_charge") + _validate_non_negative( + self.retained_generation_count, + "invalid_retained_generation_charge", + ) + if len(self.terminal_occurrence_records) > _MAX_CLOSED_EPOCH_OCCURRENCES: + _raise_invalid("closed_epoch_occurrence_limit_exceeded") + _validate_canonical_tuple( + self.terminal_occurrence_records, + "noncanonical_terminal_occurrences", + key=lambda record: record.occurrence.occurrence_id.value, + ) + _validate_canonical_tuple( + self.terminal_batch_records, + "noncanonical_terminal_batches", + key=lambda record: record.batch.batch_id.value, + ) + _validate_canonical_tuple( + self.terminal_reservations, + "noncanonical_terminal_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + _validate_canonical_tuple( + self.terminal_generation_reservations, + "noncanonical_terminal_generation_reservations", + key=lambda record: record.generation_reservation_id.value, + ) + _validate_canonical_tuple( + self.processed_event_tombstones, + "noncanonical_processed_event_tombstones", + key=lambda event_id: event_id.value, + ) + occurrence_by_id = { + record.occurrence.occurrence_id: record for record in self.terminal_occurrence_records + } + batch_by_id = {record.batch.batch_id: record for record in self.terminal_batch_records} + reservation_by_id = { + reservation.reservation_id: reservation for reservation in self.terminal_reservations + } + if ( + len(occurrence_by_id) != len(self.terminal_occurrence_records) + or len(batch_by_id) != len(self.terminal_batch_records) + or len(reservation_by_id) != len(self.terminal_reservations) + or len( + { + record.generation_reservation_id + for record in self.terminal_generation_reservations + } + ) + != len(self.terminal_generation_reservations) + or len(set(self.processed_event_tombstones)) != len(self.processed_event_tombstones) + ): + _raise_invalid("duplicate_closed_epoch_owner") + for batch_record in self.terminal_batch_records: + members = tuple( + occurrence_by_id.get(occurrence_id) + for occurrence_id in batch_record.batch.occurrence_ids + ) + if any(member is None for member in members): + _raise_invalid("missing_closed_epoch_occurrence") + if any( + member is not None + and ( + member.batch_id != batch_record.batch.batch_id + or member.reservation_id != batch_record.reservation_id + or member.state is not batch_record.state + ) + for member in members + ): + _raise_invalid("inconsistent_closed_epoch_link") + if batch_record.reservation_id is not None: + reservation = reservation_by_id.get(batch_record.reservation_id) + if reservation is None or reservation.key.batch_id != batch_record.batch.batch_id: + _raise_invalid("missing-closed-epoch-reservation") + for generation in self.terminal_generation_reservations: + generation_batch = batch_by_id.get(generation.batch_id) + if ( + generation_batch is None + or generation.request_id != generation_batch.batch.request_id + or generation.representation_revision + != generation_batch.batch.manifest.representation_revision + or generation.occurrence_ids != generation_batch.batch.occurrence_ids + ): + _raise_invalid("inconsistent_closed_epoch_generation") + retained_input = self.retained_input_count(self.terminal_batch_records) + retained_generation = sum( + record.maximum_allowance + for record in self.terminal_generation_reservations + if record.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + if ( + retained_input != self.retained_unresolved_count + or retained_generation != self.retained_generation_count + ): + _raise_invalid("closed_epoch_retained_charge_mismatch") + + +@dataclass(frozen=True, slots=True) +class CoverageEvidence(_ContractValue): + claim_id: str + kind: CoverageEvidenceKind + backend: str + configuration_mode: str + verifier: str + source_locator: str + tested_version: str + tested_revision: str + checked_at: str + freshness_policy: str + + def __post_init__(self) -> None: + for value, reason, maximum in ( + (self.claim_id, "invalid_claim_id", 96), + (self.backend, "invalid_evidence_backend", 64), + (self.configuration_mode, "invalid_configuration_mode", 64), + (self.verifier, "invalid_evidence_verifier", 64), + (self.tested_version, "invalid_tested_version", 64), + ): + _validate_bounded_text(value, reason, maximum=maximum) + _validate_git_revision(self.tested_revision) + _validate_iso_date(self.checked_at) + _validate_freshness_policy(self.freshness_policy) + _validate_bounded_text( + self.source_locator, + "invalid_source_locator", + maximum=256, + locator=True, + ) + + +@dataclass(frozen=True, slots=True) +class ProducerCoverageDef(_ContractValue): + surface: ProducerSurface + control_point_owner: str + observation_state: CoverageState + authority_state: CoverageState + evidence: tuple[CoverageEvidence, ...] + reason_code: str + + def __post_init__(self) -> None: + _validate_bounded_text( + self.control_point_owner, + "invalid_control_point_owner", + maximum=96, + ) + _validate_reason_code(self.reason_code) + _validate_canonical_tuple( + self.evidence, + "noncanonical_coverage_evidence", + key=lambda evidence: ( + evidence.kind.value, + evidence.source_locator, + evidence.claim_id, + ), + ) + if not self.evidence: + _raise_invalid("coverage_evidence_required") + if len(self.evidence) != 1: + _raise_invalid("single_coverage_evidence_required") + primary = tuple( + item for item in self.evidence if item.kind is not CoverageEvidenceKind.INFERENCE + ) + if ( + self.observation_state is CoverageState.VERIFIED + or self.authority_state is CoverageState.VERIFIED + ) and not primary: + _raise_invalid("verified_coverage_requires_primary_evidence") + + +@dataclass(frozen=True, slots=True) +class _AdmissionEventBase(_ContractValue): + event_id: AdmissionEventId + protocol_version: int + idempotency_namespace: IdempotencyNamespace + expected_aggregate_revision: AggregateRevision + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + + +@dataclass(frozen=True, slots=True) +class OpenEpochEvent(_AdmissionEventBase): + snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) + + +@dataclass(frozen=True, slots=True) +class AuthorityUnavailableEvent(_AdmissionEventBase): + reason_code: str + authority_state: CoverageState + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_reason_code(self.reason_code) + if self.authority_state is CoverageState.VERIFIED: + _raise_invalid("invalid_unavailable_authority_state") + + +@dataclass(frozen=True, slots=True) +class ProposeOccurrenceEvent(_AdmissionEventBase): + occurrence: AdmissionOccurrence + + +@dataclass(frozen=True, slots=True) +class ReserveRequestEvent(_AdmissionEventBase): + batch: AdmissionBatch + snapshot_sequence: int + input_reservations: tuple[AdmissionReservation, ...] + generation_reservation: GenerationReservationRecord | None + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_canonical_tuple( + self.input_reservations, + "noncanonical_input_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + if not self.input_reservations: + _raise_invalid("input_reservation_required") + reservation_ids = tuple( + reservation.reservation_id for reservation in self.input_reservations + ) + if len(reservation_ids) != len(set(reservation_ids)): + _raise_invalid("duplicate_reservation_id") + for reservation in self.input_reservations: + if reservation.occurrence_ids != self.batch.occurrence_ids: + _raise_invalid("reservation_occurrence_mismatch") + if self.idempotency_namespace != reservation.key.idempotency_namespace: + _raise_invalid("reservation_namespace_mismatch") + if ( + reservation.key.batch_id != self.batch.batch_id + or reservation.reserve_class is not self.batch.reserve_class + or reservation.protected_pool_owner_id != self.batch.protected_pool_owner_id + ): + _raise_invalid("reservation_batch_policy_mismatch") + generation = self.generation_reservation + if generation is not None and ( + generation.state is not GenerationState.RESERVED + or generation.exact_terminal_usage is not None + or generation.witness_ids + or generation.authority_source_id is not None + ): + _raise_invalid("generation_reservation_not_open") + if generation is not None and ( + generation.request_id != self.batch.request_id + or generation.batch_id != self.batch.batch_id + or generation.representation_revision != self.batch.manifest.representation_revision + or generation.occurrence_ids != self.batch.occurrence_ids + or generation.reserve_class is not self.batch.reserve_class + or generation.protected_pool_owner_id != self.batch.protected_pool_owner_id + ): + _raise_invalid("generation_batch_policy_mismatch") + + +@dataclass(frozen=True, slots=True) +class PrepareBatchEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId + proposed_charge: int + measurement_kind: MeasurementKind + authority_source: AuthoritySourceId + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.proposed_charge, "invalid_proposed_charge") + if self.measurement_kind in { + MeasurementKind.HOST_ESTIMATE, + MeasurementKind.BYTE_EMERGENCY, + }: + _raise_invalid("non-authoritative-measurement") + + +@dataclass(frozen=True, slots=True) +class StageHistoryEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class DispatchRequestEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class AcceptInputEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + final_manifest_revision: RepresentationRevision + final_manifest: CanonicalRepresentationManifest + exact_input_charge: int + measurement_kind: MeasurementKind + authority_source: AuthoritySourceId + representation_binding_witness: RepresentationBindingWitness + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_input_charge, "invalid_exact_input_charge") + if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: + _raise_invalid("non-authoritative-measurement") + + +@dataclass(frozen=True, slots=True) +class ReleaseNonAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class RollbackAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class MarkIndeterminateEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + final_manifest_revision: RepresentationRevision + final_manifest: CanonicalRepresentationManifest + exact_charge: int + measurement_kind: MeasurementKind + authority_source: AuthoritySourceId + representation_binding_witness: RepresentationBindingWitness + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_charge, "invalid-exact-charge") + if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: + _raise_invalid("non-authoritative-measurement") + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateNonAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateRollbackEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class StartGenerationEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class ReconcileGenerationEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + output_usage_witness: AdmissionWitness + exact_output_usage: int + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_output_usage, "invalid_exact_output_usage") + + +@dataclass(frozen=True, slots=True) +class MarkGenerationIndeterminateEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class RequestReconciliationEvent(_AdmissionEventBase): + target_id: AdmissionBatchId | GenerationReservationId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class ExpireIdempotencyKeyEvent(_AdmissionEventBase): + reservation_key: AdmissionReservationKey + expiry_witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class RolloverEpochEvent(_AdmissionEventBase): + witness: AdmissionWitness + fence_proof: EpochFenceProof | None + new_snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) + + +ContextAdmissionEvent: TypeAlias = ( + OpenEpochEvent + | AuthorityUnavailableEvent + | ProposeOccurrenceEvent + | ReserveRequestEvent + | PrepareBatchEvent + | StageHistoryEvent + | DispatchRequestEvent + | AcceptInputEvent + | ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | MarkIndeterminateEvent + | ResolveIndeterminateAcceptedEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + | StartGenerationEvent + | ReconcileGenerationEvent + | MarkGenerationIndeterminateEvent + | RequestReconciliationEvent + | ExpireIdempotencyKeyEvent + | RolloverEpochEvent +) + + +@dataclass(frozen=True, slots=True) +class _AdmissionEffectBase(_ContractValue): + source_event_id: AdmissionEventId + resulting_aggregate_revision: AggregateRevision + resulting_admission_sequence: AdmissionSequence + target_id: ( + AdmissionOccurrenceId + | AdmissionBatchId + | AdmissionReservationId + | AdmissionEventId + | GenerationReservationId + | WindowEpochId + ) + + def __post_init__(self) -> None: + if not isinstance(self.source_event_id, AdmissionEventId): + _raise_invalid("invalid_effect_source_event") + if not isinstance(self.resulting_aggregate_revision, AggregateRevision): + _raise_invalid("invalid_effect_aggregate_revision") + if not isinstance(self.resulting_admission_sequence, AdmissionSequence): + _raise_invalid("invalid_effect_admission_sequence") + + +def _validate_charge_effect( + effect: _AdmissionEffectBase, + *, + target_type: type[_OpaqueString], + charge_domain: ChargeDomain, +) -> None: + _AdmissionEffectBase.__post_init__(effect) + if not isinstance(effect.target_id, target_type): + _raise_invalid("invalid_effect_target") + if getattr(effect, "charge_domain") is not charge_domain: + _raise_invalid("invalid_effect_charge_domain") + reserve_class = getattr(effect, "reserve_class") + owner = getattr(effect, "protected_pool_owner_id") + if (reserve_class is ReserveClass.ORDINARY) != (owner is None): + _raise_invalid("invalid_effect_protected_pool_owner") + _validate_non_negative(getattr(effect, "count"), "invalid_effect_count") + _validate_non_negative( + getattr(effect, "snapshot_sequence"), + "invalid_effect_snapshot_sequence", + ) + witness_ids = getattr(effect, "witness_ids") + _validate_canonical_tuple( + witness_ids, + "noncanonical_effect_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(witness_ids) != len(set(witness_ids)): + _raise_invalid("duplicate_effect_witness_id") + + +@dataclass(frozen=True, slots=True) +class ReservationRecordedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionReservationId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + + +@dataclass(frozen=True, slots=True) +class ReservationReleasedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionReservationId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + + +@dataclass(frozen=True, slots=True) +class OccurrenceStateChangedEffect(_AdmissionEffectBase): + target_id: AdmissionOccurrenceId + previous_state: AdmissionState + next_state: AdmissionState + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionOccurrenceId): + _raise_invalid("invalid_effect_target") + + +@dataclass(frozen=True, slots=True) +class ChargeCommittedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionBatchId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + + +@dataclass(frozen=True, slots=True) +class GenerationReservationRecordedEffect(_AdmissionEffectBase): + target_id: GenerationReservationId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=GenerationReservationId, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + ) + + +@dataclass(frozen=True, slots=True) +class GenerationReconciledEffect(_AdmissionEffectBase): + target_id: GenerationReservationId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=GenerationReservationId, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + ) + + +@dataclass(frozen=True, slots=True) +class ReconciliationQueryRequestedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class ReconciliationEscalationEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class ConflictRejectedEffect(_AdmissionEffectBase): + target_id: AdmissionEventId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionEventId): + _raise_invalid("invalid_effect_target") + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class IdempotencyExpiredEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId + reservation_key: AdmissionReservationKey + expiry_witness_id: AdmissionWitnessId + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionReservationId): + _raise_invalid("invalid_effect_target") + + +@dataclass(frozen=True, slots=True) +class ReservationInvalidatedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId | GenerationReservationId + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + if isinstance(self.target_id, AdmissionReservationId): + expected_domain = ChargeDomain.INPUT_CONTEXT + elif isinstance(self.target_id, GenerationReservationId): + expected_domain = ChargeDomain.OUTPUT_GENERATION + else: + _raise_invalid("invalid_effect_target") + _validate_charge_effect( + self, target_type=type(self.target_id), charge_domain=expected_domain + ) + + +@dataclass(frozen=True, slots=True) +class EpochClosedEffect(_AdmissionEffectBase): + target_id: WindowEpochId + fence_proof: EpochFenceProof | None + deducted_unresolved_count: int + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, WindowEpochId): + _raise_invalid("invalid_effect_target") + if self.fence_proof is not None and self.target_id != self.fence_proof.old_window_epoch_id: + _raise_invalid("invalid_effect_target") + _validate_non_negative( + self.deducted_unresolved_count, + "invalid_deducted_unresolved_count", + ) + + +@dataclass(frozen=True, slots=True) +class QuarantineRecordedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId + reason_code: str + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_reason_code(self.reason_code) + + +@dataclass(frozen=True, slots=True) +class AuthorityUnavailableEffect(_AdmissionEffectBase): + target_id: WindowEpochId + reason_code: str + authority_state: CoverageState + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, WindowEpochId): + _raise_invalid("invalid_effect_target") + _validate_reason_code(self.reason_code) + if self.authority_state is CoverageState.VERIFIED: + _raise_invalid("invalid_unavailable_authority_state") + + +AdmissionEffect: TypeAlias = ( + ReservationRecordedEffect + | ReservationReleasedEffect + | OccurrenceStateChangedEffect + | ChargeCommittedEffect + | GenerationReservationRecordedEffect + | GenerationReconciledEffect + | ReconciliationQueryRequestedEffect + | ReconciliationEscalationEffect + | ConflictRejectedEffect + | IdempotencyExpiredEffect + | ReservationInvalidatedEffect + | EpochClosedEffect + | QuarantineRecordedEffect + | AuthorityUnavailableEffect +) + + +@dataclass(frozen=True, slots=True) +class ProcessedEventRecord(_ContractValue): + event_id: AdmissionEventId + event: ContextAdmissionEvent + original_decision: AdmissionDecision + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + + def __post_init__(self) -> None: + if self.event_id != self.event.event_id: + _raise_invalid("processed_event_identity_mismatch") + + +@dataclass(frozen=True, slots=True) +class IdempotencyRecord(_ContractValue): + namespace: IdempotencyNamespace + reservation_key: AdmissionReservationKey + original_descriptor: ReserveRequestEvent + original_reserve_decision: AdmissionDecision + owning_event_id: AdmissionEventId + publication_revision: AggregateRevision + + def __post_init__(self) -> None: + input_reservations = self.original_descriptor.input_reservations + if ( + self.namespace != self.original_descriptor.idempotency_namespace + or len(input_reservations) != 1 + or self.reservation_key != input_reservations[0].key + or self.owning_event_id != self.original_descriptor.event_id + ): + _raise_invalid("idempotency_record_identity_mismatch") + + +@dataclass(frozen=True, slots=True) +class UninitializedContextAdmissionState(_ContractValue): + protocol_version: int + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + processed_events: tuple[ProcessedEventRecord, ...] + idempotency_records: tuple[IdempotencyRecord, ...] + expired_idempotency_tombstones: tuple[ExpiredIdempotencyTombstone, ...] + closed_epochs: tuple[ClosedEpochAudit, ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + _validate_canonical_tuple( + self.processed_events, + "noncanonical_processed_events", + key=lambda record: (record.aggregate_revision.value, record.event_id.value), + ) + _validate_canonical_tuple( + self.idempotency_records, + "noncanonical_idempotency_records", + key=lambda record: ( + record.publication_revision.value, + record.owning_event_id.value, + ), + ) + _validate_canonical_tuple( + self.expired_idempotency_tombstones, + "noncanonical_idempotency_tombstones", + key=lambda tombstone: ( + tombstone.reservation_key.window_epoch_number, + tombstone.reservation_key.batch_id.value, + ), + ) + _validate_canonical_tuple( + self.closed_epochs, + "noncanonical_closed_epochs", + key=lambda audit: audit.snapshot.window_epoch_number, + ) + _validate_context_admission_state_metadata( + self.aggregate_revision, + self.admission_sequence, + self.processed_events, + self.idempotency_records, + self.expired_idempotency_tombstones, + self.closed_epochs, + ) + + +@dataclass(frozen=True, slots=True) +class ActiveContextAdmissionState(_ContractValue): + protocol_version: int + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + occurrence_records: tuple[AdmissionOccurrenceRecord, ...] + batch_records: tuple[AdmissionBatchRecord, ...] + reservations: tuple[AdmissionReservation, ...] + generation_reservations: tuple[GenerationReservationRecord, ...] + processed_events: tuple[ProcessedEventRecord, ...] + idempotency_records: tuple[IdempotencyRecord, ...] + expired_idempotency_tombstones: tuple[ExpiredIdempotencyTombstone, ...] + closed_epochs: tuple[ClosedEpochAudit, ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + if self.snapshot.protocol_version != self.protocol_version: + _raise_invalid("state_snapshot_protocol_mismatch") + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) + _validate_canonical_tuple( + self.occurrence_records, + "noncanonical_occurrence_records", + key=lambda record: record.occurrence.occurrence_id.value, + ) + _validate_canonical_tuple( + self.batch_records, + "noncanonical_batch_records", + key=lambda record: record.batch.batch_id.value, + ) + _validate_canonical_tuple( + self.reservations, + "noncanonical_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + _validate_canonical_tuple( + self.generation_reservations, + "noncanonical_generation_reservations", + key=lambda record: record.generation_reservation_id.value, + ) + _validate_canonical_tuple( + self.processed_events, + "noncanonical_processed_events", + key=lambda record: (record.aggregate_revision.value, record.event_id.value), + ) + _validate_canonical_tuple( + self.idempotency_records, + "noncanonical_idempotency_records", + key=lambda record: ( + record.publication_revision.value, + record.owning_event_id.value, + ), + ) + _validate_canonical_tuple( + self.expired_idempotency_tombstones, + "noncanonical_idempotency_tombstones", + key=lambda tombstone: ( + tombstone.reservation_key.window_epoch_number, + tombstone.reservation_key.batch_id.value, + ), + ) + _validate_canonical_tuple( + self.closed_epochs, + "noncanonical_closed_epochs", + key=lambda audit: audit.snapshot.window_epoch_number, + ) + _validate_context_admission_state_metadata( + self.aggregate_revision, + self.admission_sequence, + self.processed_events, + self.idempotency_records, + self.expired_idempotency_tombstones, + self.closed_epochs, + ) + pools = tuple( + (pool.reserve_class, pool.capability_owner_id) for pool in self.protected_pools + ) + if len(pools) != len(set(pools)): + _raise_invalid("duplicate_protected_pool") + if sum(pool.injected_count for pool in self.protected_pools) > ( + self.snapshot.remaining_count + ): + _raise_invalid("protected_pool_capacity_exceeded") + batch_ids = tuple(record.batch.batch_id for record in self.batch_records) + if len(batch_ids) != len(set(batch_ids)): + _raise_invalid("duplicate_batch_owner") + reservation_ids = tuple(reservation.reservation_id for reservation in self.reservations) + if len(reservation_ids) != len(set(reservation_ids)): + _raise_invalid("duplicate_reservation_owner") + generation_ids = tuple( + record.generation_reservation_id for record in self.generation_reservations + ) + if len(generation_ids) != len(set(generation_ids)): + _raise_invalid("duplicate_generation_owner") + occurrence_ids = tuple( + record.occurrence.occurrence_id for record in self.occurrence_records + ) + if len(occurrence_ids) != len(set(occurrence_ids)): + _raise_invalid("duplicate_occurrence_owner") + pools_by_key = { + (pool.reserve_class, pool.capability_owner_id): pool for pool in self.protected_pools + } + reservations_by_id = { + reservation.reservation_id: reservation for reservation in self.reservations + } + batch_records_by_id = {record.batch.batch_id: record for record in self.batch_records} + occurrence_records_by_id = { + record.occurrence.occurrence_id: record for record in self.occurrence_records + } + for occurrence_record in self.occurrence_records: + if occurrence_record.batch_id is None: + if ( + occurrence_record.reservation_id is not None + or occurrence_record.state is not AdmissionState.PROPOSED + ): + _raise_invalid("orphan_occurrence_link") + continue + linked_batch = batch_records_by_id.get(occurrence_record.batch_id) + if ( + linked_batch is None + or occurrence_record.occurrence.occurrence_id + not in linked_batch.batch.occurrence_ids + or occurrence_record.reservation_id != linked_batch.reservation_id + or occurrence_record.state is not linked_batch.state + ): + _raise_invalid("inconsistent_occurrence_link") + for batch_record in self.batch_records: + member_records = tuple( + occurrence_records_by_id.get(occurrence_id) + for occurrence_id in batch_record.batch.occurrence_ids + ) + if any(record is None for record in member_records): + _raise_invalid("missing_batch_occurrence") + concrete_members = tuple(record for record in member_records if record is not None) + if any( + record.batch_id != batch_record.batch.batch_id + or record.reservation_id != batch_record.reservation_id + or record.state is not batch_record.state + for record in concrete_members + ): + _raise_invalid("inconsistent_batch_occurrence_link") + owned_pairs = tuple( + (span_id, record.occurrence.occurrence_id) + for record in concrete_members + for span_id in record.occurrence.owned_span_ids + ) + owned_span_ids = tuple(span_id for span_id, _ in owned_pairs) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) + for owner in batch_record.batch.manifest.span_owners + ) + if ( + len(owned_span_ids) != len(set(owned_span_ids)) + or set(owned_pairs) != set(manifest_pairs) + or len(owned_pairs) != len(manifest_pairs) + ): + _raise_invalid("inconsistent-span-ownership") + for reservation in self.reservations: + matching_batch = batch_records_by_id.get(reservation.key.batch_id) + if matching_batch is None: + _raise_invalid("orphan_reservation") + if ( + matching_batch.reservation_id != reservation.reservation_id + or reservation.occurrence_ids != matching_batch.batch.occurrence_ids + or reservation.reserve_class is not matching_batch.batch.reserve_class + or reservation.protected_pool_owner_id + != matching_batch.batch.protected_pool_owner_id + ): + _raise_invalid("reservation_batch_policy_mismatch") + owner = reservation.protected_pool_owner_id + if ( + owner is not None + and ( + reservation.reserve_class, + owner, + ) + not in pools_by_key + ): + _raise_invalid("orphan_protected_charge_owner") + protected_charges: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} + for record in self.batch_records: + owner = record.batch.protected_pool_owner_id + matched_reservation = ( + reservations_by_id.get(record.reservation_id) + if record.reservation_id is not None + else None + ) + if ( + record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + and matched_reservation is None + ): + _raise_invalid("missing_active_batch_reservation") + if matched_reservation is not None and ( + matched_reservation.key.batch_id != record.batch.batch_id + or matched_reservation.reserve_class is not record.batch.reserve_class + or matched_reservation.protected_pool_owner_id != owner + ): + _raise_invalid("reservation_batch_policy_mismatch") + if record.batch.reserve_class is not ReserveClass.ORDINARY: + if owner is None: + _raise_invalid("missing_protected_pool_owner") + key = (record.batch.reserve_class, owner) + if key not in pools_by_key: + _raise_invalid("orphan_protected_charge_owner") + if record.state is AdmissionState.INDETERMINATE: + charge = record.unresolved_input_count + if charge == 0 and matched_reservation is not None: + charge = matched_reservation.reserved_count + elif record.state in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + charge = ( + matched_reservation.reserved_count + if matched_reservation is not None + else 0 + ) + else: + # Committed/quarantined facts may exceed their reservation after + # an authoritative acceptance. They remain charged by _capacity, + # but are no longer an outstanding allocation against the pool. + charge = 0 + protected_charges[key] = protected_charges.get(key, 0) + charge + for generation_record in self.generation_reservations: + matching_batch = batch_records_by_id.get(generation_record.batch_id) + if ( + matching_batch is None + or generation_record.request_id != matching_batch.batch.request_id + or generation_record.representation_revision + != matching_batch.batch.manifest.representation_revision + or generation_record.occurrence_ids != matching_batch.batch.occurrence_ids + or generation_record.reserve_class is not matching_batch.batch.reserve_class + or generation_record.protected_pool_owner_id + != matching_batch.batch.protected_pool_owner_id + or generation_record.window_epoch_id != self.snapshot.window_epoch_id + or generation_record.window_epoch_number != self.snapshot.window_epoch_number + or generation_record.snapshot_sequence != self.snapshot.snapshot_sequence + ): + _raise_invalid("inconsistent_generation_link") + owner = generation_record.protected_pool_owner_id + if generation_record.reserve_class is not ReserveClass.ORDINARY: + if owner is None: + _raise_invalid("missing_protected_pool_owner") + key = (generation_record.reserve_class, owner) + if key not in pools_by_key: + _raise_invalid("orphan_protected_charge_owner") + if generation_record.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + protected_charges[key] = ( + protected_charges.get(key, 0) + generation_record.maximum_allowance + ) + if any( + charged > pools_by_key[key].injected_count + for key, charged in protected_charges.items() + ): + _raise_invalid("protected_pool_overallocated") + global_allocated = sum( + record.charged_input_count( + reservations_by_id.get(record.reservation_id) + if record.reservation_id is not None + else None + ) + for record in self.batch_records + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + sum( + generation.charged_output_count() + for generation in self.generation_reservations + if generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + if global_allocated > self.snapshot.remaining_count: + _raise_invalid("context_capacity_overallocated") + + +ContextAdmissionState: TypeAlias = UninitializedContextAdmissionState | ActiveContextAdmissionState + + +@dataclass(frozen=True, slots=True) +class AdmissionTransition(_ContractValue): + next_state: ContextAdmissionState + decision: AdmissionDecision + effects: tuple[AdmissionEffect, ...] + + +@dataclass(frozen=True, slots=True) +class AdmissionReplay(_ContractValue): + final_state: ContextAdmissionState + transitions: tuple[AdmissionTransition, ...] + + +_VERIFIED_SURFACES = frozenset( + { + ProducerSurface.NATIVE_SHELL, + ProducerSurface.AUTOSKILLIT_MCP, + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.HEADLESS_CHILD_PROMPT, + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY, + } +) +_NON_DISPATCH_PRODUCER_SURFACES = frozenset( + { + ProducerSurface.TOOL_ARGUMENT, + ProducerSurface.TOOL_RESULT_ENVELOPE, + ProducerSurface.USER_PROMPT, + ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ProducerSurface.SKILL_PLUGIN_CONTEXT, + ProducerSurface.OTHER_CONTEXT_INJECTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION, + } +) +_UNOBSERVABLE_SURFACES = frozenset( + { + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.OTHER_CONTEXT_INJECTION, + } +) +_CONTROL_POINT_OWNERS = { + ProducerSurface.NATIVE_SHELL: "shell_capture_hook", + ProducerSurface.AUTOSKILLIT_MCP: "track_response_size", + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION: "local_function_dispatch", + ProducerSurface.HOOK_FEEDBACK: "hook_registry", + ProducerSurface.HEADLESS_CHILD_PROMPT: "headless_prompt_builder", + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY: "child_delivery_receipt", + ProducerSurface.EXTERNAL_MCP: "fastmcp_client", + ProducerSurface.MCP_RESOURCE: "fastmcp_client", + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION: "compaction_receiver", +} +_LOCAL_SOURCE_LOCATORS = { + ProducerSurface.NATIVE_SHELL: "src/autoskillit/hooks/shell_capture_hook.py", + ProducerSurface.AUTOSKILLIT_MCP: "src/autoskillit/server/_notify.py", + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION: ( + "src/autoskillit/execution/headless/_headless_helpers.py" + ), + ProducerSurface.HOOK_FEEDBACK: "src/autoskillit/hook_registry.py", + ProducerSurface.HEADLESS_CHILD_PROMPT: ( + "src/autoskillit/execution/headless/_headless_helpers.py" + ), + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY: ("src/autoskillit/server/_recipe_delivery.py"), +} + + +def _coverage_row(surface: ProducerSurface) -> ProducerCoverageDef: + if surface in _VERIFIED_SURFACES: + observation_state = CoverageState.VERIFIED + evidence_kind = CoverageEvidenceKind.AUTOSKILLIT_SOURCE + backend = "autoskillit" + verifier = "source_inspection" + locator = _LOCAL_SOURCE_LOCATORS[surface] + version = "0.10.890" + revision = "ac8f653a00d24b6be50ef285958cfb0e1b7a351b" + elif surface in _UNOBSERVABLE_SURFACES: + observation_state = CoverageState.UPSTREAM_GATED + evidence_kind = CoverageEvidenceKind.INFERENCE + backend = "codex" + verifier = "source_gap_analysis" + locator = "docs/decisions/0007-context-admission.md" + version = "0.145.0" + revision = "25af12f7e61572b0bc18ddb1008be543b91519b0" + else: + observation_state = CoverageState.PARTIAL + evidence_kind = CoverageEvidenceKind.CODEX_SOURCE + backend = "codex" + verifier = "source_inspection" + locator = "codex-rs/core/src/context_manager/history.rs" + version = "0.145.0" + revision = "25af12f7e61572b0bc18ddb1008be543b91519b0" + owner = _CONTROL_POINT_OWNERS.get(surface) + if owner is None: + if surface in { + ProducerSurface.UNIFIED_EXEC_AND_WRITE_STDIN, + ProducerSurface.APPLY_PATCH, + ProducerSurface.OTHER_LOCAL_FUNCTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + }: + owner = "codex_host" + else: + owner = "final_request_assembler" + claim_id = f"COV-{surface.name.replace('_', '-')}" + evidence = CoverageEvidence( + claim_id=claim_id, + kind=evidence_kind, + backend=backend, + configuration_mode="default", + verifier=verifier, + source_locator=locator, + tested_version=version, + tested_revision=revision, + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + return ProducerCoverageDef( + surface=surface, + control_point_owner=owner, + observation_state=observation_state, + authority_state=CoverageState.UPSTREAM_GATED, + evidence=(evidence,), + reason_code="authoritative-watermark-unavailable", + ) + + +CONTEXT_ADMISSION_COVERAGE = tuple(_coverage_row(surface) for surface in ProducerSurface) + + +__all__ = [ + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "CONTEXT_ADMISSION_COVERAGE", + "ContextAdmissionValidationError", + "UnsupportedContextAdmissionProtocolError", + "ContextSessionId", + "AgentInstanceId", + "ContextThreadId", + "ForkOccurrenceId", + "TurnId", + "ProducerInstanceId", + "ToolCallId", + "ModelItemId", + "AdmissionRequestId", + "AdmissionBatchId", + "WindowEpochId", + "TokenizerIdentity", + "CanonicalSpanId", + "AdmissionOccurrenceId", + "AdmissionAttemptId", + "DeliveryOccurrenceId", + "AdmissionEventId", + "AdmissionReservationId", + "AdmissionWitnessId", + "AuthoritySourceId", + "GenerationReservationId", + "ProtectedPoolOwnerId", + "RepresentationRevision", + "RepresentationBindingId", + "AggregateRevision", + "AdmissionSequence", + "IdempotencyNamespace", + "ContextLineage", + "ContextWindowSnapshot", + "CanonicalSpanOwner", + "CanonicalRepresentationManifest", + "AdmissionOccurrence", + "AdmissionBatch", + "AdmissionReservationKey", + "AdmissionReservation", + "AdmissionWitness", + "RepresentationBindingWitness", + "EpochFenceProof", + "ProtectedPoolSpec", + "AdmissionDecision", + "AdmissionOccurrenceRecord", + "AdmissionBatchRecord", + "GenerationReservationRecord", + "ProcessedEventRecord", + "IdempotencyRecord", + "ExpiredIdempotencyTombstone", + "ClosedEpochAudit", + "CoverageEvidence", + "ProducerCoverageDef", + "OpenEpochEvent", + "AuthorityUnavailableEvent", + "ProposeOccurrenceEvent", + "ReserveRequestEvent", + "PrepareBatchEvent", + "StageHistoryEvent", + "DispatchRequestEvent", + "AcceptInputEvent", + "ReleaseNonAdmissionEvent", + "RollbackAdmissionEvent", + "MarkIndeterminateEvent", + "ResolveIndeterminateAcceptedEvent", + "ResolveIndeterminateNonAdmissionEvent", + "ResolveIndeterminateRollbackEvent", + "StartGenerationEvent", + "ReconcileGenerationEvent", + "MarkGenerationIndeterminateEvent", + "RequestReconciliationEvent", + "ExpireIdempotencyKeyEvent", + "RolloverEpochEvent", + "ContextAdmissionEvent", + "ReservationRecordedEffect", + "ReservationReleasedEffect", + "OccurrenceStateChangedEffect", + "ChargeCommittedEffect", + "GenerationReservationRecordedEffect", + "GenerationReconciledEffect", + "ReconciliationQueryRequestedEffect", + "ReconciliationEscalationEffect", + "ConflictRejectedEffect", + "IdempotencyExpiredEffect", + "ReservationInvalidatedEffect", + "EpochClosedEffect", + "QuarantineRecordedEffect", + "AuthorityUnavailableEffect", + "AdmissionEffect", + "UninitializedContextAdmissionState", + "ActiveContextAdmissionState", + "ContextAdmissionState", + "AdmissionTransition", + "AdmissionReplay", +] diff --git a/src/autoskillit/core/types/_type_dispatch_identity.py b/src/autoskillit/core/types/_type_dispatch_identity.py index edaea263c..e4b0ffb4d 100644 --- a/src/autoskillit/core/types/_type_dispatch_identity.py +++ b/src/autoskillit/core/types/_type_dispatch_identity.py @@ -5,6 +5,7 @@ from __future__ import annotations +import re from dataclasses import dataclass from uuid import uuid4 @@ -15,6 +16,15 @@ class PromptContractError(RuntimeError): """Raised when a prompt violates the sentinel contract.""" +_DISPATCH_ID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z") + + +def _validate_dispatch_id(dispatch_id: object) -> str: + if not isinstance(dispatch_id, str) or _DISPATCH_ID.fullmatch(dispatch_id) is None: + raise ValueError("invalid_dispatch_identity") + return dispatch_id + + def _build_sentinel_contract(dispatch_id: str, short: str) -> str: return f"""\ --- SECTION 8: FINAL OUTPUT CONTRACT --- @@ -51,6 +61,17 @@ class DispatchIdentity: sentinel_close: str sentinel_contract: str + def __post_init__(self) -> None: + did = _validate_dispatch_id(self.dispatch_id) + short = did[:8] + if ( + self.completion_marker != f"%%L3_DONE::{short}%%" + or self.sentinel_open != f"---l3-result::{did}---" + or self.sentinel_close != f"---end-l3-result::{did}---" + or self.sentinel_contract != _build_sentinel_contract(did, short) + ): + raise ValueError("invalid_dispatch_identity") + @classmethod def fresh(cls) -> DispatchIdentity: did = str(uuid4()) @@ -58,7 +79,7 @@ def fresh(cls) -> DispatchIdentity: @classmethod def from_dispatch_id(cls, dispatch_id: str) -> DispatchIdentity: - return cls._from_id(dispatch_id) + return cls._from_id(_validate_dispatch_id(dispatch_id)) @classmethod def _from_id(cls, did: str) -> DispatchIdentity: diff --git a/src/autoskillit/core/types/_type_enums.py b/src/autoskillit/core/types/_type_enums.py index 3f088db8e..fdd9cb248 100644 --- a/src/autoskillit/core/types/_type_enums.py +++ b/src/autoskillit/core/types/_type_enums.py @@ -41,6 +41,16 @@ "CodexEventType", "CodexItemType", "SynthesisStrategy", + "AdmissionState", + "AdmissionDecisionKind", + "ChargeDomain", + "GenerationState", + "MeasurementKind", + "CoverageState", + "CoverageEvidenceKind", + "ReserveClass", + "WitnessKind", + "ProducerSurface", ] @@ -618,3 +628,139 @@ class SynthesisStrategy(StrEnum): ELECTRE_III = "electre_iii" DEX = "dex" CUSTOM = "custom" + + +@unique +class AdmissionState(StrEnum): + """Lifecycle states for one immutable context-admission occurrence.""" + + PROPOSED = "proposed" + RESERVED = "reserved" + PREPARED = "prepared" + HISTORY_STAGED = "history_staged" + REQUEST_DISPATCHED = "request_dispatched" + COMMITTED = "committed" + RELEASED = "released" + ROLLED_BACK = "rolled_back" + INVALIDATED = "invalidated" + INDETERMINATE = "indeterminate" + QUARANTINED = "quarantined" + + +@unique +class AdmissionDecisionKind(StrEnum): + """Closed decision vocabulary returned by the protocol-v1 reducer.""" + + WOULD_ADMIT = "would_admit" + WOULD_REJECT = "would_reject" + WATERMARK_UNAVAILABLE = "watermark_unavailable" + UPSTREAM_GATED = "upstream_gated" + NOOP_IDEMPOTENT = "noop_idempotent" + CONFLICT = "conflict" + IDEMPOTENCY_EXPIRED = "idempotency_expired" + QUARANTINED = "quarantined" + + +@unique +class ChargeDomain(StrEnum): + """Capacity domains kept separate by the admission contract.""" + + INPUT_CONTEXT = "input_context" + OUTPUT_GENERATION = "output_generation" + + +@unique +class GenerationState(StrEnum): + """Lifecycle of a generated-output allowance.""" + + RESERVED = "reserved" + STREAMING = "streaming" + RECONCILED = "reconciled" + INDETERMINATE = "indeterminate" + QUARANTINED = "quarantined" + + +@unique +class MeasurementKind(StrEnum): + """Authority level of a count supplied to the pure reducer.""" + + PROVIDER_EXACT = "provider_exact" + TOKENIZER_EXACT = "tokenizer_exact" + HOST_ESTIMATE = "host_estimate" + BYTE_EMERGENCY = "byte_emergency" + + +@unique +class CoverageState(StrEnum): + """Evidence-backed observation or authority coverage state.""" + + VERIFIED = "verified" + PARTIAL = "partial" + UPSTREAM_GATED = "upstream_gated" + + +@unique +class CoverageEvidenceKind(StrEnum): + """Primary and inference evidence kinds accepted by the coverage registry.""" + + AUTOSKILLIT_SOURCE = "autoskillit_source" + CODEX_SOURCE = "codex_source" + CODEX_OFFICIAL_DOC = "codex_official_doc" + CODEX_RUNTIME_PROBE = "codex_runtime_probe" + INFERENCE = "inference" + + +@unique +class ReserveClass(StrEnum): + """Capability-scoped context reserve classes.""" + + ORDINARY = "ordinary" + SYNTHESIS = "synthesis" + FINAL_RESPONSE = "final_response" + + +@unique +class WitnessKind(StrEnum): + """Closed vocabulary of authoritative admission witnesses.""" + + EPOCH_SNAPSHOT = "epoch_snapshot" + INPUT_COUNTED = "input_counted" + HISTORY_STAGED = "history_staged" + REPRESENTATION_BOUND = "representation_bound" + REQUEST_INCLUDED = "request_included" + PROVIDER_ACCEPTED = "provider_accepted" + OUTPUT_USAGE = "output_usage" + TRUNCATION = "truncation" + NON_ADMISSION = "non_admission" + ROLLBACK = "rollback" + RECONCILIATION = "reconciliation" + IDEMPOTENCY_EXPIRY = "idempotency_expiry" + EPOCH_FENCE = "epoch_fence" + EPOCH_ROLLOVER = "epoch_rollover" + + +@unique +class ProducerSurface(StrEnum): + """Every model-visible producer covered by protocol version 1.""" + + NATIVE_SHELL = "native_shell" + UNIFIED_EXEC_AND_WRITE_STDIN = "unified_exec_and_write_stdin" + APPLY_PATCH = "apply_patch" + AUTOSKILLIT_MCP = "autoskillit_mcp" + EXTERNAL_MCP = "external_mcp" + AUTOSKILLIT_LOCAL_FUNCTION = "autoskillit_local_function" + OTHER_LOCAL_FUNCTION = "other_local_function" + MCP_RESOURCE = "mcp_resource" + CLIENT_PROVIDER_RETRIEVAL = "client_provider_retrieval" + CODE_MODE_AGGREGATE = "code_mode_aggregate" + HOSTED_SPECIALIZED_TOOL = "hosted_specialized_tool" + HOOK_FEEDBACK = "hook_feedback" + TOOL_ARGUMENT = "tool_argument" + TOOL_RESULT_ENVELOPE = "tool_result_envelope" + USER_PROMPT = "user_prompt" + ASSISTANT_OUTPUT_HISTORY = "assistant_output_history" + SKILL_PLUGIN_CONTEXT = "skill_plugin_context" + OTHER_CONTEXT_INJECTION = "other_context_injection" + HEADLESS_CHILD_PROMPT = "headless_child_prompt" + PARENT_VISIBLE_CHILD_DELIVERY = "parent_visible_child_delivery" + COMPACTION_MODEL_WINDOW_TRANSITION = "compaction_model_window_transition" diff --git a/src/autoskillit/core/types/_type_helpers.py b/src/autoskillit/core/types/_type_helpers.py index 1093ad5aa..dd8700b92 100644 --- a/src/autoskillit/core/types/_type_helpers.py +++ b/src/autoskillit/core/types/_type_helpers.py @@ -11,14 +11,17 @@ import re import shlex import warnings +from collections.abc import Callable +from datetime import date from pathlib import Path -from typing import Any, assert_never +from types import UnionType +from typing import Any, Never, Union, assert_never, get_args, get_origin from ._type_backend import BackendConventions from ._type_constants import SKILL_COMMAND_PREFIX from ._type_constants_env import HEADLESS_ENV_VAR, SESSION_TYPE_ENV_VAR from ._type_constants_registries import FLEET_ERROR_CODES -from ._type_enums import SessionType, SkillSource +from ._type_enums import SessionType, SkillSource, WitnessKind from ._type_protocols_workspace import SkillResolver from ._type_skill_contract import SkillSourceRef @@ -44,6 +47,244 @@ _PATH_PREFIXES: tuple[str, ...] = ("/", "./", ".autoskillit/") +CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 +_MAX_UINT64 = (1 << 64) - 1 +_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") +_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") +_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") +_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") +_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") +_FRESHNESS_POLICIES = frozenset( + { + "verify_on_version_or_configuration_change", + "verify_on_revision_change", + "infer_only", + } +) +_SENSITIVE_TEXT_MARKERS = ( + "authorization", + "bearer", + "content:", + "password", + "secret", + "token=", +) + + +class ContextAdmissionValidationError(ValueError): + """Raised when a protocol value violates a content-free invariant.""" + + +class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): + """Raised when a value uses unsupported protocol semantics.""" + + +def _raise_invalid(reason_code: str) -> Never: + raise ContextAdmissionValidationError(reason_code) + + +def _validate_protocol_version(protocol_version: int) -> None: + if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: + raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") + + +def _validate_non_negative(value: int, reason_code: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: + _raise_invalid(reason_code) + + +def _reconciled_snapshot_counts( + active_count: int, + remaining_count: int, + hard_limit: int, + deducted_charge: int, + terminal_charge: int, +) -> tuple[int, int]: + charge_delta = deducted_charge - terminal_charge + if charge_delta > 0: + capacity_slack = max(hard_limit - active_count - remaining_count, 0) + active_credit = min(max(charge_delta - capacity_slack, 0), active_count) + restored_count = min(charge_delta, capacity_slack + active_credit) + return active_count - active_credit, remaining_count + restored_count + additional_charge = min(-charge_delta, remaining_count) + return active_count + additional_charge, remaining_count - additional_charge + + +def _validate_bounded_text( + value: str, + reason_code: str, + *, + maximum: int = 128, + locator: bool = False, +) -> None: + if not isinstance(value, str) or not value or len(value) > maximum: + _raise_invalid(reason_code) + lowered = value.casefold() + pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT + if ( + any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) + or lowered.startswith("sha256:") + or lowered.startswith("blake2:") + or "\n" in value + or "\r" in value + or value.startswith("/") + or "\\" in value + or value.startswith("~") + or not pattern.fullmatch(value) + or (locator and ".." in value.split("/")) + ): + _raise_invalid(reason_code) + + +def _validate_reason_code( + value: str, + validation_error: str = "invalid_reason_code", +) -> None: + _validate_bounded_text(value, validation_error, maximum=64) + if not _REASON_CODE.fullmatch(value): + _raise_invalid(validation_error) + + +def _validate_iso_date(value: str) -> None: + if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): + _raise_invalid("invalid_checked_at") + try: + date.fromisoformat(value) + except ValueError: + raise ContextAdmissionValidationError("invalid_checked_at") from None + + +def _validate_tuple(value: object, reason_code: str) -> None: + if not isinstance(value, tuple): + _raise_invalid(reason_code) + + +def _validate_canonical_tuple( + value: tuple[Any, ...], + reason_code: str, + *, + key: Callable[[Any], Any], +) -> None: + _validate_tuple(value, reason_code) + if value != tuple(sorted(value, key=key)): + _raise_invalid(reason_code) + + +def _validate_git_revision(value: str) -> None: + if not isinstance(value, str) or not _GIT_REVISION.fullmatch(value): + _raise_invalid("invalid_tested_revision") + + +def _validate_freshness_policy(value: str) -> None: + if not isinstance(value, str) or value not in _FRESHNESS_POLICIES: + _raise_invalid("invalid_freshness_policy") + if value != "verify_on_version_or_configuration_change": + _raise_invalid("unsupported_coverage_freshness_policy") + + +def _validate_expired_idempotency_tombstone(tombstone: Any) -> None: + descriptor = tombstone.original_descriptor + input_reservations = descriptor.input_reservations + batch = descriptor.batch + if ( + tombstone.namespace != descriptor.idempotency_namespace + or tombstone.reservation_key.idempotency_namespace != tombstone.namespace + or len(input_reservations) != 1 + or tombstone.reservation_key != input_reservations[0].key + or tombstone.reservation_key.batch_id != batch.batch_id + or tombstone.original_terminal_decision.window_epoch_id + != tombstone.reservation_key.window_epoch_id + or tombstone.original_terminal_decision.snapshot_sequence != descriptor.snapshot_sequence + ): + _raise_invalid("idempotency_tombstone_identity_mismatch") + witness = tombstone.expiry_witness + if ( + witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY + or witness.window_epoch_id != tombstone.reservation_key.window_epoch_id + or witness.window_epoch_number != tombstone.reservation_key.window_epoch_number + or witness.snapshot_sequence != descriptor.snapshot_sequence + or witness.request_id != batch.request_id + or witness.batch_id != batch.batch_id + or witness.representation_revision != batch.manifest.representation_revision + or witness.representation_binding_id != batch.manifest.representation_binding_id + or witness.occurrence_ids != batch.occurrence_ids + ): + _raise_invalid("idempotency_tombstone_witness_mismatch") + + +def _validate_context_admission_state_metadata( + aggregate_revision: Any, + admission_sequence: Any, + processed_events: tuple[Any, ...], + idempotency_records: tuple[Any, ...], + expired_tombstones: tuple[Any, ...], + closed_epochs: tuple[Any, ...], +) -> None: + if len({record.event_id for record in processed_events}) != len(processed_events): + _raise_invalid("duplicate_processed_event") + processed_revisions = tuple(record.aggregate_revision.value for record in processed_events) + processed_sequences = tuple(record.admission_sequence.value for record in processed_events) + if ( + any(revision > aggregate_revision.value for revision in processed_revisions) + or any(sequence > admission_sequence.value for sequence in processed_sequences) + or any( + later < earlier for earlier, later in zip(processed_sequences, processed_sequences[1:]) + ) + ): + _raise_invalid("invalid_processed_event_coordinates") + idempotency_keys = tuple( + (record.namespace, record.reservation_key) for record in idempotency_records + ) + if len(set(idempotency_keys)) != len(idempotency_keys): + _raise_invalid("duplicate_idempotency_owner") + processed_by_event_id = {record.event_id: record for record in processed_events} + if any( + record.publication_revision.value > aggregate_revision.value + or (processed := processed_by_event_id.get(record.owning_event_id)) is None + or record.publication_revision != processed.aggregate_revision + for record in idempotency_records + ): + _raise_invalid("invalid_idempotency_publication_coordinates") + tombstone_keys = tuple( + (record.namespace, record.reservation_key) for record in expired_tombstones + ) + if len(set(tombstone_keys)) != len(tombstone_keys): + _raise_invalid("duplicate_idempotency_tombstone") + epoch_keys = tuple( + (audit.snapshot.window_epoch_id, audit.snapshot.window_epoch_number) + for audit in closed_epochs + ) + if len(set(epoch_keys)) != len(epoch_keys): + _raise_invalid("duplicate_closed_epoch") + + +def _matches_declared_type(value: object, declared_type: object) -> bool: + if declared_type is Any: + return True + origin = get_origin(declared_type) + if origin in {Union, UnionType}: + return any(_matches_declared_type(value, member) for member in get_args(declared_type)) + if origin is tuple: + if type(value) is not tuple: + return False + members = get_args(declared_type) + if len(members) == 2 and members[1] is Ellipsis: + return all(_matches_declared_type(item, members[0]) for item in value) + return len(value) == len(members) and all( + _matches_declared_type(item, member) + for item, member in zip(value, members, strict=True) + ) + if origin is frozenset: + if type(value) is not frozenset: + return False + (member_type,) = get_args(declared_type) + return all(_matches_declared_type(item, member_type) for item in value) + if declared_type is None or declared_type is type(None): + return value is None + if isinstance(declared_type, type): + return type(value) is declared_type + return False + def is_path_like_token(token: str) -> bool: return any(token.startswith(p) for p in _PATH_PREFIXES) diff --git a/tests/_test_filter.py b/tests/_test_filter.py index 8794d0ed1..6be7a1edf 100644 --- a/tests/_test_filter.py +++ b/tests/_test_filter.py @@ -239,6 +239,7 @@ class ImportContext(enum.StrEnum): "_type_backend": frozenset({"core", "execution", "cli", "recipe", "server", "workspace"}), "_type_recipe_delivery": frozenset({"core", "execution", "server"}), "_type_recipe_sections": frozenset({"core", "execution", "server"}), + "_type_context_admission": frozenset({"core"}), "_type_dispatch_identity": frozenset({"core", "fleet", "execution"}), "_type_figure_spec": frozenset({"core", "report"}), "_type_session_env": frozenset({"core", "cli"}), @@ -276,6 +277,7 @@ class ImportContext(enum.StrEnum): "bash_write_targets": frozenset({"core", "execution", "server"}), "_delivery_bounds": frozenset({"core", "execution", "server"}), "_type_closure_report": frozenset({"core"}), + "context_admission": frozenset({"core"}), "closure_hashing": frozenset({"core"}), "path_containment": frozenset({"core"}), "closure_verifier": frozenset({"core", "execution"}), diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 107326c66..77697705e 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -826,9 +826,12 @@ def test_no_subpackage_exceeds_10_files() -> None: _type_invariant_registry.py adds InvariantDef frozen dataclass and INVARIANT_REGISTRY mapping 13 prose prohibitions to runtime gate targets, bringing the core/types count to 31. + _type_recipe_sections.py adds recipe-section schema and digest contracts. _type_skill_contract.py adds the backend-neutral SkillSourceRef identity - consumed by workspace projections, bringing the core/types count to 34. - Exempt at 35 files (core/types: 34). + consumed by workspace projections. + _context_admission.py adds the pure context-admission reducer, and + _type_context_admission.py adds its frozen IL-0 contract records. + Exempt at 26 files (core/types: 36). cli/ — REQ-CNST-003-E5: cli/ retains _terminal_table.py as a re-export shim for backward-compatible cli/ imports; canonical implementation lives in core/_terminal_table.py. Also contains _terminal.py — the terminal state @@ -892,9 +895,8 @@ def test_no_subpackage_exceeds_10_files() -> None: "server": 16, # +_recipe_section_pagination deterministic bounded planner "recipe": 42, # was 33; +9 from CI/graph/dataflow splits "execution": 18, - "core": 25, # +_delivery_bounds (resolve_general_output_token_limit) - "core/types": 35, # +_type_recipe_sections schema and digest contracts - # +_type_skill_contract backend-neutral source identity + "core": 26, # +_context_admission pure reducer + "core/types": 36, # +_type_recipe_sections +_type_skill_contract +context admission "cli": 21, "cli/doctor": 11, # +_doctor_skills capability declaration authenticity checks "workspace": 12, # +skill_capabilities semantic validation +skill_projection @@ -1148,6 +1150,21 @@ def test_data_directories_are_not_python_packages() -> None: "cleanup remain in one manager; semantic validation and projection are already " "isolated in dedicated workspace modules.", ), + "core/context_admission.py": ( + 2950, + "REQ-CNST-010-E13: #4333 freezes one exhaustive protocol-v1 reducer and replay " + "surface. Keeping all closed event transitions together makes atomic batch, " + "idempotency, protected-pool, reconciliation, rollover, and declarative effect " + "semantics reviewable as one state machine; splitting dispatch branches would " + "fragment exhaustiveness.", + ), + "core/types/_type_context_admission.py": ( + 2300, + "REQ-CNST-010-E14: #4333 freezes the complete content-free protocol-v1 schema in " + "one IL-0 shard. Co-locating identities, records, closed event/effect unions, " + "states, canonical serialization, and the static coverage registry prevents " + "downstream layers from defining incompatible wire contracts.", + ), } diff --git a/tests/arch/test_subpackage_structure.py b/tests/arch/test_subpackage_structure.py index c674cfa47..a761ab0e8 100644 --- a/tests/arch/test_subpackage_structure.py +++ b/tests/arch/test_subpackage_structure.py @@ -22,6 +22,7 @@ def test_core_types_has_all_type_modules(self): "_type_constants_env", "_type_constants_features", "_type_constants_registries", + "_type_context_admission", "_type_dispatch_identity", "_type_enums", "_type_exceptions", diff --git a/tests/core/AGENTS.md b/tests/core/AGENTS.md index 6982427f6..0e7c6f33a 100644 --- a/tests/core/AGENTS.md +++ b/tests/core/AGENTS.md @@ -17,6 +17,9 @@ Core layer (IL-0) unit tests — paths, IO, types, feature flags. | `test_cmd_runner.py` | Tests for core/_cmd_runner.py — CmdRunner protocol, default_cmd_runner, run_git, run_gh | | `test_core.py` | Tests for the core/ sub-package foundation layer | | `test_core_terminal_table.py` | Tests for core/_terminal_table.py — the L0 shared table primitive | +| `test_context_admission_coverage.py` | Exact producer/control-point coverage registry and evidence metadata contract | +| `test_context_admission_reducer.py` | Pure context-admission reducer transition and replay tests | +| `test_context_admission_state_machine.py` | Property-based context-admission accounting and lifecycle invariants | | `test_ensure_project_temp_with_config.py` | Tests for ensure_project_temp with configurable override | | `test_feature_flags.py` | Tests for core/feature_flags.py — _collect_disabled_feature_tags helper | | `test_github_url.py` | Unit tests for core.github_url.parse_github_repo | @@ -70,3 +73,4 @@ Core layer (IL-0) unit tests — paths, IO, types, feature flags. | `test_closure_attacks.py` | Adversarial attack tests for closure-mode verification (forged verdicts, containment escape, ref drift, unauthorized rows, metadata stability) | | `test_closure_authority_spec.py` | Tests for ClosureAuthoritySpec and factory validation | | `test_closure_report.py` | Tests for ClosureRow and ClosureReport schema validation | +| `types/test_context_admission_contract.py` | Frozen context-admission enums, records, unions, validation, serialization, and gateway contract | diff --git a/tests/core/test_context_admission_coverage.py b/tests/core/test_context_admission_coverage.py new file mode 100644 index 000000000..6ae046a6b --- /dev/null +++ b/tests/core/test_context_admission_coverage.py @@ -0,0 +1,404 @@ +"""Freeze context-admission producer coverage and its supporting evidence.""" + +from __future__ import annotations + +from typing import NamedTuple + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_COVERAGE, + CoverageEvidenceKind, + CoverageState, + ProducerSurface, + resolve_context_admission_coverage, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + +CHECKED_AT = "2026-07-23" +FRESHNESS_POLICY = "verify_on_version_or_configuration_change" +CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" +REASON_CODE = "authoritative-watermark-unavailable" + + +class ExpectedEvidence(NamedTuple): + claim_id: str + kind: str + backend: str + configuration_mode: str + verifier: str + source_locator: str + tested_version: str + tested_revision: str + checked_at: str + freshness_policy: str + + +class ExpectedCoverageRow(NamedTuple): + surface: str + control_point_owner: str + observation_state: str + authority_state: str + evidence: tuple[ExpectedEvidence, ...] + reason_code: str + + +def _autoskillit_evidence(claim_id: str, source_locator: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "AUTOSKILLIT_SOURCE", + "autoskillit", + "default", + "source_inspection", + source_locator, + "0.10.890", + "ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _codex_evidence(claim_id: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "CODEX_SOURCE", + "codex", + "default", + "source_inspection", + "codex-rs/core/src/context_manager/history.rs", + "0.145.0", + CODEX_REVISION, + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _gap_evidence(claim_id: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "INFERENCE", + "codex", + "default", + "source_gap_analysis", + "docs/decisions/0007-context-admission.md", + "0.145.0", + CODEX_REVISION, + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _row( + surface: str, + owner: str, + observation_state: str, + evidence: ExpectedEvidence, +) -> ExpectedCoverageRow: + return ExpectedCoverageRow( + surface, + owner, + observation_state, + "UPSTREAM_GATED", + (evidence,), + REASON_CODE, + ) + + +EXPECTED_COVERAGE = ( + _row( + "NATIVE_SHELL", + "shell_capture_hook", + "VERIFIED", + _autoskillit_evidence( + "COV-NATIVE-SHELL", + "src/autoskillit/hooks/shell_capture_hook.py", + ), + ), + _row( + "UNIFIED_EXEC_AND_WRITE_STDIN", + "codex_host", + "PARTIAL", + _codex_evidence("COV-UNIFIED-EXEC-AND-WRITE-STDIN"), + ), + _row( + "APPLY_PATCH", + "codex_host", + "PARTIAL", + _codex_evidence("COV-APPLY-PATCH"), + ), + _row( + "AUTOSKILLIT_MCP", + "track_response_size", + "VERIFIED", + _autoskillit_evidence( + "COV-AUTOSKILLIT-MCP", + "src/autoskillit/server/_notify.py", + ), + ), + _row( + "EXTERNAL_MCP", + "fastmcp_client", + "PARTIAL", + _codex_evidence("COV-EXTERNAL-MCP"), + ), + _row( + "AUTOSKILLIT_LOCAL_FUNCTION", + "local_function_dispatch", + "VERIFIED", + _autoskillit_evidence( + "COV-AUTOSKILLIT-LOCAL-FUNCTION", + "src/autoskillit/execution/headless/_headless_helpers.py", + ), + ), + _row( + "OTHER_LOCAL_FUNCTION", + "codex_host", + "PARTIAL", + _codex_evidence("COV-OTHER-LOCAL-FUNCTION"), + ), + _row( + "MCP_RESOURCE", + "fastmcp_client", + "PARTIAL", + _codex_evidence("COV-MCP-RESOURCE"), + ), + _row( + "CLIENT_PROVIDER_RETRIEVAL", + "codex_host", + "UPSTREAM_GATED", + _gap_evidence("COV-CLIENT-PROVIDER-RETRIEVAL"), + ), + _row( + "CODE_MODE_AGGREGATE", + "codex_host", + "PARTIAL", + _codex_evidence("COV-CODE-MODE-AGGREGATE"), + ), + _row( + "HOSTED_SPECIALIZED_TOOL", + "codex_host", + "PARTIAL", + _codex_evidence("COV-HOSTED-SPECIALIZED-TOOL"), + ), + _row( + "HOOK_FEEDBACK", + "hook_registry", + "VERIFIED", + _autoskillit_evidence( + "COV-HOOK-FEEDBACK", + "src/autoskillit/hook_registry.py", + ), + ), + _row( + "TOOL_ARGUMENT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-TOOL-ARGUMENT"), + ), + _row( + "TOOL_RESULT_ENVELOPE", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-TOOL-RESULT-ENVELOPE"), + ), + _row( + "USER_PROMPT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-USER-PROMPT"), + ), + _row( + "ASSISTANT_OUTPUT_HISTORY", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-ASSISTANT-OUTPUT-HISTORY"), + ), + _row( + "SKILL_PLUGIN_CONTEXT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-SKILL-PLUGIN-CONTEXT"), + ), + _row( + "OTHER_CONTEXT_INJECTION", + "final_request_assembler", + "UPSTREAM_GATED", + _gap_evidence("COV-OTHER-CONTEXT-INJECTION"), + ), + _row( + "HEADLESS_CHILD_PROMPT", + "headless_prompt_builder", + "VERIFIED", + _autoskillit_evidence( + "COV-HEADLESS-CHILD-PROMPT", + "src/autoskillit/execution/headless/_headless_helpers.py", + ), + ), + _row( + "PARENT_VISIBLE_CHILD_DELIVERY", + "child_delivery_receipt", + "VERIFIED", + _autoskillit_evidence( + "COV-PARENT-VISIBLE-CHILD-DELIVERY", + "src/autoskillit/server/_recipe_delivery.py", + ), + ), + _row( + "COMPACTION_MODEL_WINDOW_TRANSITION", + "compaction_receiver", + "PARTIAL", + _codex_evidence("COV-COMPACTION-MODEL-WINDOW-TRANSITION"), + ), +) + +EXPECTED_SURFACES = ( + "NATIVE_SHELL", + "UNIFIED_EXEC_AND_WRITE_STDIN", + "APPLY_PATCH", + "AUTOSKILLIT_MCP", + "EXTERNAL_MCP", + "AUTOSKILLIT_LOCAL_FUNCTION", + "OTHER_LOCAL_FUNCTION", + "MCP_RESOURCE", + "CLIENT_PROVIDER_RETRIEVAL", + "CODE_MODE_AGGREGATE", + "HOSTED_SPECIALIZED_TOOL", + "HOOK_FEEDBACK", + "TOOL_ARGUMENT", + "TOOL_RESULT_ENVELOPE", + "USER_PROMPT", + "ASSISTANT_OUTPUT_HISTORY", + "SKILL_PLUGIN_CONTEXT", + "OTHER_CONTEXT_INJECTION", + "HEADLESS_CHILD_PROMPT", + "PARENT_VISIBLE_CHILD_DELIVERY", + "COMPACTION_MODEL_WINDOW_TRANSITION", +) + + +def _coverage_projection() -> tuple[ExpectedCoverageRow, ...]: + return tuple( + ExpectedCoverageRow( + row.surface.name, + row.control_point_owner, + row.observation_state.name, + row.authority_state.name, + tuple( + ExpectedEvidence( + item.claim_id, + item.kind.name, + item.backend, + item.configuration_mode, + item.verifier, + item.source_locator, + item.tested_version, + item.tested_revision, + str(item.checked_at), + item.freshness_policy, + ) + for item in row.evidence + ), + row.reason_code, + ) + for row in CONTEXT_ADMISSION_COVERAGE + ) + + +def test_producer_surface_and_registry_are_independently_exhaustive() -> None: + assert tuple(member.name for member in ProducerSurface) == EXPECTED_SURFACES + assert tuple(row.surface.name for row in CONTEXT_ADMISSION_COVERAGE) == EXPECTED_SURFACES + + +def test_coverage_registry_freezes_every_row_and_evidence_field() -> None: + assert isinstance(CONTEXT_ADMISSION_COVERAGE, tuple) + assert _coverage_projection() == EXPECTED_COVERAGE + assert all(isinstance(row.evidence, tuple) for row in CONTEXT_ADMISSION_COVERAGE) + + +def test_verified_observations_have_primary_evidence() -> None: + primary_kinds = { + CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + CoverageEvidenceKind.CODEX_SOURCE, + CoverageEvidenceKind.CODEX_OFFICIAL_DOC, + CoverageEvidenceKind.CODEX_RUNTIME_PROBE, + } + for row in CONTEXT_ADMISSION_COVERAGE: + if row.observation_state is CoverageState.VERIFIED: + assert any(item.kind in primary_kinds for item in row.evidence) + assert all(item.kind is not CoverageEvidenceKind.INFERENCE for item in row.evidence) + + +def test_claim_ids_are_stable_and_unique() -> None: + expected = tuple(f"COV-{surface.replace('_', '-')}" for surface in EXPECTED_SURFACES) + actual = tuple(row.evidence[0].claim_id for row in CONTEXT_ADMISSION_COVERAGE) + assert actual == expected + assert len(actual) == len(set(actual)) + + +def test_runtime_version_or_configuration_mismatch_degrades_deterministically() -> None: + for row in CONTEXT_ADMISSION_COVERAGE: + evidence = row.evidence[0] + resolved = resolve_context_admission_coverage( + row.surface, + evidence.backend, + evidence.configuration_mode, + evidence.tested_version, + CHECKED_AT, + ) + assert resolved == row + + mismatch_inputs = ( + ( + f"{evidence.backend}-mismatch", + evidence.configuration_mode, + evidence.tested_version, + CHECKED_AT, + ), + ( + evidence.backend, + f"{evidence.configuration_mode}-mismatch", + evidence.tested_version, + CHECKED_AT, + ), + ( + evidence.backend, + evidence.configuration_mode, + f"{evidence.tested_version}-mismatch", + CHECKED_AT, + ), + ( + evidence.backend, + evidence.configuration_mode, + evidence.tested_version, + "2026-07-24", + ), + ) + for backend, configuration_mode, source_version, as_of in mismatch_inputs: + mismatch = resolve_context_admission_coverage( + row.surface, + backend, + configuration_mode, + source_version, + as_of, + ) + assert mismatch.observation_state is CoverageState.UPSTREAM_GATED + assert mismatch.authority_state is CoverageState.UPSTREAM_GATED + assert mismatch.reason_code == "coverage-runtime-mismatch" + assert mismatch == resolve_context_admission_coverage( + row.surface, + backend, + configuration_mode, + source_version, + as_of, + ) + + +def test_compaction_observation_does_not_imply_authority() -> None: + row = next( + item + for item in CONTEXT_ADMISSION_COVERAGE + if item.surface is ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION + ) + assert row.observation_state is CoverageState.PARTIAL + assert row.authority_state is CoverageState.UPSTREAM_GATED diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py new file mode 100644 index 000000000..63b81e8d0 --- /dev/null +++ b/tests/core/test_context_admission_reducer.py @@ -0,0 +1,2822 @@ +"""Behavioral contract for the pure cumulative context-admission reducer.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatch, + AdmissionBatchId, + AdmissionDecisionKind, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionRequestId, + AdmissionReservation, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionWitness, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + AuthoritySourceId, + AuthorityUnavailableEvent, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ContextAdmissionValidationError, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + CoverageState, + DeliveryOccurrenceId, + DispatchRequestEvent, + EpochFenceProof, + ExpireIdempotencyKeyEvent, + ForkOccurrenceId, + GenerationReservationId, + GenerationReservationRecord, + GenerationState, + IdempotencyNamespace, + MarkGenerationIndeterminateEvent, + MarkIndeterminateEvent, + MeasurementKind, + ModelIdentity, + ModelItemId, + OpenEpochEvent, + PrepareBatchEvent, + ProducerInstanceId, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + ReconcileGenerationEvent, + ReleaseNonAdmissionEvent, + RepresentationBindingId, + RepresentationBindingWitness, + RepresentationRevision, + RequestReconciliationEvent, + ReservationInvalidatedEffect, + ReserveClass, + ReserveRequestEvent, + ResolveIndeterminateAcceptedEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, + RolloverEpochEvent, + StageHistoryEvent, + StartGenerationEvent, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + WindowEpochId, + WitnessKind, + reduce_context_admission, + replay_context_admission, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="test-caller", operation_kind=operation_kind) + + +def _event_fields( + state: UninitializedContextAdmissionState | ActiveContextAdmissionState, + event_id: str, + operation_kind: str, + *, + expected_revision: int | None = None, +) -> dict[str, object]: + return { + "event_id": AdmissionEventId(event_id), + "protocol_version": CONTEXT_ADMISSION_PROTOCOL_VERSION, + "idempotency_namespace": _namespace(operation_kind), + "expected_aggregate_revision": AggregateRevision( + state.aggregate_revision.value if expected_revision is None else expected_revision + ), + } + + +def _snapshot( + *, + epoch: int = 1, + sequence: int = 1, + active_count: int = 60, + hard_limit: int = 100, + remaining_count: int = 40, + model: str = "claude-test", + tokenizer: str = "tokenizer-test", +) -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + model_identity=ModelIdentity.anthropic(model), + tokenizer_identity=TokenizerIdentity(tokenizer), + snapshot_sequence=sequence, + active_count=active_count, + hard_limit=hard_limit, + remaining_count=remaining_count, + ) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +def _open_epoch( + *, + remaining_count: int = 40, + protected_pools: tuple[ProtectedPoolSpec, ...] = (), + epoch: int = 1, + state: UninitializedContextAdmissionState | ActiveContextAdmissionState | None = None, +) -> ActiveContextAdmissionState: + prior = state or _uninitialized() + transition = reduce_context_admission( + prior, + OpenEpochEvent( + **_event_fields(prior, f"open-{epoch}", "open-epoch"), + snapshot=_snapshot( + epoch=epoch, + active_count=100 - remaining_count, + remaining_count=remaining_count, + ), + protected_pools=protected_pools, + ), + ) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state + + +def _lineage( + occurrence: str, + *, + epoch: int = 1, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + parent_agent: str | None = None, + fork: str | None = None, + delivery: str | None = None, +) -> ContextLineage: + return ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId( + "session-child" if parent_agent is not None else "session-root" + ), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId( + f"agent-{occurrence}" if parent_agent is not None else "agent-root" + ), + parent_agent_id=AgentInstanceId(parent_agent) if parent_agent is not None else None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId( + f"thread-{occurrence}" if fork is not None else "thread-root" + ), + parent_thread_id=ContextThreadId("thread-root") if fork is not None else None, + fork_occurrence_id=ForkOccurrenceId(fork) if fork is not None else None, + turn_id=TurnId(f"turn-{occurrence}"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{occurrence}"), + tool_call_id=ToolCallId(f"tool-{occurrence}"), + model_item_id=ModelItemId(f"item-{occurrence}"), + dispatch_identity=None, + attempt_id=AdmissionAttemptId(f"attempt-{occurrence}"), + delivery_occurrence_id=(DeliveryOccurrenceId(delivery) if delivery is not None else None), + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + ) + + +def _occurrence( + name: str, + *, + maximum: int, + revision: str | None = None, + epoch: int = 1, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + lineage: ContextLineage | None = None, +) -> AdmissionOccurrence: + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(name), + lineage=lineage or _lineage(name, epoch=epoch, surface=surface), + reserve_class=reserve_class, + producer_surface=surface, + predicted_authoritative_maximum=maximum, + representation_revision=RepresentationRevision(revision or f"revision-{name}"), + owned_span_ids=(CanonicalSpanId(f"span-{name}"),), + ) + + +def _manifest( + request: str, + occurrences: tuple[AdmissionOccurrence, ...], + *, + revision: str = "revision-final", +) -> CanonicalRepresentationManifest: + return CanonicalRepresentationManifest( + request_id=AdmissionRequestId(request), + representation_revision=RepresentationRevision(revision), + representation_binding_id=RepresentationBindingId(f"binding-{request}"), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId(f"assembler-{request}"), + assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{request}"), + ) + + +def _batch( + name: str, + occurrences: tuple[AdmissionOccurrence, ...], + *, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_owner: str | None = None, + manifest_revision: str = "revision-final", +) -> AdmissionBatch: + request = f"request-{name}" + return AdmissionBatch( + batch_id=AdmissionBatchId(name), + request_id=AdmissionRequestId(request), + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=reserve_class, + protected_pool_owner_id=( + ProtectedPoolOwnerId(protected_owner) if protected_owner is not None else None + ), + manifest=_manifest(request, occurrences, revision=manifest_revision), + ) + + +def _reservation( + batch: AdmissionBatch, + occurrences: tuple[AdmissionOccurrence, ...], + *, + count: int, + snapshot_sequence: int = 1, +) -> AdmissionReservation: + namespace = _namespace("reserve-request") + key = AdmissionReservationKey( + idempotency_namespace=namespace, + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=occurrences[0].lineage.window_epoch_id, + window_epoch_number=occurrences[0].lineage.window_epoch_number, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_revisions=tuple( + (occurrence.occurrence_id, occurrence.representation_revision) + for occurrence in occurrences + ), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-{batch.batch_id.value}"), + key=key, + window_epoch_id=occurrences[0].lineage.window_epoch_id, + window_epoch_number=occurrences[0].lineage.window_epoch_number, + snapshot_sequence=snapshot_sequence, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_ids=batch.occurrence_ids, + reserved_count=count, + ) + + +def _generation_reservation( + batch: AdmissionBatch, + *, + maximum: int, + epoch: int = 1, + snapshot_sequence: int = 1, +) -> GenerationReservationRecord: + return GenerationReservationRecord( + generation_reservation_id=GenerationReservationId(f"generation-{batch.batch_id.value}"), + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=batch.occurrence_ids, + response_id=ModelItemId(f"response-{batch.batch_id.value}"), + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + snapshot_sequence=snapshot_sequence, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + maximum_allowance=maximum, + state=GenerationState.RESERVED, + exact_terminal_usage=None, + witness_ids=(), + authority_source_id=None, + ) + + +def _witness( + batch: AdmissionBatch, + kind: WitnessKind, + *, + witness: str | None = None, + revision: str | None = None, + epoch: int = 1, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(witness or f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=RepresentationRevision( + revision or batch.manifest.representation_revision.value + ), + representation_binding_id=batch.manifest.representation_binding_id, + occurrence_ids=batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _binding( + batch: AdmissionBatch, *, revision: str | None = None +) -> RepresentationBindingWitness: + bound_revision = RepresentationRevision( + revision or batch.manifest.representation_revision.value + ) + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _propose( + state: ActiveContextAdmissionState, + occurrence: AdmissionOccurrence, + *, + event_id: str | None = None, +) -> tuple[ActiveContextAdmissionState, ProposeOccurrenceEvent]: + event = ProposeOccurrenceEvent( + **_event_fields( + state, + event_id or f"propose-{occurrence.occurrence_id.value}", + "propose-occurrence", + ), + occurrence=occurrence, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state, event + + +def _reserve( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + occurrences: tuple[AdmissionOccurrence, ...], + *, + input_count: int, + generation_count: int = 0, + event_id: str | None = None, + expected_revision: int | None = None, +) -> tuple[Any, ReserveRequestEvent]: + event = ReserveRequestEvent( + **_event_fields( + state, + event_id or f"reserve-{batch.batch_id.value}", + "reserve-request", + expected_revision=expected_revision, + ), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, occurrences, count=input_count),), + generation_reservation=_generation_reservation(batch, maximum=generation_count), + ) + return reduce_context_admission(state, event), event + + +def _records_for(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> tuple[Any, ...]: + wanted = set(batch.occurrence_ids) + return tuple( + record for record in state.occurrence_records if record.occurrence.occurrence_id in wanted + ) + + +def _batch_record(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> Any: + return next( + record for record in state.batch_records if record.batch.batch_id == batch.batch_id + ) + + +def _assert_rejection_unchanged( + before: ActiveContextAdmissionState, + after: ActiveContextAdmissionState, +) -> None: + assert len(after.processed_events) == len(before.processed_events) + 1 + assert replace(after, processed_events=before.processed_events) == before + + +def _generation_record( + state: ActiveContextAdmissionState, reservation_id: GenerationReservationId +) -> Any: + return next( + record + for record in state.generation_reservations + if record.generation_reservation_id == reservation_id + ) + + +def _input_reservation( + state: ActiveContextAdmissionState, reservation_id: AdmissionReservationId +) -> AdmissionReservation: + return next( + reservation + for reservation in state.reservations + if reservation.reservation_id == reservation_id + ) + + +@pytest.mark.parametrize( + ("counts", "expected"), + [ + ((20, 20, 1), ("WOULD_ADMIT", "WOULD_ADMIT", "WOULD_REJECT")), + ((39, 1), ("WOULD_ADMIT", "WOULD_ADMIT")), + ((41,), ("WOULD_REJECT",)), + ], +) +def test_sequential_occurrences_exhaust_one_authoritative_window( + counts: tuple[int, ...], expected: tuple[str, ...] +) -> None: + state = _open_epoch(remaining_count=40) + decisions: list[str] = [] + for index, count in enumerate(counts): + occurrence = _occurrence(f"sequential-{index}", maximum=count) + state, _ = _propose(state, occurrence) + batch = _batch(f"batch-sequential-{index}", (occurrence,)) + transition, _ = _reserve(state, batch, (occurrence,), input_count=count) + decisions.append(transition.decision.kind.name) + assert isinstance(transition.next_state, ActiveContextAdmissionState) + state = transition.next_state + assert tuple(decisions) == expected + + +def test_stale_concurrent_proposals_cannot_overcommit_one_snapshot() -> None: + state = _open_epoch(remaining_count=30) + first = _occurrence("concurrent-a", maximum=20) + second = _occurrence("concurrent-b", maximum=20) + state, _ = _propose(state, first) + state, _ = _propose(state, second) + shared_revision = state.aggregate_revision.value + + first_batch = _batch("batch-concurrent-a", (first,)) + second_batch = _batch("batch-concurrent-b", (second,)) + first_transition, _ = _reserve( + state, + first_batch, + (first,), + input_count=20, + expected_revision=shared_revision, + ) + assert first_transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(first_transition.next_state, ActiveContextAdmissionState) + + stale_transition, _ = _reserve( + first_transition.next_state, + second_batch, + (second,), + input_count=20, + expected_revision=shared_revision, + ) + assert stale_transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged( + first_transition.next_state, + stale_transition.next_state, + ) + + +@pytest.mark.parametrize( + ("remaining", "input_count", "generation_count", "expected_kind"), + [ + (50, 30, 20, AdmissionDecisionKind.WOULD_ADMIT), + (49, 30, 20, AdmissionDecisionKind.WOULD_REJECT), + ], +) +def test_ordered_batch_and_generation_maximum_are_reserved_atomically( + remaining: int, + input_count: int, + generation_count: int, + expected_kind: AdmissionDecisionKind, +) -> None: + state = _open_epoch(remaining_count=remaining) + occurrences = ( + _occurrence("atomic-a", maximum=10), + _occurrence("atomic-b", maximum=20), + ) + for occurrence in occurrences: + state, _ = _propose(state, occurrence) + batch = _batch("batch-atomic", occurrences) + transition, _ = _reserve( + state, + batch, + occurrences, + input_count=input_count, + generation_count=generation_count, + ) + assert transition.decision.kind is expected_kind + assert isinstance(transition.next_state, ActiveContextAdmissionState) + records = _records_for(transition.next_state, batch) + assert len(records) == 2 + expected_state = ( + AdmissionState.RESERVED + if expected_kind is AdmissionDecisionKind.WOULD_ADMIT + else AdmissionState.PROPOSED + ) + assert {record.state for record in records} == {expected_state} + if expected_kind is AdmissionDecisionKind.WOULD_REJECT: + assert transition.next_state.generation_reservations == () + + +def _reserved_batch( + *, + remaining_count: int = 60, + input_count: int = 25, + generation_count: int = 20, + name: str = "lifecycle", +) -> tuple[ + ActiveContextAdmissionState, + AdmissionBatch, + tuple[AdmissionOccurrence, ...], + GenerationReservationRecord, +]: + state = _open_epoch(remaining_count=remaining_count) + occurrences = (_occurrence(name, maximum=input_count),) + state, _ = _propose(state, occurrences[0]) + batch = _batch(f"batch-{name}", occurrences) + transition, _ = _reserve( + state, + batch, + occurrences, + input_count=input_count, + generation_count=generation_count, + ) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + generation = _generation_reservation(batch, maximum=generation_count) + return transition.next_state, batch, occurrences, generation + + +def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: + state, _, _, existing_generation = _reserved_batch( + name="generation-id-owner", + input_count=5, + generation_count=5, + ) + occurrence = _occurrence("generation-id-collision", maximum=5) + state, _ = _propose(state, occurrence) + batch = _batch("batch-generation-id-collision", (occurrence,)) + _, event = _reserve( + state, + batch, + (occurrence,), + input_count=5, + generation_count=5, + ) + assert event.generation_reservation is not None + collision = replace( + event, + generation_reservation=replace( + event.generation_reservation, + generation_reservation_id=existing_generation.generation_reservation_id, + ), + ) + + rejected = reduce_context_admission(state, collision) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert ( + rejected.decision.reason_code == "generation-reservation-id-reuse-with-changed-descriptor" + ) + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(state, rejected.next_state) + + +def test_reserve_rejects_generation_reservation_id_retained_in_closed_epoch() -> None: + state, batch, _, existing_generation = _reserved_batch( + name="closed-generation-id-owner", + input_count=5, + generation_count=5, + ) + state = _prepare_dispatch(state, batch) + rolled = _rollover_with_receiver_fence( + state, + batch, + name="closed-generation-id-owner", + ) + occurrence = _occurrence( + "closed-generation-id-collision", + maximum=5, + epoch=2, + ) + rolled, _ = _propose(rolled, occurrence) + new_batch = _batch("batch-closed-generation-id-collision", (occurrence,)) + _, event = _reserve( + rolled, + new_batch, + (occurrence,), + input_count=5, + generation_count=5, + ) + assert event.generation_reservation is not None + collision = replace( + event, + generation_reservation=replace( + event.generation_reservation, + generation_reservation_id=existing_generation.generation_reservation_id, + ), + ) + + rejected = reduce_context_admission(rolled, collision) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert ( + rejected.decision.reason_code == "generation-reservation-id-reuse-with-changed-descriptor" + ) + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(rolled, rejected.next_state) + + +def test_reserve_event_rejects_terminal_generation_record() -> None: + state = _open_epoch() + occurrence = _occurrence("terminal-generation-reserve", maximum=10) + batch = _batch("batch-terminal-generation-reserve", (occurrence,)) + generation = replace( + _generation_reservation(batch, maximum=5), + state=GenerationState.RECONCILED, + exact_terminal_usage=5, + witness_ids=(AdmissionWitnessId("terminal-generation-witness"),), + authority_source_id=AuthoritySourceId("terminal-generation-authority"), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="generation_reservation_not_open", + ): + ReserveRequestEvent( + **_event_fields(state, "reserve-terminal-generation", "reserve-request"), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, (occurrence,), count=5),), + generation_reservation=generation, + ) + + +def test_active_state_rejects_global_capacity_overallocation() -> None: + state, _, _, _ = _reserved_batch( + remaining_count=60, + input_count=25, + generation_count=20, + name="global-overallocation", + ) + impossible_snapshot = replace( + state.snapshot, + active_count=56, + remaining_count=44, + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(state, snapshot=impossible_snapshot) + + assert "context_capacity_overallocated" in str(exc_info.value) + + +def test_active_state_requires_dispatched_batch_reservation_owner() -> None: + state, batch, _, _ = _reserved_batch(name="missing-dispatched-reservation") + state = _prepare_dispatch(state, batch) + batch_record = _batch_record(state, batch) + + with pytest.raises( + ContextAdmissionValidationError, + match="missing_active_batch_reservation", + ): + replace( + state, + occurrence_records=tuple( + replace(record, reservation_id=None) + if record.batch_id == batch.batch_id + else record + for record in state.occurrence_records + ), + batch_records=(replace(batch_record, reservation_id=None),), + reservations=(), + ) + + +def test_active_state_requires_indeterminate_batch_reservation_owner() -> None: + state, batch, _, _ = _reserved_batch(name="missing-indeterminate-reservation") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields( + state, + "mark-missing-indeterminate-reservation", + "mark-indeterminate", + ), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + batch_record = _batch_record(state, batch) + + with pytest.raises( + ContextAdmissionValidationError, + match="missing_active_batch_reservation", + ): + replace( + state, + occurrence_records=tuple( + replace(record, reservation_id=None) + if record.batch_id == batch.batch_id + else record + for record in state.occurrence_records + ), + batch_records=( + replace( + batch_record, + reservation_id=None, + unresolved_input_count=0, + ), + ), + reservations=(), + ) + + +def _prepare_dispatch( + state: ActiveContextAdmissionState, batch: AdmissionBatch +) -> ActiveContextAdmissionState: + batch_record = _batch_record(state, batch) + assert batch_record.reservation_id is not None + prepare = PrepareBatchEvent( + **_event_fields(state, f"prepare-{batch.batch_id.value}", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + ) + prepared = reduce_context_admission(state, prepare) + assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(prepared.next_state, ActiveContextAdmissionState) + stage = StageHistoryEvent( + **_event_fields( + prepared.next_state, + f"stage-{batch.batch_id.value}", + "stage-history", + ), + batch_id=batch.batch_id, + witness=_witness( + batch, + WitnessKind.HISTORY_STAGED, + epoch=prepared.next_state.snapshot.window_epoch_number, + ), + ) + staged = reduce_context_admission(prepared.next_state, stage) + assert staged.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(staged.next_state, ActiveContextAdmissionState) + dispatch = DispatchRequestEvent( + **_event_fields( + staged.next_state, + f"dispatch-{batch.batch_id.value}", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness( + batch, + WitnessKind.REQUEST_INCLUDED, + epoch=staged.next_state.snapshot.window_epoch_number, + ), + ) + dispatched = reduce_context_admission(staged.next_state, dispatch) + assert dispatched.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(dispatched.next_state, ActiveContextAdmissionState) + return dispatched.next_state + + +def _rollover_with_receiver_fence( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + name: str, +) -> ActiveContextAdmissionState: + receiver = AuthoritySourceId(f"receiver-{name}") + next_epoch_number = state.snapshot.window_epoch_number + 1 + next_epoch_id = WindowEpochId(f"epoch-{next_epoch_number}") + dispatch_count = sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == state.snapshot.window_epoch_id + and record.event.witness.window_epoch_number == state.snapshot.window_epoch_number + for record in state.processed_events + ) + event = RolloverEpochEvent( + **_event_fields(state, f"rollover-{name}", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=next_epoch_id, + new_window_epoch_number=next_epoch_number, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId(f"fence-{name}"), + highest_admitted_dispatch_sequence=dispatch_count, + ), + new_snapshot=_snapshot(epoch=next_epoch_number), + protected_pools=(), + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state + + +def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> None: + state, batch, _, generation = _reserved_batch(name="strict-lifecycle") + batch_record = _batch_record(state, batch) + assert batch_record.reservation_id is not None + prepare = PrepareBatchEvent( + **_event_fields(state, "prepare-strict-lifecycle", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + ) + prepared = reduce_context_admission(state, prepare) + assert isinstance(prepared.next_state, ActiveContextAdmissionState) + + skipped_stage = DispatchRequestEvent( + **_event_fields( + prepared.next_state, + "dispatch-without-history-stage", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + rejected_dispatch = reduce_context_admission(prepared.next_state, skipped_stage) + assert rejected_dispatch.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged(prepared.next_state, rejected_dispatch.next_state) + + stage = StageHistoryEvent( + **_event_fields(prepared.next_state, "stage-strict-lifecycle", "stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ) + staged = reduce_context_admission(prepared.next_state, stage) + assert isinstance(staged.next_state, ActiveContextAdmissionState) + premature_generation = StartGenerationEvent( + **_event_fields( + staged.next_state, + "generation-before-dispatch", + "start-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + rejected_generation = reduce_context_admission( + staged.next_state, + premature_generation, + ) + assert rejected_generation.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged(staged.next_state, rejected_generation.next_state) + + +def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> None: + state, batch, _, generation = _reserved_batch() + state = _prepare_dispatch(state, batch) + start = StartGenerationEvent( + **_event_fields(state, "start-generation-lifecycle", "start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + started = reduce_context_admission(state, start) + assert started.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(started.next_state, ActiveContextAdmissionState) + accept = AcceptInputEvent( + **_event_fields(started.next_state, "accept-lifecycle", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=18, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + accepted = reduce_context_admission(started.next_state, accept) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + assert _batch_record(accepted.next_state, batch).committed_input_count == 18 + + reconcile = ReconcileGenerationEvent( + **_event_fields( + accepted.next_state, + "reconcile-generation-lifecycle", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=7, + ) + reconciled = reduce_context_admission(accepted.next_state, reconcile) + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + generation_record = _generation_record( + reconciled.next_state, generation.generation_reservation_id + ) + assert generation_record.exact_terminal_usage == 7 + assert _batch_record(reconciled.next_state, batch).committed_input_count == 18 + + +def test_overlapping_tool_argument_and_history_span_is_rejected_before_debit() -> None: + tool_argument = _occurrence( + "tool-argument", + maximum=5, + surface=ProducerSurface.TOOL_ARGUMENT, + ) + history = replace( + _occurrence( + "history", + maximum=5, + surface=ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ), + owned_span_ids=tool_argument.owned_span_ids, + ) + with pytest.raises(ContextAdmissionValidationError): + _batch("overlapping", (tool_argument, history)) + + +def test_serialized_retry_is_idempotent_and_changed_intent_conflicts() -> None: + initial = _open_epoch() + occurrence = _occurrence("idempotent", maximum=7) + next_state, event = _propose(initial, occurrence, event_id="same-event") + restored_state = ActiveContextAdmissionState.from_dict(next_state.to_dict()) + restored_event = ProposeOccurrenceEvent.from_dict(event.to_dict()) + + retry = reduce_context_admission(restored_state, restored_event) + assert retry.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert retry.next_state == restored_state + + changed = replace( + restored_event, + occurrence=replace( + occurrence, + representation_revision=RepresentationRevision("changed-revision"), + ), + ) + conflict = reduce_context_admission(restored_state, changed) + assert conflict.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflict.next_state == restored_state + + +def test_same_reservation_key_under_new_delivery_returns_original_result() -> None: + state = _open_epoch() + occurrence = _occurrence("new-delivery-retry", maximum=7) + state, _ = _propose(state, occurrence) + batch = _batch("batch-new-delivery-retry", (occurrence,)) + reserved, event = _reserve( + state, + batch, + (occurrence,), + input_count=7, + generation_count=3, + ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + + redelivered = replace(event, event_id=AdmissionEventId("reserve-new-delivery-retry-2")) + retry = reduce_context_admission(reserved.next_state, redelivered) + assert retry.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert retry.next_state == reserved.next_state + + assert redelivered.generation_reservation is not None + changed_intent = replace( + redelivered, + generation_reservation=replace( + redelivered.generation_reservation, + maximum_allowance=4, + ), + ) + conflict = reduce_context_admission(reserved.next_state, changed_intent) + assert conflict.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflict.next_state == reserved.next_state + + +@pytest.mark.parametrize( + ("event_kind", "witness_kind"), + [ + ("stage", WitnessKind.REQUEST_INCLUDED), + ("dispatch", WitnessKind.HISTORY_STAGED), + ("release", WitnessKind.ROLLBACK), + ("rollback", WitnessKind.NON_ADMISSION), + ], +) +def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable( + event_kind: str, witness_kind: WitnessKind +) -> None: + state, batch, _, _ = _reserved_batch(name=f"witness-{event_kind}") + if event_kind == "stage": + event = StageHistoryEvent( + **_event_fields(state, f"bad-{event_kind}", "stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + elif event_kind == "dispatch": + event = DispatchRequestEvent( + **_event_fields(state, f"bad-{event_kind}", "dispatch-request"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + elif event_kind == "release": + event = ReleaseNonAdmissionEvent( + **_event_fields(state, f"bad-{event_kind}", "release-non-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + else: + event = RollbackAdmissionEvent( + **_event_fields(state, f"bad-{event_kind}", "rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged(state, rejected.next_state) + + +@pytest.mark.parametrize( + ("mutation", "expected_reason_fragment"), + [ + ("count-then-mutate", "revision"), + ("stale-prepared-revision", "revision"), + ("stale-receiver-fence", "fence"), + ], +) +def test_revision_and_fence_failures_do_not_commit( + mutation: str, expected_reason_fragment: str +) -> None: + state, batch, _, _ = _reserved_batch(name=mutation) + if mutation != "stale-receiver-fence": + event = PrepareBatchEvent( + **_event_fields(state, f"prepare-{mutation}", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=RepresentationRevision("mutated-revision"), + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=25, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + ) + else: + event = RolloverEpochEvent( + **_event_fields(state, "rollover-stale-fence", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=EpochFenceProof( + old_window_epoch_id=WindowEpochId("epoch-1"), + old_window_epoch_number=1, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=AuthoritySourceId("stale-receiver"), + fence_witness_id=AdmissionWitnessId("stale-fence"), + highest_admitted_dispatch_sequence=0, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert expected_reason_fragment in rejected.decision.reason_code + _assert_rejection_unchanged(state, rejected.next_state) + + +def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> None: + state = _open_epoch(remaining_count=40) + occurrences = ( + _occurrence("partial-a", maximum=10), + _occurrence("partial-b", maximum=10), + ) + for occurrence in occurrences: + state, _ = _propose(state, occurrence) + batch = _batch("batch-partial", occurrences) + reserved, _ = _reserve(state, batch, occurrences, input_count=20) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = reserved.next_state + + partial_witness = replace( + _witness(batch, WitnessKind.REQUEST_INCLUDED), + occurrence_ids=(occurrences[0].occurrence_id,), + ) + for index, event in enumerate( + ( + DispatchRequestEvent( + **_event_fields( + state, + "illegal-dispatch-before-prepare", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ), + DispatchRequestEvent( + **_event_fields( + state, + "partial-batch-dispatch", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=partial_witness, + ), + ) + ): + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT, index + _assert_rejection_unchanged(state, rejected.next_state) + + +@pytest.mark.parametrize( + "mutation", + ( + "final-manifest-revision", + "representation-revision-binding", + "same-revision-binding-identity", + "assembler-attestation", + "silent-truncation", + ), +) +def test_acceptance_rejects_mutated_or_unattested_final_representation( + mutation: str, +) -> None: + state, batch, _, _ = _reserved_batch(name=f"accept-{mutation}") + state = _prepare_dispatch(state, batch) + witness_kind = ( + WitnessKind.TRUNCATION + if mutation == "silent-truncation" + else WitnessKind.PROVIDER_ACCEPTED + ) + final_revision = ( + RepresentationRevision("mutated-final") + if mutation == "final-manifest-revision" + else batch.manifest.representation_revision + ) + binding_revision = ( + "mutated-binding" + if mutation == "representation-revision-binding" + else batch.manifest.representation_revision.value + ) + final_manifest = ( + replace( + batch.manifest, + assembler_witness_id=AdmissionWitnessId("mutated-assembler-attestation"), + ) + if mutation == "assembler-attestation" + else batch.manifest + ) + binding = _binding(batch, revision=binding_revision) + if mutation == "same-revision-binding-identity": + binding = replace( + binding, + representation_binding_id=RepresentationBindingId("mutated-binding-identity"), + ) + event = AcceptInputEvent( + **_event_fields(state, f"reject-{mutation}", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + final_manifest_revision=final_revision, + final_manifest=final_manifest, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("provider-test"), + representation_binding_witness=binding, + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged(state, rejected.next_state) + + +@pytest.mark.parametrize( + ("resolve_kind", "terminal_state"), + [ + ("non-admission", AdmissionState.RELEASED), + ("rollback", AdmissionState.ROLLED_BACK), + ], +) +def test_ambiguous_crash_stays_charged_until_an_authoritative_resolution( + resolve_kind: str, terminal_state: AdmissionState +) -> None: + state, batch, _, _ = _reserved_batch(name=f"crash-{resolve_kind}") + state = _prepare_dispatch(state, batch) + mark = MarkIndeterminateEvent( + **_event_fields(state, f"mark-{resolve_kind}", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-crash", + ) + marked = reduce_context_admission(state, mark) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + assert {record.state for record in _records_for(marked.next_state, batch)} == { + AdmissionState.INDETERMINATE + } + + query = RequestReconciliationEvent( + **_event_fields( + marked.next_state, + f"query-{resolve_kind}", + "request-reconciliation", + ), + target_id=batch.batch_id, + reason_code="deadline-observed", + ) + queried = reduce_context_admission(marked.next_state, query) + assert isinstance(queried.next_state, ActiveContextAdmissionState) + queried_batch = _batch_record(queried.next_state, batch) + assert queried_batch.reservation_id is not None + assert ( + _input_reservation(queried.next_state, queried_batch.reservation_id).reserved_count == 25 + ) + assert not any("Released" in type(effect).__name__ for effect in queried.effects) + + if resolve_kind == "non-admission": + resolution = ResolveIndeterminateNonAdmissionEvent( + **_event_fields( + queried.next_state, + f"resolve-{resolve_kind}", + "resolve-indeterminate-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ) + else: + resolution = ResolveIndeterminateRollbackEvent( + **_event_fields( + queried.next_state, + f"resolve-{resolve_kind}", + "resolve-indeterminate-rollback", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + resolved = reduce_context_admission(queried.next_state, resolution) + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert {record.state for record in _records_for(resolved.next_state, batch)} == { + terminal_state + } + + +@pytest.mark.parametrize("exact_charge", [26, 101]) +def test_provider_accepted_overage_is_recorded_and_quarantined( + exact_charge: int, +) -> None: + state, batch, _, _ = _reserved_batch( + remaining_count=100, input_count=25, name=f"overage-{exact_charge}" + ) + state = _prepare_dispatch(state, batch) + event = AcceptInputEvent( + **_event_fields(state, f"accept-overage-{exact_charge}", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=exact_charge, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("provider-test"), + representation_binding_witness=_binding(batch), + ) + quarantined = reduce_context_admission(state, event) + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + record = _batch_record(quarantined.next_state, batch) + assert record.committed_input_count == exact_charge + assert record.state is AdmissionState.QUARANTINED + + +def test_incomplete_manifest_rejection_is_non_mutating_and_retains_reservation() -> None: + state = _open_epoch(remaining_count=40) + occurrence = replace( + _occurrence("incomplete-manifest", maximum=20), + owned_span_ids=( + CanonicalSpanId("span-incomplete-manifest-a"), + CanonicalSpanId("span-incomplete-manifest-b"), + ), + ) + state, _ = _propose(state, occurrence) + complete_batch = _batch("batch-incomplete-manifest", (occurrence,)) + incomplete_manifest = replace( + complete_batch.manifest, + span_owners=complete_batch.manifest.span_owners[:1], + ) + batch = complete_batch + reserved, _ = _reserve(state, batch, (occurrence,), input_count=20) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + event = AcceptInputEvent( + **_event_fields(state, "accept-incomplete-manifest", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=incomplete_manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + rejected_acceptance = reduce_context_admission(state, event) + assert rejected_acceptance.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_acceptance.decision.reason_code == "representation-binding-mismatch" + _assert_rejection_unchanged(state, rejected_acceptance.next_state) + assert rejected_acceptance.effects == () + + next_occurrence = _occurrence("after-incomplete-manifest", maximum=24) + next_state, _ = _propose(state, next_occurrence) + next_batch = _batch("batch-after-incomplete-manifest", (next_occurrence,)) + rejected, _ = _reserve( + next_state, + next_batch, + (next_occurrence,), + input_count=24, + ) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + + +def test_authority_mismatch_quarantine_publishes_charge_state_and_quarantine() -> None: + state, batch, _, _ = _reserved_batch(name="authority-mismatch-effects") + state = _prepare_dispatch(state, batch) + event = AcceptInputEvent( + **_event_fields(state, "accept-authority-mismatch", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ) + quarantined = reduce_context_admission(state, event) + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + assert _batch_record(quarantined.next_state, batch).committed_input_count == 20 + assert {type(effect).__name__ for effect in quarantined.effects} == { + "ChargeCommittedEffect", + "OccurrenceStateChangedEffect", + "QuarantineRecordedEffect", + } + + +def test_fork_requires_distinct_epoch_and_parent_accepts_only_delivery() -> None: + state = _open_epoch(remaining_count=40) + lineages = ( + _lineage("resume"), + _lineage("fork", parent_agent="agent-root", fork="fork-1"), + _lineage( + "delivery", + parent_agent="agent-root", + fork="fork-1", + delivery="delivery-1", + surface=ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY, + ), + ) + occurrences = tuple( + _occurrence( + name, + maximum=5, + lineage=lineage, + surface=lineage.producer_surface, + ) + for name, lineage in zip(("resume", "fork", "delivery"), lineages, strict=True) + ) + state, _ = _propose(state, occurrences[0]) + fork_event = ProposeOccurrenceEvent( + **_event_fields(state, "propose-fork", "propose-occurrence"), + occurrence=occurrences[1], + ) + rejected_fork = reduce_context_admission(state, fork_event) + assert rejected_fork.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_fork.decision.reason_code == "fork-requires-distinct-epoch" + assert isinstance(rejected_fork.next_state, ActiveContextAdmissionState) + state = rejected_fork.next_state + state, _ = _propose(state, occurrences[2]) + assert {record.occurrence.occurrence_id for record in state.occurrence_records} == { + occurrences[0].occurrence_id, + occurrences[2].occurrence_id, + } + assert occurrences[1].lineage.current_thread_id != occurrences[0].lineage.current_thread_id + assert occurrences[2].lineage.delivery_occurrence_id is not None + + +def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> None: + state, batch, _, _ = _reserved_batch(name="rollover") + receiver_authority = AuthoritySourceId("receiver-2") + proof = EpochFenceProof( + old_window_epoch_id=WindowEpochId("epoch-1"), + old_window_epoch_number=1, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver_authority, + fence_witness_id=AdmissionWitnessId("fence-1-to-2"), + highest_admitted_dispatch_sequence=0, + ) + rollover_witness = replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver_authority, + ) + event = RolloverEpochEvent( + **_event_fields(state, "rollover-1-to-2", "rollover-epoch"), + witness=rollover_witness, + fence_proof=proof, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rolled = reduce_context_admission(state, event) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + assert rolled.next_state.snapshot.window_epoch_id == WindowEpochId("epoch-2") + assert len(rolled.next_state.closed_epochs) == 1 + assert { + record.state for record in rolled.next_state.closed_epochs[0].terminal_occurrence_records + } == {AdmissionState.INVALIDATED} + + second_batch = _batch("batch-rollover-2", (_occurrence("rollover-2", maximum=1),)) + second_proof = replace( + proof, + old_window_epoch_id=WindowEpochId("epoch-2"), + old_window_epoch_number=2, + new_window_epoch_id=WindowEpochId("epoch-3"), + new_window_epoch_number=3, + fence_witness_id=AdmissionWitnessId("fence-2-to-3"), + highest_admitted_dispatch_sequence=0, + ) + second_witness = replace( + _witness(second_batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + authority_source_id=second_proof.receiver_authority_source_id, + ) + second = RolloverEpochEvent( + **_event_fields(rolled.next_state, "rollover-2-to-3", "rollover-epoch"), + witness=second_witness, + fence_proof=second_proof, + new_snapshot=_snapshot(epoch=3, model="claude-new", tokenizer="tokenizer-new"), + protected_pools=(), + ) + rerolled = reduce_context_admission(rolled.next_state, second) + assert isinstance(rerolled.next_state, ActiveContextAdmissionState) + assert len(rerolled.next_state.closed_epochs) == 2 + assert rerolled.next_state.closed_epochs[0] == rolled.next_state.closed_epochs[0] + + +def test_reserve_rejects_batch_id_retained_in_closed_epoch() -> None: + state, batch, _, _ = _reserved_batch(name="closed-batch-id") + rolled = _rollover_with_receiver_fence(state, batch, name="closed-batch-id") + occurrence = _occurrence("closed-batch-id-new", maximum=5, epoch=2) + rolled, _ = _propose(rolled, occurrence) + reused_batch = _batch(batch.batch_id.value, (occurrence,)) + + rejected, _ = _reserve( + rolled, + reused_batch, + (occurrence,), + input_count=5, + event_id="reserve-closed-batch-id-reused", + ) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "batch-already-reserved" + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(rolled, rejected.next_state) + + +def test_closed_epoch_occurrence_identity_is_immutable_and_exact_retry_is_idempotent() -> None: + state, batch, occurrences, _ = _reserved_batch(name="closed-occurrence") + rolled = _rollover_with_receiver_fence( + state, + batch, + name="closed-occurrence", + ) + occurrence = occurrences[0] + exact_retry = ProposeOccurrenceEvent( + **_event_fields( + rolled, + "retry-closed-occurrence", + "propose-occurrence", + ), + occurrence=occurrence, + ) + replayed = reduce_context_admission(rolled, exact_retry) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == rolled + + changed_occurrences = ( + replace( + occurrence, + predicted_authoritative_maximum=occurrence.predicted_authoritative_maximum + 1, + ), + replace( + occurrence, + lineage=replace( + occurrence.lineage, + window_epoch_id=rolled.snapshot.window_epoch_id, + window_epoch_number=rolled.snapshot.window_epoch_number, + ), + ), + ) + current = rolled + for index, changed_occurrence in enumerate(changed_occurrences): + changed = ProposeOccurrenceEvent( + **_event_fields( + current, + f"reuse-closed-occurrence-{index}", + "propose-occurrence", + ), + occurrence=changed_occurrence, + ) + rejected = reduce_context_admission(current, changed) + assert rejected.decision.kind is AdmissionDecisionKind.QUARANTINED + assert rejected.decision.reason_code == "occurrence-identity-corruption" + _assert_rejection_unchanged(current, rejected.next_state) + current = rejected.next_state + + +def test_rollover_rejects_stale_admitted_dispatch_fence() -> None: + state, batch, _, _ = _reserved_batch(name="stale-dispatch-fence") + state = _prepare_dispatch(state, batch) + receiver_authority = AuthoritySourceId("receiver-authority") + proof = EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver_authority, + fence_witness_id=AdmissionWitnessId("stale-dispatch-fence"), + highest_admitted_dispatch_sequence=0, + ) + event = RolloverEpochEvent( + **_event_fields(state, "rollover-stale-dispatch", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver_authority, + ), + fence_proof=proof, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(state, rejected.next_state) + + +def _protected_pools() -> tuple[ProtectedPoolSpec, ...]: + return ( + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("synthesis-owner"), + injected_count=20, + priority=10, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ProtectedPoolSpec( + reserve_class=ReserveClass.FINAL_RESPONSE, + capability_owner_id=ProtectedPoolOwnerId("final-owner"), + injected_count=10, + priority=20, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ) + + +@pytest.mark.parametrize( + ("required_kind", "expected_state"), + [ + (WitnessKind.NON_ADMISSION, AdmissionState.RELEASED), + (WitnessKind.ROLLBACK, AdmissionState.ROLLED_BACK), + ], +) +def test_protected_pool_release_policy_accepts_only_its_configured_resolution( + required_kind: WitnessKind, + expected_state: AdmissionState, +) -> None: + owner = ProtectedPoolOwnerId("policy-owner") + pool = ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=owner, + injected_count=30, + priority=1, + required_release_witness_kind=required_kind, + ) + state = _open_epoch(remaining_count=60, protected_pools=(pool,)) + occurrence = _occurrence( + "protected-release-policy", + maximum=10, + reserve_class=ReserveClass.SYNTHESIS, + ) + state, _ = _propose(state, occurrence) + batch = _batch( + "batch-protected-release-policy", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner=owner.value, + ) + reserved, _ = _reserve( + state, + batch, + (occurrence,), + input_count=10, + generation_count=0, + ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-protected-policy", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + non_admission = ResolveIndeterminateNonAdmissionEvent( + **_event_fields(state, "resolve-protected-non-admission", "resolve-non-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ) + rollback = ResolveIndeterminateRollbackEvent( + **_event_fields(state, "resolve-protected-rollback", "resolve-rollback"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + allowed_event, rejected_event = ( + (non_admission, rollback) + if required_kind is WitnessKind.NON_ADMISSION + else (rollback, non_admission) + ) + + rejected = reduce_context_admission(state, rejected_event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "protected-release-policy-mismatch" + _assert_rejection_unchanged(state, rejected.next_state) + + allowed = reduce_context_admission(state, allowed_event) + assert allowed.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(allowed.next_state, ActiveContextAdmissionState) + assert _batch_record(allowed.next_state, batch).state is expected_state + + +def test_protected_pools_are_isolated_without_double_subtracting_usage() -> None: + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + synthesis = _occurrence("synthesis", maximum=15, reserve_class=ReserveClass.SYNTHESIS) + state, _ = _propose(state, synthesis) + synthesis_batch = _batch( + "batch-synthesis", + (synthesis,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + synthesis_transition, _ = _reserve( + state, + synthesis_batch, + (synthesis,), + input_count=10, + generation_count=5, + ) + assert synthesis_transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(synthesis_transition.next_state, ActiveContextAdmissionState) + + ordinary = _occurrence("ordinary-after-protected", maximum=70) + state, _ = _propose(synthesis_transition.next_state, ordinary) + ordinary_batch = _batch("batch-ordinary-after-protected", (ordinary,)) + exact_fit, _ = _reserve(state, ordinary_batch, (ordinary,), input_count=70) + assert exact_fit.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + + +def test_multiple_charges_aggregate_within_one_protected_pool() -> None: + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + for name, count in (("synthesis-a", 8), ("synthesis-b", 7)): + occurrence = _occurrence(name, maximum=count, reserve_class=ReserveClass.SYNTHESIS) + state, _ = _propose(state, occurrence) + batch = _batch( + f"batch-{name}", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + admitted, _ = _reserve(state, batch, (occurrence,), input_count=count) + assert admitted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(admitted.next_state, ActiveContextAdmissionState) + state = admitted.next_state + + over_pool = _occurrence( + "synthesis-over-pool", + maximum=6, + reserve_class=ReserveClass.SYNTHESIS, + ) + state, _ = _propose(state, over_pool) + over_pool_batch = _batch( + "batch-synthesis-over-pool", + (over_pool,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + rejected, _ = _reserve(state, over_pool_batch, (over_pool,), input_count=6) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + + +def test_reserve_event_rejects_batch_reservation_pool_policy_mismatch() -> None: + occurrence = _occurrence( + "synthesis-policy", + maximum=5, + reserve_class=ReserveClass.SYNTHESIS, + ) + batch = _batch( + "batch-synthesis-policy", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + reservation = _reservation(batch, (occurrence,), count=5) + wrong_owner = ProtectedPoolOwnerId("different-owner") + wrong_reservation = replace( + reservation, + key=replace( + reservation.key, + protected_pool_owner_id=wrong_owner, + ), + protected_pool_owner_id=wrong_owner, + ) + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + with pytest.raises(ContextAdmissionValidationError): + ReserveRequestEvent( + **_event_fields(state, "reserve-policy-mismatch", "reserve-request"), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(wrong_reservation,), + generation_reservation=None, + ) + + +@pytest.mark.parametrize( + ("authority_state", "expected"), + [ + (CoverageState.PARTIAL, AdmissionDecisionKind.WATERMARK_UNAVAILABLE), + (CoverageState.UPSTREAM_GATED, AdmissionDecisionKind.UPSTREAM_GATED), + ], +) +def test_authority_unavailable_never_creates_spendable_capacity( + authority_state: CoverageState, expected: AdmissionDecisionKind +) -> None: + state = _uninitialized() + event = AuthorityUnavailableEvent( + **_event_fields(state, f"authority-{authority_state.value}", "authority-unavailable"), + reason_code="no-atomic-watermark", + authority_state=authority_state, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is expected + assert isinstance(transition.next_state, UninitializedContextAdmissionState) + assert not hasattr(transition.next_state, "snapshot") + replayed = reduce_context_admission(transition.next_state, event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == transition.next_state + + +def test_authority_unavailable_active_event_replay_is_idempotent() -> None: + state = _open_epoch() + event = AuthorityUnavailableEvent( + **_event_fields(state, "authority-active", "authority-unavailable"), + reason_code="provider-watermark-unavailable", + authority_state=CoverageState.PARTIAL, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WATERMARK_UNAVAILABLE + assert isinstance(transition.next_state, ActiveContextAdmissionState) + replayed = reduce_context_admission(transition.next_state, event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == transition.next_state + + +def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> None: + initial = _uninitialized() + open_event = OpenEpochEvent( + **_event_fields(initial, "replay-open", "open-epoch"), + snapshot=_snapshot(), + protected_pools=(), + ) + opened = reduce_context_admission(initial, open_event) + assert isinstance(opened.next_state, ActiveContextAdmissionState) + occurrence = _occurrence("replay", maximum=10) + propose_event = ProposeOccurrenceEvent( + **_event_fields(opened.next_state, "replay-propose", "propose-occurrence"), + occurrence=occurrence, + ) + proposed = reduce_context_admission(opened.next_state, propose_event) + assert isinstance(proposed.next_state, ActiveContextAdmissionState) + batch = _batch("batch-replay", (occurrence,)) + reserve_event = ReserveRequestEvent( + **_event_fields(proposed.next_state, "replay-reserve", "reserve-request"), + batch=batch, + snapshot_sequence=proposed.next_state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, (occurrence,), count=10),), + generation_reservation=_generation_reservation(batch, maximum=0), + ) + sequential = reduce_context_admission(proposed.next_state, reserve_event) + replay = replay_context_admission(initial, (open_event, propose_event, reserve_event)) + assert replay.final_state == sequential.next_state + assert replay.transitions[-1] == sequential + restored = type(replay).from_dict(replay.to_dict()) + assert restored == replay + + +def test_rejected_event_retries_and_changed_reuse_have_replay_semantics() -> None: + state = _open_epoch() + rejected_event = PrepareBatchEvent( + **_event_fields(state, "rejected-event", "prepare-batch"), + batch_id=AdmissionBatchId("unknown-batch"), + representation_revision=RepresentationRevision("revision-1"), + representation_binding_id=RepresentationBindingId("binding-unknown"), + proposed_charge=1, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + ) + rejected = reduce_context_admission(state, rejected_event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + + replayed = reduce_context_admission(rejected.next_state, rejected_event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == rejected.next_state + + changed = replace( + rejected_event, + batch_id=AdmissionBatchId("different-unknown-batch"), + ) + conflicted = reduce_context_admission(rejected.next_state, changed) + assert conflicted.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflicted.next_state == rejected.next_state + + +def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: + state, batch, occurrences, _ = _reserved_batch(name="expiry") + reservation = _reservation(batch, occurrences, count=25) + event = ExpireIdempotencyKeyEvent( + **_event_fields(state, "expire-active", "expire-idempotency-key"), + reservation_key=reservation.key, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + _assert_rejection_unchanged(state, rejected.next_state) + + +@pytest.mark.parametrize("mark_indeterminate", [False, True]) +def test_post_rollover_expiry_cannot_tombstone_retained_work( + mark_indeterminate: bool, +) -> None: + state, batch, _, _ = _reserved_batch(name=f"retained-expiry-{mark_indeterminate}") + state = _prepare_dispatch(state, batch) + if mark_indeterminate: + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields( + state, + "mark-retained-expiry", + "mark-indeterminate", + ), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + reservation = state.reservations[0] + rolled = _rollover_with_receiver_fence( + state, + batch, + name=f"retained-expiry-{mark_indeterminate}", + ) + retained_before = rolled.closed_epochs[-1] + expiry = ExpireIdempotencyKeyEvent( + **_event_fields( + rolled, + f"expire-retained-{mark_indeterminate}", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + ) + rejected = reduce_context_admission(rolled, expiry) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "idempotency-key-not-terminal" + assert rejected.next_state.expired_idempotency_tombstones == () + assert rejected.next_state.closed_epochs[-1] == retained_before + _assert_rejection_unchanged(rolled, rejected.next_state) + + +def test_post_rollover_expiry_requires_the_complete_witness_binding() -> None: + state, batch, _, _ = _reserved_batch(name="expiry-binding") + reservation = state.reservations[0] + rolled = _rollover_with_receiver_fence( + state, + batch, + name="expiry-binding", + ) + valid_witness = _witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY) + mismatched_witnesses = ( + replace(valid_witness, request_id=AdmissionRequestId("wrong-request")), + replace(valid_witness, batch_id=AdmissionBatchId("wrong-batch")), + replace( + valid_witness, + representation_revision=RepresentationRevision("wrong-revision"), + ), + replace( + valid_witness, + representation_binding_id=RepresentationBindingId("wrong-binding"), + ), + replace(valid_witness, occurrence_ids=(AdmissionOccurrenceId("wrong-occurrence"),)), + replace(valid_witness, snapshot_sequence=valid_witness.snapshot_sequence + 1), + ) + current = rolled + for index, mismatched_witness in enumerate(mismatched_witnesses): + expiry = ExpireIdempotencyKeyEvent( + **_event_fields( + current, + f"expire-mismatched-binding-{index}", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=mismatched_witness, + ) + rejected = reduce_context_admission(current, expiry) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "invalid-expiry-witness" + assert rejected.next_state.expired_idempotency_tombstones == () + _assert_rejection_unchanged(current, rejected.next_state) + current = rejected.next_state + + accepted = reduce_context_admission( + current, + ExpireIdempotencyKeyEvent( + **_event_fields( + current, + "expire-complete-binding", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=valid_witness, + ), + ) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + assert len(accepted.next_state.expired_idempotency_tombstones) == 1 + + repeated = reduce_context_admission( + accepted.next_state, + ExpireIdempotencyKeyEvent( + **_event_fields( + accepted.next_state, + "expire-complete-binding-again", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=valid_witness, + ), + ) + assert repeated.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert repeated.decision.reason_code == "idempotency-key-expired" + assert len(repeated.next_state.expired_idempotency_tombstones) == 1 + _assert_rejection_unchanged(accepted.next_state, repeated.next_state) + + +def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: + state, batch, _, generation = _reserved_batch(name="generation-crash") + event = MarkGenerationIndeterminateEvent( + **_event_fields(state, "mark-generation-indeterminate", "mark-generation-indeterminate"), + generation_reservation_id=generation.generation_reservation_id, + reason_code="stream-disconnected", + ) + marked = reduce_context_admission(state, event) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + record = _generation_record(marked.next_state, generation.generation_reservation_id) + assert record.state is GenerationState.INDETERMINATE + assert record.maximum_allowance == 20 + + query = RequestReconciliationEvent( + **_event_fields( + marked.next_state, + "query-generation-indeterminate", + "request-reconciliation", + ), + target_id=generation.generation_reservation_id, + reason_code="deadline-observed", + ) + queried = reduce_context_admission(marked.next_state, query) + assert isinstance(queried.next_state, ActiveContextAdmissionState) + assert ( + _generation_record( + queried.next_state, generation.generation_reservation_id + ).maximum_allowance + == 20 + ) + assert not any("Released" in type(effect).__name__ for effect in queried.effects) + + +def test_resolve_indeterminate_acceptance_reconciles_exact_charge() -> None: + state, batch, _, _ = _reserved_batch(name="resolve-accepted") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-resolve-accepted", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + resolution = ResolveIndeterminateAcceptedEvent( + **_event_fields( + marked.next_state, + "resolve-accepted", + "resolve-indeterminate-accepted", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + resolved = reduce_context_admission(marked.next_state, resolution) + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert _batch_record(resolved.next_state, batch).committed_input_count == 19 + assert {record.state for record in _records_for(resolved.next_state, batch)} == { + AdmissionState.COMMITTED + } + + +def test_plain_rollback_cannot_resolve_indeterminate_input() -> None: + state, batch, _, _ = _reserved_batch(name="plain-rollback-indeterminate") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-plain-rollback", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + event = RollbackAdmissionEvent( + **_event_fields(marked.next_state, "plain-rollback", "rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + rejected = reduce_context_admission(marked.next_state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(marked.next_state, rejected.next_state) + assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE + + +def test_indeterminate_acceptance_requires_matching_authority_source() -> None: + state, batch, _, _ = _reserved_batch(name="indeterminate-authority") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-indeterminate-authority", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + event = ResolveIndeterminateAcceptedEvent( + **_event_fields( + marked.next_state, + "resolve-indeterminate-authority", + "resolve-indeterminate-accepted", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ) + rejected = reduce_context_admission(marked.next_state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE + + +def test_staged_release_uses_the_exact_single_witness_schema() -> None: + state, batch, _, _ = _reserved_batch(name="history-removal") + record = _batch_record(state, batch) + assert record.reservation_id is not None + prepared = reduce_context_admission( + state, + PrepareBatchEvent( + **_event_fields(state, "prepare-history-removal", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=_input_reservation( + state, + record.reservation_id, + ).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + ), + ) + staged = reduce_context_admission( + prepared.next_state, + StageHistoryEvent( + **_event_fields( + prepared.next_state, + "stage-history-removal", + "stage-history", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ), + ) + release = ReleaseNonAdmissionEvent( + **_event_fields( + staged.next_state, + "release-with-exact-witness", + "release-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ) + released = reduce_context_admission(staged.next_state, release) + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert _batch_record(released.next_state, batch).state is AdmissionState.RELEASED + + +def test_output_reconciliation_is_bound_to_exact_generation_request() -> None: + state, batch, _, generation = _reserved_batch( + name="bound-generation", + generation_count=8, + ) + state = _prepare_dispatch(state, batch) + started = reduce_context_admission( + state, + StartGenerationEvent( + **_event_fields(state, "start-bound-generation", "start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ), + ) + other_occurrence = _occurrence("other-output-request", maximum=1) + other_batch = _batch("batch-other-output-request", (other_occurrence,)) + mismatched = ReconcileGenerationEvent( + **_event_fields( + started.next_state, + "reconcile-cross-request", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(other_batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=4, + ) + rejected = reduce_context_admission(started.next_state, mismatched) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + unchanged = _generation_record( + rejected.next_state, + generation.generation_reservation_id, + ) + assert unchanged.state is GenerationState.STREAMING + + +def test_release_invalidates_generation_for_exact_batch_not_shared_request() -> None: + state = _open_epoch() + first_occurrence = _occurrence("shared-request-first", maximum=10) + second_occurrence = _occurrence("shared-request-second", maximum=10) + state, _ = _propose(state, first_occurrence) + state, _ = _propose(state, second_occurrence) + first_batch = _batch("batch-shared-request-first", (first_occurrence,)) + second_batch_template = _batch("batch-shared-request-second", (second_occurrence,)) + second_batch = replace( + second_batch_template, + request_id=first_batch.request_id, + manifest=replace( + second_batch_template.manifest, + request_id=first_batch.request_id, + ), + ) + first_reserved, _ = _reserve( + state, + first_batch, + (first_occurrence,), + input_count=5, + generation_count=5, + ) + assert isinstance(first_reserved.next_state, ActiveContextAdmissionState) + second_reserved, _ = _reserve( + first_reserved.next_state, + second_batch, + (second_occurrence,), + input_count=5, + generation_count=5, + ) + assert isinstance(second_reserved.next_state, ActiveContextAdmissionState) + before_release = second_reserved.next_state + + released = reduce_context_admission( + before_release, + ReleaseNonAdmissionEvent( + **_event_fields(before_release, "release-shared-request-first", "release"), + batch_id=first_batch.batch_id, + witness=_witness(first_batch, WitnessKind.NON_ADMISSION), + ), + ) + + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert tuple(record.batch_id for record in released.next_state.generation_reservations) == ( + second_batch.batch_id, + ) + invalidated_targets = { + effect.target_id + for effect in released.effects + if isinstance(effect, ReservationInvalidatedEffect) + } + assert invalidated_targets == { + GenerationReservationId(f"generation-{first_batch.batch_id.value}") + } + + +def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-resolution", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + receiver = AuthoritySourceId("receiver-closed-resolution") + rollover = RolloverEpochEvent( + **_event_fields(state, "rollover-closed-resolution", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId("fence-closed-resolution"), + highest_admitted_dispatch_sequence=1, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rolled = reduce_context_admission(state, rollover) + assert rolled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + assert rolled.next_state.batch_records == () + assert rolled.next_state.generation_reservations == () + audit = rolled.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 18 + assert audit.retained_generation_count == 7 + + accepted = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-resolution", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + accepted_audit = accepted.next_state.closed_epochs[-1] + assert accepted_audit.retained_unresolved_count == 0 + assert accepted_audit.terminal_batch_records[0].state is AdmissionState.COMMITTED + + reconciled = reduce_context_admission( + accepted.next_state, + ReconcileGenerationEvent( + **_event_fields( + accepted.next_state, + "reconcile-closed-generation", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=5, + ), + ) + assert reconciled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + reconciled_audit = reconciled.next_state.closed_epochs[-1] + assert reconciled_audit.retained_generation_count == 0 + assert reconciled_audit.terminal_generation_reservations[0].state is GenerationState.RECONCILED + + +def _rollover_closed_work( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + name: str, +) -> ActiveContextAdmissionState: + receiver = AuthoritySourceId(f"receiver-{name}") + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, f"rollover-{name}", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId(f"fence-{name}"), + highest_admitted_dispatch_sequence=1, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ), + ) + assert rolled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + return rolled.next_state + + +def test_closed_epoch_input_overage_quarantines_and_clears_retained_charge() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-input-overage", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-input-overage") + + quarantined = reduce_context_admission( + state, + AcceptInputEvent( + **_event_fields(state, "accept-closed-input-overage", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "provider-charge-exceeds-reservation" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert "QuarantineRecordedEffect" in {type(effect).__name__ for effect in quarantined.effects} + + +def test_closed_epoch_incomplete_span_ownership_quarantines() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-incomplete-spans", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-incomplete-spans") + audit = state.closed_epochs[-1] + occurrence_record = audit.terminal_occurrence_records[0] + incomplete_audit = replace( + audit, + terminal_occurrence_records=( + replace( + occurrence_record, + occurrence=replace( + occurrence_record.occurrence, + owned_span_ids=( + *occurrence_record.occurrence.owned_span_ids, + CanonicalSpanId("span-missing-from-closed-manifest"), + ), + ), + ), + ), + ) + state = replace(state, closed_epochs=(incomplete_audit,)) + + quarantined = reduce_context_admission( + state, + AcceptInputEvent( + **_event_fields(state, "accept-closed-incomplete-spans", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "incomplete-canonical-span-ownership" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + closed_audit = quarantined.next_state.closed_epochs[-1] + assert closed_audit.retained_unresolved_count == 0 + assert closed_audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert "QuarantineRecordedEffect" in {type(effect).__name__ for effect in quarantined.effects} + + +def test_closed_epoch_generation_overage_quarantines_and_clears_retained_charge() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-generation-overage", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-generation-overage") + + quarantined = reduce_context_admission( + state, + ReconcileGenerationEvent( + **_event_fields( + state, + "reconcile-closed-generation-overage", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=8, + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "generation-usage-exceeds-allowance" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_generation_count == 0 + assert audit.terminal_generation_reservations[0].state is GenerationState.QUARANTINED + assert {type(effect).__name__ for effect in quarantined.effects} == { + "GenerationReconciledEffect", + "QuarantineRecordedEffect", + } + + +def test_closed_epoch_authority_mismatch_quarantines_exact_charge() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-authority-mismatch", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + receiver = AuthoritySourceId("receiver-closed-authority-mismatch") + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-authority-mismatch", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId("fence-closed-authority-mismatch"), + highest_admitted_dispatch_sequence=1, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + quarantined = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-authority-mismatch", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "authority-source-mismatch" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert audit.terminal_batch_records[0].committed_input_count == 20 + assert {type(effect).__name__ for effect in quarantined.effects} == { + "ChargeCommittedEffect", + "OccurrenceStateChangedEffect", + "QuarantineRecordedEffect", + } + + +def test_no_fence_closed_reconciliation_restores_deducted_capacity() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-capacity-reconciliation", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-capacity-reconciliation", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2, active_count=65, remaining_count=35), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + accepted = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-capacity-reconciliation", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + assert accepted.next_state.snapshot.active_count == 64 + assert accepted.next_state.snapshot.remaining_count == 36 + + reconciled = reduce_context_admission( + accepted.next_state, + ReconcileGenerationEvent( + **_event_fields( + accepted.next_state, + "reconcile-closed-capacity-generation", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=5, + ), + ) + + assert reconciled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + assert reconciled.next_state.snapshot.active_count == 62 + assert reconciled.next_state.snapshot.remaining_count == 38 + + +def test_no_fence_closed_release_restores_all_deducted_capacity() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-capacity-release", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-capacity-release", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2, active_count=65, remaining_count=35), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + released = reduce_context_admission( + rolled.next_state, + ReleaseNonAdmissionEvent( + **_event_fields( + rolled.next_state, + "release-closed-capacity", + "release", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ), + ) + + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert released.next_state.snapshot.active_count == 40 + assert released.next_state.snapshot.remaining_count == 60 + audit = released.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.retained_generation_count == 0 + + +def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: + owner = ProtectedPoolOwnerId("rollover-policy-owner") + pool = ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=owner, + injected_count=20, + priority=1, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ) + state = _open_epoch(remaining_count=60) + sentinel = _batch( + "batch-rollover-policy-sentinel", + (_occurrence("rollover-policy-sentinel", maximum=1),), + ) + opened_by_rollover = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-policy-open", "rollover-epoch"), + witness=_witness(sentinel, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2), + protected_pools=(pool,), + ), + ) + assert isinstance(opened_by_rollover.next_state, ActiveContextAdmissionState) + state = opened_by_rollover.next_state + + occurrence = _occurrence( + "rollover-policy-work", + maximum=10, + reserve_class=ReserveClass.SYNTHESIS, + epoch=2, + ) + state, _ = _propose(state, occurrence) + batch = _batch( + "batch-rollover-policy-work", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner=owner.value, + ) + _, reserve_event = _reserve( + state, + batch, + (occurrence,), + input_count=10, + generation_count=0, + ) + reserved = reduce_context_admission( + state, + replace(reserve_event, generation_reservation=None), + ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-rollover-policy", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + + rolled_again = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-policy-close", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + fence_proof=None, + new_snapshot=_snapshot(epoch=3, active_count=70, remaining_count=30), + protected_pools=(), + ), + ) + assert rolled_again.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(rolled_again.next_state, ActiveContextAdmissionState) + resolved = reduce_context_admission( + rolled_again.next_state, + ResolveIndeterminateNonAdmissionEvent( + **_event_fields( + rolled_again.next_state, + "resolve-rollover-policy", + "resolve-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION, epoch=2), + ), + ) + + assert resolved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert ( + resolved.next_state.closed_epochs[-1].terminal_batch_records[0].state + is AdmissionState.RELEASED + ) + + +def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: + empty_state = _open_epoch() + sentinel = _batch( + "batch-rollover-authority", + (_occurrence("rollover-authority", maximum=1),), + ) + fully_resolved = RolloverEpochEvent( + **_event_fields(empty_state, "rollover-fully-resolved", "rollover-epoch"), + witness=_witness(sentinel, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + resolved_rollover = reduce_context_admission(empty_state, fully_resolved) + assert resolved_rollover.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + + state, batch, _, _ = _reserved_batch( + name="snapshot-deduction", + input_count=10, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + deducted = RolloverEpochEvent( + **_event_fields(state, "rollover-snapshot-deducted", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2, active_count=70, remaining_count=30), + protected_pools=(), + ) + deducted_rollover = reduce_context_admission(state, deducted) + assert deducted_rollover.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + invalid_deduction = replace( + deducted, + event_id=AdmissionEventId("rollover-invalid-deduction"), + new_snapshot=_snapshot(epoch=2, active_count=40, remaining_count=60), + ) + rejected = reduce_context_admission(state, invalid_deduction) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py new file mode 100644 index 000000000..6c247601c --- /dev/null +++ b/tests/core/test_context_admission_state_machine.py @@ -0,0 +1,1393 @@ +"""Bounded model-based checks for cumulative context-admission accounting.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +import pytest +from hypothesis import settings +from hypothesis import strategies as st +from hypothesis.stateful import RuleBasedStateMachine, invariant, precondition, rule + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatch, + AdmissionBatchId, + AdmissionDecisionKind, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionRequestId, + AdmissionReservation, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionWitness, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + AuthoritySourceId, + AuthorityUnavailableEffect, + AuthorityUnavailableEvent, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ChargeCommittedEffect, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + CoverageState, + DeliveryOccurrenceId, + DispatchRequestEvent, + EpochClosedEffect, + EpochFenceProof, + ExpireIdempotencyKeyEvent, + ForkOccurrenceId, + GenerationReconciledEffect, + GenerationReservationId, + GenerationReservationRecord, + GenerationReservationRecordedEffect, + GenerationState, + IdempotencyExpiredEffect, + IdempotencyNamespace, + MarkIndeterminateEvent, + MeasurementKind, + ModelIdentity, + ModelItemId, + OccurrenceStateChangedEffect, + OpenEpochEvent, + PrepareBatchEvent, + ProducerInstanceId, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + QuarantineRecordedEffect, + ReconcileGenerationEvent, + ReconciliationEscalationEffect, + ReconciliationQueryRequestedEffect, + ReleaseNonAdmissionEvent, + RepresentationBindingId, + RepresentationBindingWitness, + RepresentationRevision, + RequestReconciliationEvent, + ReservationInvalidatedEffect, + ReservationRecordedEffect, + ReservationReleasedEffect, + ReserveClass, + ReserveRequestEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, + RolloverEpochEvent, + StageHistoryEvent, + StartGenerationEvent, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + WindowEpochId, + WitnessKind, + reduce_context_admission, + replay_context_admission, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +_SLOT = st.integers(min_value=0, max_value=1) +_RESERVE_CLASS = st.sampled_from( + (ReserveClass.ORDINARY, ReserveClass.SYNTHESIS, ReserveClass.FINAL_RESPONSE) +) + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="state-machine", operation_kind=operation_kind) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +def _snapshot() -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ) + + +def _pool_specs() -> tuple[ProtectedPoolSpec, ...]: + return ( + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("synthesis-owner"), + injected_count=12, + priority=10, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ProtectedPoolSpec( + reserve_class=ReserveClass.FINAL_RESPONSE, + capability_owner_id=ProtectedPoolOwnerId("final-owner"), + injected_count=12, + priority=20, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ) + + +def _owner(reserve_class: ReserveClass) -> ProtectedPoolOwnerId | None: + if reserve_class is ReserveClass.SYNTHESIS: + return ProtectedPoolOwnerId("synthesis-owner") + if reserve_class is ReserveClass.FINAL_RESPONSE: + return ProtectedPoolOwnerId("final-owner") + return None + + +def _occurrence( + slot: int, + reserve_class: ReserveClass, + *, + window_epoch_id: WindowEpochId, + window_epoch_number: int, +) -> AdmissionOccurrence: + name = f"occurrence-{window_epoch_number}-{slot}" + surface = ( + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + if slot == 1 + else ProducerSurface.TOOL_RESULT_ENVELOPE + ) + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(name), + lineage=ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId("session-child" if slot == 1 else "session-root"), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId("agent-child" if slot == 1 else "agent-root"), + parent_agent_id=AgentInstanceId("agent-root") if slot == 1 else None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId("thread-child" if slot == 1 else "thread-root"), + parent_thread_id=ContextThreadId("thread-root") if slot == 1 else None, + fork_occurrence_id=(ForkOccurrenceId("fork-state-machine") if slot == 1 else None), + turn_id=TurnId(f"turn-{slot}"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{slot}"), + tool_call_id=ToolCallId(f"tool-{slot}"), + model_item_id=ModelItemId(f"item-{slot}"), + dispatch_identity=None, + attempt_id=AdmissionAttemptId(f"attempt-{slot}"), + delivery_occurrence_id=( + DeliveryOccurrenceId("delivery-state-machine") if slot == 1 else None + ), + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, + ), + reserve_class=reserve_class, + producer_surface=surface, + predicted_authoritative_maximum=15, + representation_revision=RepresentationRevision(f"revision-{slot}"), + owned_span_ids=(CanonicalSpanId(f"span-{slot}"),), + ) + + +def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: + identity = occurrence.occurrence_id.value.removeprefix("occurrence-") + request_id = AdmissionRequestId(f"request-{identity}") + manifest = CanonicalRepresentationManifest( + request_id=request_id, + representation_revision=occurrence.representation_revision, + representation_binding_id=RepresentationBindingId(f"binding-{identity}"), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId(f"assembler-{identity}"), + assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{identity}"), + ) + return AdmissionBatch( + batch_id=AdmissionBatchId(f"batch-{identity}"), + request_id=request_id, + occurrence_ids=(occurrence.occurrence_id,), + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + manifest=manifest, + ) + + +def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: + occurrences = tuple(sorted(occurrences, key=lambda occurrence: occurrence.occurrence_id.value)) + identities = tuple( + occurrence.occurrence_id.value.removeprefix("occurrence-") for occurrence in occurrences + ) + request_id = AdmissionRequestId("request-multi-" + "-".join(identities)) + reserve_class = occurrences[0].reserve_class + if any(occurrence.reserve_class is not reserve_class for occurrence in occurrences): + msg = "multi-member batch must share one reserve class" + raise ValueError(msg) + manifest = CanonicalRepresentationManifest( + request_id=request_id, + representation_revision=occurrences[0].representation_revision, + representation_binding_id=RepresentationBindingId("binding-multi-" + "-".join(identities)), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId("assembler-multi-" + "-".join(identities)), + assembler_witness_id=AdmissionWitnessId("assembler-witness-multi-" + "-".join(identities)), + ) + return AdmissionBatch( + batch_id=AdmissionBatchId("batch-multi-" + "-".join(identities)), + request_id=request_id, + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=reserve_class, + protected_pool_owner_id=_owner(reserve_class), + manifest=manifest, + ) + + +def _reservation( + occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int +) -> AdmissionReservation: + window_epoch_id = occurrence.lineage.window_epoch_id + window_epoch_number = occurrence.lineage.window_epoch_number + key = AdmissionReservationKey( + idempotency_namespace=_namespace("reserve-request"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, + batch_id=batch.batch_id, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + occurrence_revisions=((occurrence.occurrence_id, occurrence.representation_revision),), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-{occurrence.occurrence_id.value}"), + key=key, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, + snapshot_sequence=1, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + occurrence_ids=(occurrence.occurrence_id,), + reserved_count=count, + ) + + +def _reservation_for_batch( + batch: AdmissionBatch, + occurrences: tuple[AdmissionOccurrence, ...], + count: int, +) -> AdmissionReservation: + occurrence_ids = batch.occurrence_ids + occurrences_by_id = {occurrence.occurrence_id: occurrence for occurrence in occurrences} + slot = occurrence_ids[0].value.rsplit("-", 1)[-1] + occurrence = occurrences[0] + window_epoch_id = occurrence.lineage.window_epoch_id + window_epoch_number = occurrence.lineage.window_epoch_number + key = AdmissionReservationKey( + idempotency_namespace=_namespace("reserve-multi"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_revisions=tuple( + ( + occurrence_id, + occurrences_by_id[occurrence_id].representation_revision, + ) + for occurrence_id in occurrence_ids + ), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-multi-{slot}"), + key=key, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, + snapshot_sequence=1, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_ids=occurrence_ids, + reserved_count=count, + ) + + +def _generation( + occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int +) -> GenerationReservationRecord: + return GenerationReservationRecord( + generation_reservation_id=GenerationReservationId( + f"generation-{occurrence.occurrence_id.value}" + ), + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=batch.occurrence_ids, + response_id=ModelItemId(f"response-{occurrence.occurrence_id.value}"), + window_epoch_id=occurrence.lineage.window_epoch_id, + window_epoch_number=occurrence.lineage.window_epoch_number, + snapshot_sequence=1, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + maximum_allowance=count, + state=GenerationState.RESERVED, + exact_terminal_usage=None, + witness_ids=(), + authority_source_id=None, + ) + + +def _witness( + batch: AdmissionBatch, + occurrence: AdmissionOccurrence, + kind: WitnessKind, + *, + occurrence_ids: tuple[AdmissionOccurrenceId, ...] | None = None, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=occurrence.lineage.window_epoch_id, + window_epoch_number=occurrence.lineage.window_epoch_number, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + occurrence_ids=occurrence_ids or batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + + +def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: + bound_revision = batch.manifest.representation_revision + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + + +class ContextAdmissionStateMachine(RuleBasedStateMachine): + """Compare reducer transitions with a deliberately small accounting oracle.""" + + def __init__(self) -> None: + super().__init__() + initial = _uninitialized() + open_event = OpenEpochEvent( + event_id=AdmissionEventId("open-state-machine"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("open-epoch"), + expected_aggregate_revision=initial.aggregate_revision, + snapshot=_snapshot(), + protected_pools=_pool_specs(), + ) + opened = reduce_context_admission(initial, open_event) + assert opened.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(opened.next_state, ActiveContextAdmissionState) + self.state = opened.next_state + self.occurrences: dict[int, AdmissionOccurrence] = {} + self.batches: dict[int, AdmissionBatch] = {} + self.charges: dict[int, tuple[ReserveClass, int, int]] = {} + self.latest_replayable_event: Any | None = open_event + self.latest_propose_event: ProposeOccurrenceEvent | None = None + self.events: list[Any] = [open_event] + self.published_effects: list[tuple[object, ...]] = [opened.effects] + self.event_sequence = 0 + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + self.closed_audit_count = len(self.state.closed_epochs) + self.last_rollover_retention: int | None = None + + def _find_batch(self, batch_id: AdmissionBatchId) -> Any: + return next( + (record for record in self.state.batch_records if record.batch.batch_id == batch_id), + None, + ) + + def _reserved_input_count(self, batch: AdmissionBatch) -> int | None: + record = self._find_batch(batch.batch_id) + if record is None or record.reservation_id is None: + return None + reservation = next( + ( + reservation + for reservation in self.state.reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + return reservation.reserved_count if reservation is not None else None + + def _clear_batch_charges(self, batch: AdmissionBatch) -> None: + for slot, tracked_batch in self.batches.items(): + if tracked_batch.batch_id != batch.batch_id or slot not in self.charges: + continue + reserve_class, _, _ = self.charges[slot] + self.charges[slot] = (reserve_class, 0, 0) + + def _clear_generation_charges(self, batch: AdmissionBatch) -> None: + for slot, tracked_batch in self.batches.items(): + if tracked_batch.batch_id != batch.batch_id or slot not in self.charges: + continue + reserve_class, input_count, _ = self.charges[slot] + self.charges[slot] = (reserve_class, input_count, 0) + + def _fields(self, operation_kind: str) -> dict[str, object]: + self.event_sequence += 1 + return { + "event_id": AdmissionEventId(f"event-{self.event_sequence}"), + "protocol_version": CONTEXT_ADMISSION_PROTOCOL_VERSION, + "idempotency_namespace": _namespace(operation_kind), + "expected_aggregate_revision": self.state.aggregate_revision, + } + + def _accept_publication(self, transition: Any, event: object) -> None: + assert isinstance(transition.next_state, ActiveContextAdmissionState) + assert ( + transition.next_state.aggregate_revision.value >= self.state.aggregate_revision.value + ) + assert ( + transition.next_state.admission_sequence.value >= self.state.admission_sequence.value + ) + expected_effect_types = self._expected_effect_types(transition, event) + assert tuple(type(effect) for effect in transition.effects) == expected_effect_types + for effect in transition.effects: + assert effect.source_event_id == getattr(event, "event_id") + assert effect.resulting_aggregate_revision == transition.next_state.aggregate_revision + assert effect.resulting_admission_sequence == transition.next_state.admission_sequence + self.state = transition.next_state + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + event_id = getattr(event, "event_id") + if any(record.event_id == event_id for record in self.state.processed_events): + self.latest_replayable_event = event + self.events.append(event) + self.published_effects.append(transition.effects) + + def _expected_effect_types( + self, + transition: Any, + event: object, + ) -> tuple[type[object], ...]: + if isinstance(event, AuthorityUnavailableEvent): + return (AuthorityUnavailableEffect,) + if transition.decision.kind not in { + AdmissionDecisionKind.WOULD_ADMIT, + AdmissionDecisionKind.QUARANTINED, + }: + return () + if isinstance(event, ProposeOccurrenceEvent | StartGenerationEvent): + return () + if isinstance(event, ReserveRequestEvent): + effect_types: tuple[type[object], ...] = ( + ReservationRecordedEffect, + *(OccurrenceStateChangedEffect for _ in event.batch.occurrence_ids), + ) + if ( + event.generation_reservation is not None + and event.generation_reservation.maximum_allowance > 0 + ): + effect_types += (GenerationReservationRecordedEffect,) + return effect_types + if isinstance( + event, + PrepareBatchEvent | StageHistoryEvent | DispatchRequestEvent | MarkIndeterminateEvent, + ): + record = self._find_batch(event.batch_id) + assert record is not None + return tuple(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids) + if isinstance(event, AcceptInputEvent): + record = self._find_batch(event.batch_id) + assert record is not None + effect_types = ( + ChargeCommittedEffect, + *(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids), + ) + if transition.decision.kind is AdmissionDecisionKind.QUARANTINED: + effect_types += (QuarantineRecordedEffect,) + return effect_types + if isinstance( + event, + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent, + ): + record = self._find_batch(event.batch_id) + assert record is not None + generation_count = sum( + generation.batch_id == record.batch.batch_id + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + for generation in self.state.generation_reservations + ) + return ( + ReservationReleasedEffect, + *(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids), + *(ReservationInvalidatedEffect for _ in range(generation_count)), + ) + if isinstance(event, ReconcileGenerationEvent): + effect_types = (GenerationReconciledEffect,) + if transition.decision.kind is AdmissionDecisionKind.QUARANTINED: + effect_types += (QuarantineRecordedEffect,) + return effect_types + if isinstance(event, ExpireIdempotencyKeyEvent): + return (IdempotencyExpiredEffect,) + if isinstance(event, RolloverEpochEvent): + invalidated_batch_ids = { + record.batch.batch_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + } + input_invalidation_count = sum( + reservation.key.batch_id in invalidated_batch_ids + for reservation in self.state.reservations + ) + retained_batch_ids = { + record.batch.batch_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + } + generation_invalidation_count = sum( + generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + and generation.batch_id not in retained_batch_ids + for generation in self.state.generation_reservations + ) + occurrence_change_count = sum( + record.state + in { + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + for record in self.state.occurrence_records + ) + return ( + *( + ReservationInvalidatedEffect + for _ in range(input_invalidation_count + generation_invalidation_count) + ), + *(OccurrenceStateChangedEffect for _ in range(occurrence_change_count)), + EpochClosedEffect, + ) + if isinstance(event, RequestReconciliationEvent): + return ( + ReconciliationEscalationEffect + if "deadline" in event.reason_code.casefold() + else ReconciliationQueryRequestedEffect, + ) + raise AssertionError(f"missing effect contract for {type(event).__name__}") + + def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: + total_charged = sum( + input_count + output_count for _, input_count, output_count in self.charges.values() + ) + global_unallocated = 50 - total_charged + synthesis_used = sum( + input_count + output_count + for charged_class, input_count, output_count in self.charges.values() + if charged_class is ReserveClass.SYNTHESIS + ) + final_used = sum( + input_count + output_count + for charged_class, input_count, output_count in self.charges.values() + if charged_class is ReserveClass.FINAL_RESPONSE + ) + synthesis_unused = 12 - synthesis_used + final_unused = 12 - final_used + ordinary_available = global_unallocated - synthesis_unused - final_unused + if reserve_class is ReserveClass.SYNTHESIS: + class_available = min(global_unallocated, synthesis_unused) + elif reserve_class is ReserveClass.FINAL_RESPONSE: + class_available = min(global_unallocated, final_unused) + else: + class_available = ordinary_available + return global_unallocated, ordinary_available, class_available + + @rule(authority_state=st.sampled_from((CoverageState.PARTIAL, CoverageState.UPSTREAM_GATED))) + def report_authority_unavailable(self, authority_state: CoverageState) -> None: + event = AuthorityUnavailableEvent( + **self._fields("authority-unavailable"), + reason_code="authority-watermark-unavailable", + authority_state=authority_state, + ) + transition = reduce_context_admission(self.state, event) + expected = ( + AdmissionDecisionKind.UPSTREAM_GATED + if authority_state is CoverageState.UPSTREAM_GATED + else AdmissionDecisionKind.WATERMARK_UNAVAILABLE + ) + assert transition.decision.kind is expected + self._accept_publication(transition, event) + + @rule(slot=_SLOT, reserve_class=_RESERVE_CLASS) + def propose(self, slot: int, reserve_class: ReserveClass) -> None: + if slot in self.occurrences: + return + occurrence = _occurrence( + slot, + reserve_class, + window_epoch_id=self.state.snapshot.window_epoch_id, + window_epoch_number=self.state.snapshot.window_epoch_number, + ) + event = ProposeOccurrenceEvent(**self._fields("propose-occurrence"), occurrence=occurrence) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self.occurrences[slot] = occurrence + self.latest_propose_event = event + + @rule( + slot=_SLOT, + input_count=st.integers(min_value=1, max_value=15), + generation_count=st.integers(min_value=0, max_value=8), + ) + def reserve(self, slot: int, input_count: int, generation_count: int) -> None: + if slot not in self.occurrences or slot in self.charges: + return + occurrence = self.occurrences[slot] + batch = _batch(occurrence) + event = ReserveRequestEvent( + **self._fields("reserve-request"), + batch=batch, + snapshot_sequence=1, + input_reservations=(_reservation(occurrence, batch, input_count),), + generation_reservation=_generation(occurrence, batch, generation_count), + ) + prior = self.state + transition = reduce_context_admission(prior, event) + _, _, class_available = self._availability(occurrence.reserve_class) + expected_admit = input_count + generation_count <= class_available + assert transition.decision.kind is ( + AdmissionDecisionKind.WOULD_ADMIT + if expected_admit + else AdmissionDecisionKind.WOULD_REJECT + ) + if expected_admit: + self._accept_publication(transition, event) + self.batches[slot] = batch + self.charges[slot] = ( + occurrence.reserve_class, + input_count, + generation_count, + ) + else: + assert ( + replace( + transition.next_state, + processed_events=prior.processed_events, + ) + == prior + ) + self._accept_publication(transition, event) + + @rule() + def stale_fence_is_rejected(self) -> None: + old_snapshot = self.state.snapshot + new_window_epoch_number = old_snapshot.window_epoch_number + 1 + receiver_authority = AuthoritySourceId("authority-state-machine") + event = RolloverEpochEvent( + **self._fields("rollover-epoch"), + witness=AdmissionWitness( + witness_id=AdmissionWitnessId(f"stale-rollover-witness-{self.event_sequence}"), + kind=WitnessKind.EPOCH_ROLLOVER, + window_epoch_id=old_snapshot.window_epoch_id, + window_epoch_number=old_snapshot.window_epoch_number, + snapshot_sequence=old_snapshot.snapshot_sequence, + request_id=AdmissionRequestId("stale-rollover-request"), + batch_id=AdmissionBatchId("stale-rollover-batch"), + representation_revision=RepresentationRevision("stale-rollover-revision"), + representation_binding_id=RepresentationBindingId("stale-rollover-binding"), + occurrence_ids=(), + authority_source_id=receiver_authority, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=WindowEpochId("stale-window-epoch"), + old_window_epoch_number=old_snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId( + f"epoch-state-machine-{new_window_epoch_number}" + ), + new_window_epoch_number=new_window_epoch_number, + receiver_authority_source_id=receiver_authority, + fence_witness_id=AdmissionWitnessId(f"stale-fence-{self.event_sequence}"), + highest_admitted_dispatch_sequence=self.state.admission_sequence.value, + ), + new_snapshot=ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}"), + window_epoch_number=new_window_epoch_number, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ), + protected_pools=_pool_specs(), + ) + prior = self.state + transition = reduce_context_admission(prior, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert replace(transition.next_state, processed_events=prior.processed_events) == prior + self._accept_publication(transition, event) + + @rule( + slot=_SLOT, + input_count=st.integers(min_value=1, max_value=10), + ) + def reserve_multi_member_batch(self, slot: int, input_count: int) -> None: + other_slot = 1 if slot == 0 else 0 + if other_slot not in self.occurrences or slot not in self.occurrences: + return + if slot in self.charges or other_slot in self.charges: + return + first = self.occurrences[slot] + second = self.occurrences[other_slot] + if first.reserve_class is not second.reserve_class: + return + batch = _multi_batch((first, second)) + total = input_count * 2 + event = ReserveRequestEvent( + **self._fields("reserve-multi"), + batch=batch, + snapshot_sequence=1, + input_reservations=(_reservation_for_batch(batch, (first, second), total),), + generation_reservation=None, + ) + prior = self.state + transition = reduce_context_admission(prior, event) + if transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT: + self._accept_publication(transition, event) + return + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self.batches[slot] = batch + self.batches[other_slot] = batch + self.charges[slot] = (first.reserve_class, total, 0) + self.charges[other_slot] = (second.reserve_class, 0, 0) + + @rule(accept_now=st.booleans()) + def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: + if not self.charges: + return + slot = min(self.charges) + reserve_class, _, _ = self.charges[slot] + if reserve_class is ReserveClass.ORDINARY: + return + occurrence = self.occurrences[slot] + batch = self.batches.get(slot) + if batch is None: + return + reserved = self._find_batch(batch.batch_id) + if reserved is None or reserved.state is not AdmissionState.RESERVED: + return + reserved_input_count = self._reserved_input_count(batch) + if reserved_input_count is None: + return + prepare_event = PrepareBatchEvent( + **self._fields("prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=reserved_input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-state-machine"), + ) + transition = reduce_context_admission(self.state, prepare_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, prepare_event) + skipped_dispatch = DispatchRequestEvent( + **self._fields("dispatch-without-history-stage"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), + ) + prior = self.state + rejected = reduce_context_admission(prior, skipped_dispatch) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert replace(rejected.next_state, processed_events=prior.processed_events) == prior + self._accept_publication(rejected, skipped_dispatch) + stage_event = StageHistoryEvent( + **self._fields("stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.HISTORY_STAGED), + ) + transition = reduce_context_admission(self.state, stage_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, stage_event) + generation = next( + ( + record + for record in self.state.generation_reservations + if record.batch_id == batch.batch_id + ), + None, + ) + if generation is not None: + premature_generation = StartGenerationEvent( + **self._fields("generation-before-dispatch"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), + ) + prior = self.state + rejected = reduce_context_admission(prior, premature_generation) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert replace(rejected.next_state, processed_events=prior.processed_events) == prior + self._accept_publication(rejected, premature_generation) + dispatch_event = DispatchRequestEvent( + **self._fields("dispatch-request"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), + ) + transition = reduce_context_admission(self.state, dispatch_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, dispatch_event) + if not accept_now: + return + accept_event = AcceptInputEvent( + **self._fields("accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=reserved_input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-state-machine"), + representation_binding_witness=_binding(batch), + ) + transition = reduce_context_admission(self.state, accept_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, accept_event) + + @rule() + def prepare_and_mark_indeterminate(self) -> None: + if not self.charges: + return + slot = min(self.charges) + batch = self.batches.get(slot) + if batch is None: + return + reserved = self._find_batch(batch.batch_id) + if reserved is None or reserved.state is not AdmissionState.RESERVED: + return + input_count = self._reserved_input_count(batch) + if input_count is None: + return + prepare_event = PrepareBatchEvent( + **self._fields("prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-state-machine"), + ) + prepared = reduce_context_admission(self.state, prepare_event) + assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(prepared, prepare_event) + indeterminate_event = MarkIndeterminateEvent( + **self._fields("mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ) + marked = reduce_context_admission(self.state, indeterminate_event) + assert marked.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(marked, indeterminate_event) + + @rule(rollback=st.booleans()) + def release_or_rollback_preacceptance(self, rollback: bool) -> None: + for slot in sorted(self.charges): + occurrence = self.occurrences.get(slot) + if occurrence is None: + continue + batch = self.batches.get(slot) + if batch is None: + continue + record = self._find_batch(batch.batch_id) + if record is None: + continue + if rollback: + if record.state not in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + continue + event: Any = RollbackAdmissionEvent( + **self._fields("rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.ROLLBACK), + ) + else: + if record.state not in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + continue + event = ReleaseNonAdmissionEvent( + **self._fields("release-non-admission"), + batch_id=batch.batch_id, + witness=_witness( + batch, + occurrence, + WitnessKind.NON_ADMISSION, + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self._clear_batch_charges(batch) + return + + @rule(rollback=st.booleans()) + def resolve_indeterminate_nonacceptance(self, rollback: bool) -> None: + for slot in sorted(self.charges): + occurrence = self.occurrences.get(slot) + if occurrence is None: + continue + batch = self.batches.get(slot) + if batch is None: + continue + record = self._find_batch(batch.batch_id) + if record is None or record.state is not AdmissionState.INDETERMINATE: + continue + if rollback: + event: Any = ResolveIndeterminateRollbackEvent( + **self._fields("resolve-indeterminate-rollback"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.ROLLBACK), + ) + else: + event = ResolveIndeterminateNonAdmissionEvent( + **self._fields("resolve-indeterminate-non-admission"), + batch_id=batch.batch_id, + witness=_witness( + batch, + occurrence, + WitnessKind.NON_ADMISSION, + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self._clear_batch_charges(batch) + return + + @rule(exact_usage=st.integers(min_value=0, max_value=16)) + def start_and_reconcile_generation(self, exact_usage: int) -> None: + for generation in self.state.generation_reservations: + if generation.state is not GenerationState.RESERVED: + continue + slot_and_occurrence = next( + ( + (slot, occurrence) + for slot, occurrence in self.occurrences.items() + if ( + self.batches.get(slot) is not None + and self.batches[slot].batch_id == generation.batch_id + ) + ), + None, + ) + if slot_and_occurrence is None: + continue + slot, occurrence = slot_and_occurrence + batch = self.batches[slot] + record = self._find_batch(batch.batch_id) + if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: + continue + start = StartGenerationEvent( + **self._fields("start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness( + batch, + occurrence, + WitnessKind.REQUEST_INCLUDED, + ), + ) + started = reduce_context_admission(self.state, start) + assert started.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(started, start) + reconcile = ReconcileGenerationEvent( + **self._fields("reconcile-generation"), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness( + batch, + occurrence, + WitnessKind.OUTPUT_USAGE, + ), + exact_output_usage=exact_usage, + ) + reconciled = reduce_context_admission(self.state, reconcile) + expected = ( + AdmissionDecisionKind.QUARANTINED + if exact_usage > generation.maximum_allowance + else AdmissionDecisionKind.WOULD_ADMIT + ) + assert reconciled.decision.kind is expected + self._accept_publication(reconciled, reconcile) + self._clear_generation_charges(batch) + return + + @rule() + def expire_terminal_idempotency_key(self) -> None: + for record in self.state.idempotency_records: + batch = record.original_descriptor.batch + occurrence = next( + ( + occurrence + for occurrence in self.occurrences.values() + if occurrence.occurrence_id in batch.occurrence_ids + ), + None, + ) + batch_record = self._find_batch(batch.batch_id) + if ( + occurrence is None + or batch_record is None + or batch_record.state + not in { + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.QUARANTINED, + } + ): + continue + event = ExpireIdempotencyKeyEvent( + **self._fields("expire-idempotency-key"), + reservation_key=record.reservation_key, + expiry_witness=_witness( + batch, + occurrence, + WitnessKind.IDEMPOTENCY_EXPIRY, + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + return + + @rule() + def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: + before_charges = dict(self.charges) + retained_batch_ids = { + record.batch.batch_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + } + retained_slots = { + slot + for slot in self.occurrences + if ( + self.batches.get(slot) is not None + and self.batches[slot].batch_id in retained_batch_ids + ) + } + old_snapshot = self.state.snapshot + new_window_epoch_number = old_snapshot.window_epoch_number + 1 + new_window_epoch_id = WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}") + event_fields = self._fields("rollover-epoch") + witness_suffix = ( + f"{old_snapshot.window_epoch_number}-to-" + f"{new_window_epoch_number}-{self.event_sequence}" + ) + proof = EpochFenceProof( + old_window_epoch_id=old_snapshot.window_epoch_id, + old_window_epoch_number=old_snapshot.window_epoch_number, + new_window_epoch_id=new_window_epoch_id, + new_window_epoch_number=new_window_epoch_number, + receiver_authority_source_id=AuthoritySourceId("authority-state-machine"), + fence_witness_id=AdmissionWitnessId(f"fence-{witness_suffix}"), + highest_admitted_dispatch_sequence=sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == old_snapshot.window_epoch_id + and record.event.witness.window_epoch_number == old_snapshot.window_epoch_number + for record in self.state.processed_events + ), + ) + rollover_event = RolloverEpochEvent( + **event_fields, + witness=AdmissionWitness( + witness_id=AdmissionWitnessId(f"rollover-witness-{witness_suffix}"), + kind=WitnessKind.EPOCH_ROLLOVER, + window_epoch_id=old_snapshot.window_epoch_id, + window_epoch_number=old_snapshot.window_epoch_number, + snapshot_sequence=old_snapshot.snapshot_sequence, + request_id=AdmissionRequestId("rollover-request"), + batch_id=AdmissionBatchId("rollover-batch"), + representation_revision=RepresentationRevision("rollover-revision"), + representation_binding_id=RepresentationBindingId("rollover-binding"), + occurrence_ids=(), + authority_source_id=AuthoritySourceId("authority-state-machine"), + ), + fence_proof=proof, + new_snapshot=ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=new_window_epoch_id, + window_epoch_number=new_window_epoch_number, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ), + protected_pools=_pool_specs(), + ) + transition = reduce_context_admission(self.state, rollover_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + new_state = transition.next_state + assert isinstance(new_state, ActiveContextAdmissionState) + assert len(new_state.closed_epochs) >= 1 + prior_audit = new_state.closed_epochs[-1] + assert prior_audit.retained_unresolved_count == sum( + before_charges[slot][1] for slot in retained_slots + ) + for record in new_state.batch_records: + if record.state in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + }: + owner = record.batch.protected_pool_owner_id + if owner is None: + assert record.batch.reserve_class is ReserveClass.ORDINARY + self._accept_publication(transition, rollover_event) + self.occurrences = {} + self.batches = {} + self.charges = {} + self.last_rollover_retention = prior_audit.retained_unresolved_count + + @precondition(lambda self: self.latest_replayable_event is not None) + @rule() + def identical_replay_is_equivalent(self) -> None: + assert self.latest_replayable_event is not None + event_type = type(self.latest_replayable_event) + serialized_event = self.latest_replayable_event.to_dict() + restored_event = event_type.from_dict(serialized_event) + transition = reduce_context_admission(self.state, restored_event) + assert transition.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert transition.next_state == self.state + assert transition.effects == () + + @rule() + def generated_stream_replay_is_deterministic(self) -> None: + replay = replay_context_admission(_uninitialized(), tuple(self.events)) + assert replay.final_state == self.state + assert len(replay.transitions) == len(self.events) + assert [transition.effects for transition in replay.transitions] == self.published_effects + + @precondition(lambda self: self.latest_propose_event is not None) + @rule() + def changed_intent_conflict_does_not_mutate(self) -> None: + assert self.latest_propose_event is not None + changed = replace( + self.latest_propose_event, + occurrence=replace( + self.latest_propose_event.occurrence, + representation_revision=RepresentationRevision( + f"conflicting-{self.event_sequence}" + ), + ), + ) + transition = reduce_context_admission(self.state, changed) + assert transition.decision.kind is AdmissionDecisionKind.CONFLICT + assert transition.next_state == self.state + + @precondition(lambda self: bool(self.charges)) + @rule() + def reconciliation_deadline_never_releases(self) -> None: + slot = min(self.charges) + batch = self.batches[slot] + before = dict(self.charges) + event = RequestReconciliationEvent( + **self._fields("request-reconciliation"), + target_id=batch.batch_id, + reason_code="deadline-observed", + ) + transition = reduce_context_admission(self.state, event) + assert not any("Released" in type(effect).__name__ for effect in transition.effects) + self._accept_publication(transition, event) + assert self.charges == before + + @invariant() + def capacity_is_non_negative_and_protected_pools_are_isolated(self) -> None: + global_unallocated, ordinary_available, _ = self._availability(ReserveClass.ORDINARY) + assert global_unallocated >= 0 + assert ordinary_available >= 0 + for reserve_class in ( + ReserveClass.SYNTHESIS, + ReserveClass.FINAL_RESPONSE, + ): + _, _, pool_available = self._availability(reserve_class) + assert pool_available >= 0 + + @invariant() + def state_round_trip_is_canonical(self) -> None: + restored = ActiveContextAdmissionState.from_dict(self.state.to_dict()) + assert restored == self.state + assert restored.to_dict() == self.state.to_dict() + + @invariant() + def revisions_sequences_and_closed_audits_are_monotonic(self) -> None: + assert self.state.aggregate_revision.value >= self.last_revision + assert self.state.admission_sequence.value >= self.last_admission_sequence + assert len(self.state.closed_epochs) >= self.closed_audit_count + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + self.closed_audit_count = len(self.state.closed_epochs) + + @invariant() + def no_batch_is_partially_reserved_or_double_charged(self) -> None: + charged_occurrences = {self.occurrences[slot].occurrence_id for slot in self.charges} + assert len(charged_occurrences) == len(self.charges) + for record in self.state.batch_records: + member_states = { + occurrence_record.state + for occurrence_record in self.state.occurrence_records + if occurrence_record.occurrence.occurrence_id in record.batch.occurrence_ids + } + assert len(member_states) == 1 + assert member_states <= { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.INVALIDATED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + + +def test_multi_member_batch_progresses_through_full_lifecycle() -> None: + machine = ContextAdmissionStateMachine() + machine.propose(0, ReserveClass.SYNTHESIS) + machine.propose(1, ReserveClass.SYNTHESIS) + machine.reserve_multi_member_batch(0, 5) + batch = machine.batches[0] + assert machine.batches[1] == batch + + machine.prepare_stage_dispatch_and_accept(accept_now=True) + + record = machine._find_batch(batch.batch_id) + assert record is not None + assert record.state is AdmissionState.COMMITTED + assert { + occurrence_record.state + for occurrence_record in machine.state.occurrence_records + if occurrence_record.occurrence.occurrence_id in batch.occurrence_ids + } == {AdmissionState.COMMITTED} + + +def test_rollover_scopes_shared_request_generation_retention_to_batch() -> None: + machine = ContextAdmissionStateMachine() + machine.propose(0, ReserveClass.SYNTHESIS) + machine.propose(1, ReserveClass.SYNTHESIS) + machine.reserve(0, input_count=2, generation_count=4) + machine.reserve(1, input_count=2, generation_count=3) + retained_batch = machine.batches[0] + invalidated_batch = machine.batches[1] + invalidated_batch = replace( + invalidated_batch, + request_id=retained_batch.request_id, + manifest=replace( + invalidated_batch.manifest, + request_id=retained_batch.request_id, + ), + ) + machine.batches[1] = invalidated_batch + machine.state = replace( + machine.state, + batch_records=tuple( + replace(record, batch=invalidated_batch) + if record.batch.batch_id == invalidated_batch.batch_id + else record + for record in machine.state.batch_records + ), + generation_reservations=tuple( + replace(generation, request_id=retained_batch.request_id) + if generation.batch_id == invalidated_batch.batch_id + else generation + for generation in machine.state.generation_reservations + ), + ) + + machine.prepare_stage_dispatch_and_accept(accept_now=True) + machine.rollover_preserves_dispatched_and_indeterminate_charge() + + audit = machine.state.closed_epochs[-1] + assert audit.retained_generation_count == 4 + assert tuple(generation.batch_id for generation in audit.terminal_generation_reservations) == ( + retained_batch.batch_id, + ) + assert ( + sum( + isinstance(effect, ReservationInvalidatedEffect) + for effect in machine.published_effects[-1] + ) + == 2 + ) + + +ContextAdmissionStateMachine.TestCase.settings = settings( + max_examples=24, + stateful_step_count=12, + deadline=None, +) +TestContextAdmissionStateMachine = ContextAdmissionStateMachine.TestCase diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py new file mode 100644 index 000000000..a9a8a50bb --- /dev/null +++ b/tests/core/types/test_context_admission_contract.py @@ -0,0 +1,1867 @@ +"""Freeze the protocol-v1 cumulative context-admission value contract.""" + +from __future__ import annotations + +import dataclasses +from dataclasses import FrozenInstanceError, fields, replace +from typing import get_args + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatch, + AdmissionBatchId, + AdmissionBatchRecord, + AdmissionDecision, + AdmissionDecisionKind, + AdmissionEffect, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionOccurrenceRecord, + AdmissionReplay, + AdmissionRequestId, + AdmissionReservation, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionTransition, + AdmissionWitness, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + AuthoritySourceId, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ChargeCommittedEffect, + ChargeDomain, + ClosedEpochAudit, + ContextAdmissionEvent, + ContextAdmissionState, + ContextAdmissionValidationError, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + CoverageEvidence, + CoverageEvidenceKind, + CoverageState, + DeliveryOccurrenceId, + DispatchIdentity, + EpochFenceProof, + ExpiredIdempotencyTombstone, + ForkOccurrenceId, + GenerationReservationId, + GenerationReservationRecord, + GenerationState, + IdempotencyNamespace, + IdempotencyRecord, + MeasurementKind, + ModelIdentity, + ModelItemId, + PrepareBatchEvent, + ProcessedEventRecord, + ProducerCoverageDef, + ProducerInstanceId, + ProducerSurface, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + RepresentationBindingId, + RepresentationBindingWitness, + RepresentationRevision, + ReservationRecordedEffect, + ReserveClass, + ReserveRequestEvent, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + UnsupportedContextAdmissionProtocolError, + WindowEpochId, + WitnessKind, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +EXPECTED_ENUM_MEMBERS = { + AdmissionState: ( + "PROPOSED", + "RESERVED", + "PREPARED", + "HISTORY_STAGED", + "REQUEST_DISPATCHED", + "COMMITTED", + "RELEASED", + "ROLLED_BACK", + "INVALIDATED", + "INDETERMINATE", + "QUARANTINED", + ), + AdmissionDecisionKind: ( + "WOULD_ADMIT", + "WOULD_REJECT", + "WATERMARK_UNAVAILABLE", + "UPSTREAM_GATED", + "NOOP_IDEMPOTENT", + "CONFLICT", + "IDEMPOTENCY_EXPIRED", + "QUARANTINED", + ), + ChargeDomain: ("INPUT_CONTEXT", "OUTPUT_GENERATION"), + GenerationState: ( + "RESERVED", + "STREAMING", + "RECONCILED", + "INDETERMINATE", + "QUARANTINED", + ), + MeasurementKind: ( + "PROVIDER_EXACT", + "TOKENIZER_EXACT", + "HOST_ESTIMATE", + "BYTE_EMERGENCY", + ), + CoverageState: ("VERIFIED", "PARTIAL", "UPSTREAM_GATED"), + CoverageEvidenceKind: ( + "AUTOSKILLIT_SOURCE", + "CODEX_SOURCE", + "CODEX_OFFICIAL_DOC", + "CODEX_RUNTIME_PROBE", + "INFERENCE", + ), + ReserveClass: ("ORDINARY", "SYNTHESIS", "FINAL_RESPONSE"), + WitnessKind: ( + "EPOCH_SNAPSHOT", + "INPUT_COUNTED", + "HISTORY_STAGED", + "REPRESENTATION_BOUND", + "REQUEST_INCLUDED", + "PROVIDER_ACCEPTED", + "OUTPUT_USAGE", + "TRUNCATION", + "NON_ADMISSION", + "ROLLBACK", + "RECONCILIATION", + "IDEMPOTENCY_EXPIRY", + "EPOCH_FENCE", + "EPOCH_ROLLOVER", + ), +} + +EXPECTED_PRODUCER_SURFACES = ( + "NATIVE_SHELL", + "UNIFIED_EXEC_AND_WRITE_STDIN", + "APPLY_PATCH", + "AUTOSKILLIT_MCP", + "EXTERNAL_MCP", + "AUTOSKILLIT_LOCAL_FUNCTION", + "OTHER_LOCAL_FUNCTION", + "MCP_RESOURCE", + "CLIENT_PROVIDER_RETRIEVAL", + "CODE_MODE_AGGREGATE", + "HOSTED_SPECIALIZED_TOOL", + "HOOK_FEEDBACK", + "TOOL_ARGUMENT", + "TOOL_RESULT_ENVELOPE", + "USER_PROMPT", + "ASSISTANT_OUTPUT_HISTORY", + "SKILL_PLUGIN_CONTEXT", + "OTHER_CONTEXT_INJECTION", + "HEADLESS_CHILD_PROMPT", + "PARENT_VISIBLE_CHILD_DELIVERY", + "COMPACTION_MODEL_WINDOW_TRANSITION", +) + +EXPECTED_EVENT_TYPES = { + "OpenEpochEvent", + "AuthorityUnavailableEvent", + "ProposeOccurrenceEvent", + "ReserveRequestEvent", + "PrepareBatchEvent", + "StageHistoryEvent", + "DispatchRequestEvent", + "AcceptInputEvent", + "ReleaseNonAdmissionEvent", + "RollbackAdmissionEvent", + "MarkIndeterminateEvent", + "ResolveIndeterminateAcceptedEvent", + "ResolveIndeterminateNonAdmissionEvent", + "ResolveIndeterminateRollbackEvent", + "StartGenerationEvent", + "ReconcileGenerationEvent", + "MarkGenerationIndeterminateEvent", + "RequestReconciliationEvent", + "ExpireIdempotencyKeyEvent", + "RolloverEpochEvent", +} + +EXPECTED_EFFECT_TYPES = { + "ReservationRecordedEffect", + "ReservationReleasedEffect", + "OccurrenceStateChangedEffect", + "ChargeCommittedEffect", + "GenerationReservationRecordedEffect", + "GenerationReconciledEffect", + "ReconciliationQueryRequestedEffect", + "ReconciliationEscalationEffect", + "ConflictRejectedEffect", + "IdempotencyExpiredEffect", + "ReservationInvalidatedEffect", + "EpochClosedEffect", + "QuarantineRecordedEffect", + "AuthorityUnavailableEffect", +} + +_EVENT_BASE_FIELDS = ( + "event_id", + "protocol_version", + "idempotency_namespace", + "expected_aggregate_revision", +) + +EXPECTED_EVENT_FIELDS = { + "OpenEpochEvent": _EVENT_BASE_FIELDS + ("snapshot", "protected_pools"), + "AuthorityUnavailableEvent": _EVENT_BASE_FIELDS + ("reason_code", "authority_state"), + "ProposeOccurrenceEvent": _EVENT_BASE_FIELDS + ("occurrence",), + "ReserveRequestEvent": _EVENT_BASE_FIELDS + + ( + "batch", + "snapshot_sequence", + "input_reservations", + "generation_reservation", + ), + "PrepareBatchEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "representation_revision", + "representation_binding_id", + "proposed_charge", + "measurement_kind", + "authority_source", + ), + "StageHistoryEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "DispatchRequestEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "AcceptInputEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "witness", + "final_manifest_revision", + "final_manifest", + "exact_input_charge", + "measurement_kind", + "authority_source", + "representation_binding_witness", + ), + "ReleaseNonAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "RollbackAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "MarkIndeterminateEvent": _EVENT_BASE_FIELDS + ("batch_id", "reason_code"), + "ResolveIndeterminateAcceptedEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "witness", + "final_manifest_revision", + "final_manifest", + "exact_charge", + "measurement_kind", + "authority_source", + "representation_binding_witness", + ), + "ResolveIndeterminateNonAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "ResolveIndeterminateRollbackEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "StartGenerationEvent": _EVENT_BASE_FIELDS + ("generation_reservation_id", "witness"), + "ReconcileGenerationEvent": _EVENT_BASE_FIELDS + + ("generation_reservation_id", "output_usage_witness", "exact_output_usage"), + "MarkGenerationIndeterminateEvent": _EVENT_BASE_FIELDS + + ("generation_reservation_id", "reason_code"), + "RequestReconciliationEvent": _EVENT_BASE_FIELDS + ("target_id", "reason_code"), + "ExpireIdempotencyKeyEvent": _EVENT_BASE_FIELDS + ("reservation_key", "expiry_witness"), + "RolloverEpochEvent": _EVENT_BASE_FIELDS + + ( + "witness", + "fence_proof", + "new_snapshot", + "protected_pools", + ), +} + +_EFFECT_BASE_FIELDS = ( + "source_event_id", + "resulting_aggregate_revision", + "resulting_admission_sequence", + "target_id", +) +_CHARGE_EFFECT_FIELDS = _EFFECT_BASE_FIELDS + ( + "charge_domain", + "reserve_class", + "protected_pool_owner_id", + "count", + "window_epoch_id", + "snapshot_sequence", + "witness_ids", +) + +EXPECTED_EFFECT_FIELDS = { + "ReservationRecordedEffect": _CHARGE_EFFECT_FIELDS, + "ReservationReleasedEffect": _CHARGE_EFFECT_FIELDS, + "OccurrenceStateChangedEffect": _EFFECT_BASE_FIELDS + ("previous_state", "next_state"), + "ChargeCommittedEffect": _CHARGE_EFFECT_FIELDS, + "GenerationReservationRecordedEffect": _CHARGE_EFFECT_FIELDS, + "GenerationReconciledEffect": _CHARGE_EFFECT_FIELDS, + "ReconciliationQueryRequestedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "ReconciliationEscalationEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "ConflictRejectedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "IdempotencyExpiredEffect": _EFFECT_BASE_FIELDS + ("reservation_key", "expiry_witness_id"), + "ReservationInvalidatedEffect": _CHARGE_EFFECT_FIELDS, + "EpochClosedEffect": _EFFECT_BASE_FIELDS + ("fence_proof", "deducted_unresolved_count"), + "QuarantineRecordedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "AuthorityUnavailableEffect": _EFFECT_BASE_FIELDS + ("reason_code", "authority_state"), +} + +EXPECTED_RECORD_FIELDS = { + CanonicalSpanOwner: ("span_id", "occurrence_id"), + CanonicalRepresentationManifest: ( + "request_id", + "representation_revision", + "representation_binding_id", + "span_owners", + "assembler_identity", + "assembler_witness_id", + ), + AdmissionOccurrence: ( + "occurrence_id", + "lineage", + "reserve_class", + "producer_surface", + "predicted_authoritative_maximum", + "representation_revision", + "owned_span_ids", + ), + AdmissionBatch: ( + "batch_id", + "request_id", + "occurrence_ids", + "reserve_class", + "protected_pool_owner_id", + "manifest", + ), + AdmissionReservationKey: ( + "idempotency_namespace", + "protocol_version", + "window_epoch_id", + "window_epoch_number", + "batch_id", + "reserve_class", + "protected_pool_owner_id", + "occurrence_revisions", + ), + AdmissionReservation: ( + "reservation_id", + "key", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "reserve_class", + "protected_pool_owner_id", + "occurrence_ids", + "reserved_count", + ), + AdmissionWitness: ( + "witness_id", + "kind", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "request_id", + "batch_id", + "representation_revision", + "representation_binding_id", + "occurrence_ids", + "authority_source_id", + ), + RepresentationBindingWitness: ( + "counted_representation_revision", + "dispatched_representation_revision", + "final_manifest_revision", + "representation_binding_id", + "request_id", + "batch_id", + "authority_source_id", + ), + EpochFenceProof: ( + "old_window_epoch_id", + "old_window_epoch_number", + "new_window_epoch_id", + "new_window_epoch_number", + "receiver_authority_source_id", + "fence_witness_id", + "highest_admitted_dispatch_sequence", + ), + ProtectedPoolSpec: ( + "reserve_class", + "capability_owner_id", + "injected_count", + "priority", + "required_release_witness_kind", + ), + AdmissionOccurrenceRecord: ( + "occurrence", + "state", + "batch_id", + "reservation_id", + "accepted_witness_ids", + "indeterminate_reason_code", + "quarantine_reason_code", + ), + AdmissionBatchRecord: ( + "batch", + "state", + "reservation_id", + "witness_ids", + "committed_input_count", + "unresolved_input_count", + ), + GenerationReservationRecord: ( + "generation_reservation_id", + "request_id", + "batch_id", + "representation_revision", + "occurrence_ids", + "response_id", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "reserve_class", + "protected_pool_owner_id", + "maximum_allowance", + "state", + "exact_terminal_usage", + "witness_ids", + "authority_source_id", + ), + ExpiredIdempotencyTombstone: ( + "namespace", + "reservation_key", + "original_descriptor", + "expiry_witness", + "original_terminal_decision", + ), + ClosedEpochAudit: ( + "snapshot", + "terminal_occurrence_records", + "terminal_batch_records", + "terminal_reservations", + "terminal_generation_reservations", + "closure_witness_id", + "fence_proof", + "processed_event_tombstones", + "retained_unresolved_count", + "retained_generation_count", + ), + ProcessedEventRecord: ( + "event_id", + "event", + "original_decision", + "aggregate_revision", + "admission_sequence", + ), + IdempotencyRecord: ( + "namespace", + "reservation_key", + "original_descriptor", + "original_reserve_decision", + "owning_event_id", + "publication_revision", + ), + CoverageEvidence: ( + "claim_id", + "kind", + "backend", + "configuration_mode", + "verifier", + "source_locator", + "tested_version", + "tested_revision", + "checked_at", + "freshness_policy", + ), + ProducerCoverageDef: ( + "surface", + "control_point_owner", + "observation_state", + "authority_state", + "evidence", + "reason_code", + ), + AdmissionReplay: ("final_state", "transitions"), +} + +OPAQUE_STRING_TYPES = ( + ContextSessionId, + AgentInstanceId, + ContextThreadId, + ForkOccurrenceId, + TurnId, + ProducerInstanceId, + ToolCallId, + ModelItemId, + AdmissionRequestId, + AdmissionBatchId, + WindowEpochId, + TokenizerIdentity, + CanonicalSpanId, + AdmissionOccurrenceId, + AdmissionAttemptId, + DeliveryOccurrenceId, + AdmissionEventId, + AdmissionReservationId, + AdmissionWitnessId, + AuthoritySourceId, + GenerationReservationId, + ProtectedPoolOwnerId, + RepresentationRevision, + RepresentationBindingId, +) + + +def _field_names(value_type: type[object]) -> tuple[str, ...]: + return tuple(field.name for field in fields(value_type)) + + +def _lineage( + *, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + span_suffix: str = "1", + dispatch_identity: DispatchIdentity | None = None, +) -> ContextLineage: + return ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId("session-current"), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId("agent-current"), + parent_agent_id=None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId("thread-current"), + parent_thread_id=None, + fork_occurrence_id=None, + turn_id=TurnId("turn-1"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{span_suffix}"), + tool_call_id=ToolCallId(f"tool-{span_suffix}"), + model_item_id=ModelItemId(f"item-{span_suffix}"), + dispatch_identity=dispatch_identity, + attempt_id=AdmissionAttemptId(f"attempt-{span_suffix}"), + delivery_occurrence_id=None, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + ) + + +def _occurrence( + occurrence_id: str = "occurrence-1", + *, + span_id: str = "span-1", + revision: str = "representation-1", + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, +) -> AdmissionOccurrence: + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(occurrence_id), + lineage=_lineage(surface=surface, span_suffix=occurrence_id), + reserve_class=ReserveClass.ORDINARY, + producer_surface=surface, + predicted_authoritative_maximum=7, + representation_revision=RepresentationRevision(revision), + owned_span_ids=(CanonicalSpanId(span_id),), + ) + + +def _manifest(*occurrences: AdmissionOccurrence) -> CanonicalRepresentationManifest: + return CanonicalRepresentationManifest( + request_id=AdmissionRequestId("request-1"), + representation_revision=RepresentationRevision("representation-1"), + representation_binding_id=RepresentationBindingId("binding-1"), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId("assembler-1"), + assembler_witness_id=AdmissionWitnessId("assembler-witness-1"), + ) + + +def _batch(batch_id: str, occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: + return AdmissionBatch( + batch_id=AdmissionBatchId(batch_id), + request_id=AdmissionRequestId("request-1"), + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + manifest=_manifest(*occurrences), + ) + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="test-caller", operation_kind=operation_kind) + + +def _witness( + batch: AdmissionBatch, + kind: WitnessKind, + *, + witness: str | None = None, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(witness or f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + occurrence_ids=batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: + bound_revision = batch.manifest.representation_revision + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _snapshot( + *, + protocol_version: int = CONTEXT_ADMISSION_PROTOCOL_VERSION, + model_identity: ModelIdentity | None = None, +) -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=protocol_version, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + model_identity=model_identity or ModelIdentity.anthropic("claude-test"), + tokenizer_identity=TokenizerIdentity("tokenizer-test"), + snapshot_sequence=1, + active_count=60, + hard_limit=100, + remaining_count=40, + ) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +@pytest.mark.parametrize(("enum_type", "expected"), EXPECTED_ENUM_MEMBERS.items()) +def test_protocol_v1_enum_members_are_exact( + enum_type: type[object], expected: tuple[str, ...] +) -> None: + assert tuple(enum_type.__members__) == expected + assert len(tuple(enum_type)) == len(enum_type.__members__) + + +def test_producer_surface_is_the_independently_frozen_literal_set() -> None: + assert tuple(ProducerSurface.__members__) == EXPECTED_PRODUCER_SURFACES + + +def test_event_and_effect_unions_are_closed() -> None: + assert {event_type.__name__ for event_type in get_args(ContextAdmissionEvent)} == ( + EXPECTED_EVENT_TYPES + ) + assert {effect_type.__name__ for effect_type in get_args(AdmissionEffect)} == ( + EXPECTED_EFFECT_TYPES + ) + assert set(get_args(ContextAdmissionState)) == { + UninitializedContextAdmissionState, + ActiveContextAdmissionState, + } + + +def test_every_event_effect_and_record_field_set_is_exact() -> None: + event_types = { + event_type.__name__: event_type for event_type in get_args(ContextAdmissionEvent) + } + effect_types = {effect_type.__name__: effect_type for effect_type in get_args(AdmissionEffect)} + assert set(event_types) == set(EXPECTED_EVENT_FIELDS) + assert set(effect_types) == set(EXPECTED_EFFECT_FIELDS) + for type_name, expected_fields in EXPECTED_EVENT_FIELDS.items(): + assert _field_names(event_types[type_name]) == expected_fields + for type_name, expected_fields in EXPECTED_EFFECT_FIELDS.items(): + assert _field_names(effect_types[type_name]) == expected_fields + for record_type, expected_fields in EXPECTED_RECORD_FIELDS.items(): + assert _field_names(record_type) == expected_fields + + +@pytest.mark.parametrize( + ("record_type", "expected_fields"), + [ + ( + UninitializedContextAdmissionState, + ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "processed_events", + "idempotency_records", + "expired_idempotency_tombstones", + "closed_epochs", + ), + ), + ( + ActiveContextAdmissionState, + ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "snapshot", + "protected_pools", + "occurrence_records", + "batch_records", + "reservations", + "generation_reservations", + "processed_events", + "idempotency_records", + "expired_idempotency_tombstones", + "closed_epochs", + ), + ), + ( + AdmissionDecision, + ( + "kind", + "reason_code", + "window_epoch_id", + "snapshot_sequence", + "requested_count", + "available_ordinary_count", + "available_protected_count", + ), + ), + (AdmissionTransition, ("next_state", "decision", "effects")), + ], +) +def test_frozen_record_fields_are_exact( + record_type: type[object], expected_fields: tuple[str, ...] +) -> None: + assert _field_names(record_type) == expected_fields + + +def test_lineage_fields_keep_all_identity_domains_separate() -> None: + assert _field_names(ContextLineage) == ( + "root_session_id", + "current_session_id", + "root_agent_id", + "current_agent_id", + "parent_agent_id", + "root_thread_id", + "current_thread_id", + "parent_thread_id", + "fork_occurrence_id", + "turn_id", + "producer_surface", + "producer_instance_id", + "tool_call_id", + "model_item_id", + "dispatch_identity", + "attempt_id", + "delivery_occurrence_id", + "window_epoch_id", + "window_epoch_number", + ) + + +@pytest.mark.parametrize("opaque_type", OPAQUE_STRING_TYPES) +def test_opaque_string_identifiers_are_frozen_content_free_values( + opaque_type: type[object], +) -> None: + value = opaque_type("opaque-123") + assert value.value == "opaque-123" + assert value.from_dict(value.to_dict()) == value + with pytest.raises((FrozenInstanceError, AttributeError)): + value.value = "changed" + + +@pytest.mark.parametrize( + "invalid_identifier", + [ + "a" * 97, + "-leading-segment", + "trailing-segment-", + "contains+unsupported", + "contains@unsupported", + ], +) +def test_opaque_identifiers_enforce_protocol_shape(invalid_identifier: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionEventId(invalid_identifier) + assert "invalid_opaque_identifier" in str(exc_info.value) + + +@pytest.mark.parametrize("revision_type", [AggregateRevision, AdmissionSequence]) +def test_numeric_revisions_are_non_negative_and_have_no_default( + revision_type: type[object], +) -> None: + assert revision_type(0).value == 0 + assert revision_type((1 << 64) - 1).value == (1 << 64) - 1 + with pytest.raises(ContextAdmissionValidationError): + revision_type(-1) + with pytest.raises(ContextAdmissionValidationError): + revision_type(1 << 64) + assert all(field.default is dataclasses.MISSING for field in fields(revision_type)) + + +def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: + identity = DispatchIdentity.from_dispatch_id("12345678-1234-1234-1234-123456789abc") + lineage = _lineage(surface=ProducerSurface.NATIVE_SHELL, dispatch_identity=identity) + serialized = lineage.to_dict() + assert serialized["dispatch_identity"] == {"dispatch_id": identity.dispatch_id} + assert ContextLineage.from_dict(serialized) == lineage + + forged = object.__new__(DispatchIdentity) + for identity_field in fields(identity): + object.__setattr__(forged, identity_field.name, getattr(identity, identity_field.name)) + object.__setattr__(forged, "completion_marker", "forged-private-marker") + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(lineage, dispatch_identity=forged) + assert "forged-private-marker" not in str(exc_info.value) + + object.__setattr__(identity, "completion_marker", "tampered-after-construction") + with pytest.raises(ContextAdmissionValidationError) as serialization_error: + lineage.to_dict() + assert "tampered-after-construction" not in str(serialization_error.value) + + +@pytest.mark.parametrize( + ("tagged_value", "reason_code"), + [ + ( + { + "__enum__": "ReserveClass", + "value": "ordinary", + "unexpected": "ignored", + }, + "unknown_serialized_enum", + ), + ({"__tuple__": [], "unexpected": "ignored"}, "invalid_serialized_tuple"), + ( + {"__frozenset__": [], "unexpected": "ignored"}, + "invalid_serialized_frozenset", + ), + ], +) +def test_tagged_serialization_requires_exact_key_sets( + tagged_value: dict[str, object], + reason_code: str, +) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict({"value": tagged_value}) + assert reason_code in str(exc_info.value) + + +def test_invalid_serialized_enum_suppresses_attacker_controlled_cause() -> None: + canary = "private-enum-value" + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict( + { + "value": { + "__enum__": "ReserveClass", + "value": canary, + } + } + ) + + assert exc_info.value.__cause__ is None + assert exc_info.value.__suppress_context__ + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + +def test_invalid_serialized_contract_suppresses_attacker_controlled_cause() -> None: + canary = "private-attacker-field" + serialized = ContextSessionId("session-1").to_dict() + serialized[canary] = "private-attacker-value" + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict(serialized) + + assert exc_info.value.__cause__ is None + assert exc_info.value.__suppress_context__ + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + +@pytest.mark.parametrize( + "field_name", + ["configured_model", "effective_model", "profile_name"], +) +def test_model_identity_deserialization_requires_string_fields(field_name: str) -> None: + encoded_identity: dict[str, object] = { + "__type__": "ModelIdentity", + "configured_model": "claude-test", + "effective_model": "claude-test", + "profile_name": "default", + } + encoded_identity[field_name] = 42 + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict({"value": encoded_identity}) + + assert "invalid_model_identity" in str(exc_info.value) + + +@pytest.mark.parametrize( + "canary", + [ + "", + "/home/alice/private/context.txt", + "Bearer-secret-token", + "dispatch-private-user-content", + f"sha256:{'a' * 64}", + ], +) +def test_dispatch_identity_rejects_non_uuid_content_without_echoing_it(canary: str) -> None: + with pytest.raises(ValueError) as exc_info: + DispatchIdentity.from_dispatch_id(canary) + if canary: + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + +def test_annotated_protocol_fields_reject_raw_values_and_invalid_effect_members() -> None: + occurrence = _occurrence() + batch = _batch("batch-typed-fields", (occurrence,)) + witness = _witness(batch, WitnessKind.PROVIDER_ACCEPTED) + decision = AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_ADMIT, + reason_code="accepted", + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=1, + available_ordinary_count=1, + available_protected_count=0, + ) + transition = AdmissionTransition( + next_state=_uninitialized(), + decision=decision, + effects=(), + ) + malformed_fields = ( + (decision, "kind", "would_admit"), + (occurrence, "reserve_class", "ordinary"), + (batch, "batch_id", "batch-raw"), + (witness, "kind", "provider_accepted"), + (witness, "occurrence_ids", ("occurrence-raw",)), + (batch.manifest, "span_owners", ("span-owner-raw",)), + (transition, "effects", ("effect-raw",)), + ) + for value, field_name, malformed in malformed_fields: + with pytest.raises(ContextAdmissionValidationError): + replace(value, **{field_name: malformed}) + + serialized_decision = decision.to_dict() + serialized_decision["kind"] = "unknown-decision" + with pytest.raises(ContextAdmissionValidationError): + AdmissionDecision.from_dict(serialized_decision) + + +@pytest.mark.parametrize( + "canary", + [ + "/srv/private/representation", + "Bearer-private-capability", + f"sha256:{'a' * 64}", + ], +) +def test_representation_binding_identity_is_opaque_and_content_free(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + RepresentationBindingId(canary) + assert canary not in str(exc_info.value) + + +def test_authoritative_snapshot_reuses_only_known_model_identity() -> None: + snapshot = _snapshot() + assert ContextWindowSnapshot.from_dict(snapshot.to_dict()) == snapshot + assert snapshot.model_identity == ModelIdentity.anthropic("claude-test") + with pytest.raises(ContextAdmissionValidationError): + _snapshot(model_identity=ModelIdentity.unknown()) + + +def test_uninitialized_state_is_explicitly_non_spendable() -> None: + state = _uninitialized() + assert not isinstance(state, ActiveContextAdmissionState) + assert not hasattr(state, "snapshot") + assert not hasattr(state, "reservations") + assert UninitializedContextAdmissionState.from_dict(state.to_dict()) == state + + +def test_reservation_key_is_attempt_independent_and_revision_sensitive() -> None: + key = AdmissionReservationKey( + idempotency_namespace=IdempotencyNamespace( + caller_scope="caller-1", operation_kind="reserve-request" + ), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + batch_id=AdmissionBatchId("batch-1"), + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + occurrence_revisions=( + ( + AdmissionOccurrenceId("occurrence-1"), + RepresentationRevision("representation-1"), + ), + ), + ) + assert "attempt" not in _field_names(AdmissionReservationKey) + assert key.from_dict(key.to_dict()) == key + assert ( + replace( + key, + occurrence_revisions=( + ( + AdmissionOccurrenceId("occurrence-1"), + RepresentationRevision("representation-2"), + ), + ), + ) + != key + ) + + +def test_protected_pool_policy_is_injected_and_has_no_borrowing_or_defaults() -> None: + assert _field_names(ProtectedPoolSpec) == ( + "reserve_class", + "capability_owner_id", + "injected_count", + "priority", + "required_release_witness_kind", + ) + assert "borrow" not in " ".join(_field_names(ProtectedPoolSpec)) + assert all(field.default is dataclasses.MISSING for field in fields(ProtectedPoolSpec)) + with pytest.raises(ContextAdmissionValidationError): + ProtectedPoolSpec( + reserve_class=ReserveClass.ORDINARY, + capability_owner_id=ProtectedPoolOwnerId("ordinary-must-not-own-a-pool"), + injected_count=1, + priority=1, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("owner-unsupported-witness"), + injected_count=10, + priority=1, + required_release_witness_kind=WitnessKind.PROVIDER_ACCEPTED, + ) + assert "invalid_protected_release_witness_kind" in str(exc_info.value) + + +def test_manifest_rejects_overlapping_span_ownership() -> None: + tool_argument = _occurrence( + "tool-argument", + span_id="shared-span", + surface=ProducerSurface.TOOL_ARGUMENT, + ) + assistant_history = _occurrence( + "assistant-history", + span_id="shared-span", + surface=ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ) + with pytest.raises(ContextAdmissionValidationError): + _manifest(tool_argument, assistant_history) + + +def test_contract_values_are_deeply_immutable() -> None: + occurrence = _occurrence() + assert isinstance(occurrence.owned_span_ids, tuple) + with pytest.raises((FrozenInstanceError, AttributeError)): + occurrence.predicted_authoritative_maximum = 100 + + def assert_no_mutable_collections(value: object) -> None: + if dataclasses.is_dataclass(value): + for field in fields(value): + assert_no_mutable_collections(getattr(value, field.name)) + elif isinstance(value, tuple | frozenset): + for child in value: + assert_no_mutable_collections(child) + else: + assert not isinstance(value, list | dict | set) + + assert_no_mutable_collections(_manifest(occurrence)) + + +@pytest.mark.parametrize( + "canary", + [ + "payload: private user message", + "/home/alice/private/context.txt", + "Bearer secret-token-value", + f"sha256:{'a' * 64}", + ], +) +def test_privacy_canaries_never_escape_validation_repr_or_serialization(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId(canary) + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + safe = _lineage() + rendered = repr(safe) + serialized = repr(safe.to_dict()) + assert canary not in rendered + assert canary not in serialized + + +@pytest.mark.parametrize("opaque_type", OPAQUE_STRING_TYPES) +def test_unprefixed_digest_shaped_values_are_rejected_by_every_opaque_wrapper( + opaque_type: type[object], +) -> None: + digest = "a" * 64 + with pytest.raises(ContextAdmissionValidationError) as exc_info: + opaque_type(digest) + assert digest not in str(exc_info.value) + assert digest not in repr(exc_info.value) + + +@pytest.mark.parametrize( + "canary", + [ + "private user message", + "secret-value", + "token=private-value", + "/srv/private/model", + ], +) +def test_free_text_and_model_identity_privacy_is_fail_closed(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError): + AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_REJECT, + reason_code=canary, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=0, + available_ordinary_count=0, + available_protected_count=0, + ) + with pytest.raises(ContextAdmissionValidationError): + _snapshot(model_identity=ModelIdentity.anthropic(canary)) + + +@pytest.mark.parametrize( + "invalid_reason_code", + [ + "Uppercase", + "under_score", + "dot.value", + "colon:value", + ], +) +def test_reason_codes_require_lowercase_kebab_case(invalid_reason_code: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_REJECT, + reason_code=invalid_reason_code, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=0, + available_ordinary_count=0, + available_protected_count=0, + ) + assert "invalid_reason_code" in str(exc_info.value) + + +def test_aggregate_tuples_reject_noncanonical_ordering() -> None: + first = _occurrence("occurrence-a", span_id="span-a") + second = _occurrence("occurrence-b", span_id="span-b") + with pytest.raises(ContextAdmissionValidationError): + CanonicalRepresentationManifest( + request_id=AdmissionRequestId("request-1"), + representation_revision=RepresentationRevision("representation-1"), + representation_binding_id=RepresentationBindingId("binding-overlap"), + span_owners=( + CanonicalSpanOwner( + span_id=second.owned_span_ids[0], + occurrence_id=second.occurrence_id, + ), + CanonicalSpanOwner( + span_id=first.owned_span_ids[0], + occurrence_id=first.occurrence_id, + ), + ), + assembler_identity=ProducerInstanceId("assembler-1"), + assembler_witness_id=AdmissionWitnessId("assembler-witness-1"), + ) + + records = ( + AdmissionOccurrenceRecord( + occurrence=first, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ), + AdmissionOccurrenceRecord( + occurrence=second, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ), + ) + state = ActiveContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + snapshot=_snapshot(), + protected_pools=(), + occurrence_records=records, + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(state, occurrence_records=tuple(reversed(records))) + + +def test_active_state_rejects_inconsistent_bidirectional_ownership_links() -> None: + occurrence = _occurrence("graph-member", span_id="graph-span") + batch = _batch("batch-graph", (occurrence,)) + proposed = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + state = ActiveContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + snapshot=_snapshot(), + protected_pools=(), + occurrence_records=(proposed,), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + orphan_batch = AdmissionBatchRecord( + batch=batch, + state=AdmissionState.RESERVED, + reservation_id=None, + witness_ids=(), + committed_input_count=0, + unresolved_input_count=0, + ) + with pytest.raises(ContextAdmissionValidationError): + replace(state, batch_records=(orphan_batch,)) + with pytest.raises(ContextAdmissionValidationError): + replace( + state, + occurrence_records=( + replace( + proposed, + state=AdmissionState.RESERVED, + batch_id=AdmissionBatchId("missing-batch"), + ), + ), + ) + + +def test_charge_effects_validate_exact_target_domain_owner_and_counts() -> None: + effect = ReservationRecordedEffect( + source_event_id=AdmissionEventId("event-1"), + resulting_aggregate_revision=AggregateRevision(1), + resulting_admission_sequence=AdmissionSequence(1), + target_id=AdmissionReservationId("reservation-1"), + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + count=3, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + witness_ids=(), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, target_id=AdmissionBatchId("batch-1")) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, charge_domain=ChargeDomain.OUTPUT_GENERATION) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, count=-1) + with pytest.raises(ContextAdmissionValidationError): + replace( + effect, + protected_pool_owner_id=ProtectedPoolOwnerId("ordinary-owner"), + ) + + committed = ChargeCommittedEffect( + source_event_id=AdmissionEventId("event-2"), + resulting_aggregate_revision=AggregateRevision(2), + resulting_admission_sequence=AdmissionSequence(2), + target_id=AdmissionBatchId("batch-1"), + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + count=3, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + witness_ids=(AdmissionWitnessId("witness-1"),), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(committed, target_id=AdmissionReservationId("reservation-1")) + + +def test_unknown_protocol_versions_fail_closed() -> None: + assert CONTEXT_ADMISSION_PROTOCOL_VERSION == 1 + with pytest.raises(UnsupportedContextAdmissionProtocolError): + replace(_uninitialized(), protocol_version=2) + with pytest.raises(UnsupportedContextAdmissionProtocolError): + _snapshot(protocol_version=2) + + +def test_closed_epoch_audits_survive_state_serialization() -> None: + empty_audit = ClosedEpochAudit( + snapshot=_snapshot(), + terminal_occurrence_records=(), + terminal_batch_records=(), + terminal_reservations=(), + terminal_generation_reservations=(), + closure_witness_id=AdmissionWitnessId("closure-witness-1"), + fence_proof=None, + processed_event_tombstones=(), + retained_unresolved_count=0, + retained_generation_count=0, + ) + state = replace(_uninitialized(), closed_epochs=(empty_audit,)) + restored = UninitializedContextAdmissionState.from_dict(state.to_dict()) + assert restored == state + assert restored.closed_epochs == (empty_audit,) + + +def test_closed_epoch_audit_rejects_excessive_occurrence_cardinality() -> None: + occurrence = _occurrence("occurrence-cardinality", span_id="span-cardinality") + record = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ClosedEpochAudit( + snapshot=_snapshot(), + terminal_occurrence_records=(record,) * 10_001, + terminal_batch_records=(), + terminal_reservations=(), + terminal_generation_reservations=(), + closure_witness_id=AdmissionWitnessId("closure-witness-cardinality"), + fence_proof=None, + processed_event_tombstones=(), + retained_unresolved_count=0, + retained_generation_count=0, + ) + + assert "closed_epoch_occurrence_limit_exceeded" in str(exc_info.value) + + +@pytest.mark.parametrize( + "non_dispatch_surface", + [ + ProducerSurface.TOOL_ARGUMENT, + ProducerSurface.TOOL_RESULT_ENVELOPE, + ProducerSurface.USER_PROMPT, + ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ProducerSurface.SKILL_PLUGIN_CONTEXT, + ProducerSurface.OTHER_CONTEXT_INJECTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION, + ], +) +def test_dispatch_identity_is_rejected_on_non_dispatch_surfaces( + non_dispatch_surface: ProducerSurface, +) -> None: + identity = DispatchIdentity.from_dispatch_id("12345678-1234-1234-1234-123456789abc") + with pytest.raises(ContextAdmissionValidationError) as exc_info: + _lineage(surface=non_dispatch_surface, dispatch_identity=identity) + assert "dispatch_identity_on_non_dispatch_surface" in str(exc_info.value) + + +def test_occurrence_record_rejects_duplicate_witness_ids() -> None: + occurrence = _occurrence() + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.COMMITTED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=( + AdmissionWitnessId("dup"), + AdmissionWitnessId("dup"), + ), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + assert "duplicate_witness_id" in str(exc_info.value) + + +def test_admission_witness_rejects_missing_or_duplicate_batch_occurrences() -> None: + occurrence = _occurrence() + batch = _batch("batch-witness-occurrences", (occurrence,)) + witness = _witness(batch, WitnessKind.PROVIDER_ACCEPTED) + + for invalid_occurrences in ( + (), + (occurrence.occurrence_id, occurrence.occurrence_id), + ): + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(witness, occurrence_ids=invalid_occurrences) + assert "invalid_witness_occurrences" in str(exc_info.value) + + rollover = replace( + witness, + kind=WitnessKind.EPOCH_ROLLOVER, + occurrence_ids=(), + ) + assert rollover.occurrence_ids == () + + +def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: + occurrence = _occurrence() + batch = _batch("batch-simul", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionBatchRecord( + batch=batch, + state=AdmissionState.INDETERMINATE, + reservation_id=None, + witness_ids=(), + committed_input_count=5, + unresolved_input_count=5, + ) + assert "committed_and_unresolved_simultaneously" in str(exc_info.value) + + +@pytest.mark.parametrize( + ("state", "committed_count", "unresolved_count", "reason_code"), + [ + ( + AdmissionState.RESERVED, + 1, + 0, + "committed_count_for_nonterminal_batch", + ), + ( + AdmissionState.REQUEST_DISPATCHED, + 0, + 1, + "unresolved_count_for_resolved_batch", + ), + ], +) +def test_batch_record_counts_match_lifecycle_state( + state: AdmissionState, + committed_count: int, + unresolved_count: int, + reason_code: str, +) -> None: + occurrence = _occurrence() + batch = _batch("batch-lifecycle-counts", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionBatchRecord( + batch=batch, + state=state, + reservation_id=AdmissionReservationId("reservation-lifecycle-counts"), + witness_ids=(), + committed_input_count=committed_count, + unresolved_input_count=unresolved_count, + ) + assert reason_code in str(exc_info.value) + + +def test_journal_records_cross_check_duplicated_event_identities() -> None: + occurrence = _occurrence() + batch = _batch("batch-journal-identities", (occurrence,)) + namespace = _namespace("reserve-request") + reservation_key = AdmissionReservationKey( + idempotency_namespace=namespace, + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=None, + occurrence_revisions=((occurrence.occurrence_id, occurrence.representation_revision),), + ) + reservation = AdmissionReservation( + reservation_id=AdmissionReservationId("reservation-journal-identities"), + key=reservation_key, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + snapshot_sequence=1, + reserve_class=batch.reserve_class, + protected_pool_owner_id=None, + occurrence_ids=batch.occurrence_ids, + reserved_count=5, + ) + event = ReserveRequestEvent( + event_id=AdmissionEventId("event-journal-identities"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=namespace, + expected_aggregate_revision=AggregateRevision(0), + batch=batch, + snapshot_sequence=1, + input_reservations=(reservation,), + generation_reservation=None, + ) + decision = AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_ADMIT, + reason_code="accepted", + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=5, + available_ordinary_count=35, + available_protected_count=0, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + ) + with pytest.raises(ContextAdmissionValidationError) as processed_error: + replace(processed, event_id=AdmissionEventId("different-processed-event")) + assert "processed_event_identity_mismatch" in str(processed_error.value) + + idempotency = IdempotencyRecord( + namespace=namespace, + reservation_key=reservation_key, + original_descriptor=event, + original_reserve_decision=decision, + owning_event_id=event.event_id, + publication_revision=AggregateRevision(1), + ) + mismatches = ( + {"namespace": _namespace("different-operation")}, + { + "reservation_key": replace( + reservation_key, + batch_id=AdmissionBatchId("different-reservation-key"), + ) + }, + {"owning_event_id": AdmissionEventId("different-owning-event")}, + ) + for mismatch in mismatches: + with pytest.raises(ContextAdmissionValidationError) as idempotency_error: + replace(idempotency, **mismatch) + assert "idempotency_record_identity_mismatch" in str(idempotency_error.value) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_processed_event_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(processed,), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(idempotency,), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + processed_events=(), + idempotency_records=(idempotency,), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + processed_events=(processed,), + idempotency_records=( + replace( + idempotency, + publication_revision=AggregateRevision(0), + ), + ), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + tombstone = ExpiredIdempotencyTombstone( + namespace=namespace, + reservation_key=reservation_key, + original_descriptor=event, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + original_terminal_decision=decision, + ) + identity_mismatches = ( + {"namespace": _namespace("different-operation")}, + { + "reservation_key": replace( + reservation_key, + batch_id=AdmissionBatchId("different-tombstone-key"), + ) + }, + { + "original_terminal_decision": replace( + decision, + window_epoch_id=WindowEpochId("different-decision-epoch"), + ) + }, + ) + for mismatch in identity_mismatches: + with pytest.raises( + ContextAdmissionValidationError, + match="idempotency_tombstone_identity_mismatch", + ): + replace(tombstone, **mismatch) + + witness_mismatches = ( + {"kind": WitnessKind.PROVIDER_ACCEPTED}, + {"request_id": AdmissionRequestId("different-tombstone-request")}, + {"batch_id": AdmissionBatchId("different-tombstone-batch")}, + ) + for mismatch in witness_mismatches: + with pytest.raises( + ContextAdmissionValidationError, + match="idempotency_tombstone_witness_mismatch", + ): + replace( + tombstone, + expiry_witness=replace(tombstone.expiry_witness, **mismatch), + ) + + +def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: + with pytest.raises(ContextAdmissionValidationError): + CoverageEvidence( + claim_id="COV-test", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="/home/alice/private/source.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + with pytest.raises(ContextAdmissionValidationError): + CoverageEvidence( + claim_id="COV-test", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="~alice/private", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + +@pytest.mark.parametrize( + ("field_name", "invalid_value", "reason_code"), + [ + ("tested_revision", "abc123", "invalid_tested_revision"), + ("tested_revision", "g" * 40, "invalid_tested_revision"), + ("checked_at", "2026-7-3", "invalid_checked_at"), + ("checked_at", "2026-02-30", "invalid_checked_at"), + ("freshness_policy", "always_fresh", "invalid_freshness_policy"), + ], +) +def test_coverage_evidence_enforces_semantic_provenance_fields( + field_name: str, + invalid_value: str, + reason_code: str, +) -> None: + evidence = CoverageEvidence( + claim_id="COV-semantic-fields", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(evidence, **{field_name: invalid_value}) + + assert reason_code in str(exc_info.value) + + +@pytest.mark.parametrize( + "freshness_policy", + ["verify_on_revision_change", "infer_only"], +) +def test_coverage_v1_rejects_unimplemented_freshness_policies( + freshness_policy: str, +) -> None: + evidence = CoverageEvidence( + claim_id="COV-v1-policy", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="unsupported_coverage_freshness_policy", + ): + replace(evidence, freshness_policy=freshness_policy) + + +def test_coverage_v1_requires_exactly_one_evidence_record() -> None: + evidence = CoverageEvidence( + claim_id="COV-v1-cardinality", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + additional = replace( + evidence, + claim_id="COV-v1-cardinality-secondary", + source_locator="src/autoskillit/core/context_admission_secondary.py", + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="single_coverage_evidence_required", + ): + ProducerCoverageDef( + surface=ProducerSurface.NATIVE_SHELL, + control_point_owner="shell_capture_hook", + observation_state=CoverageState.VERIFIED, + authority_state=CoverageState.UPSTREAM_GATED, + evidence=(evidence, additional), + reason_code="authoritative-watermark-unavailable", + ) + + +def test_prepare_event_rejects_estimate_measurement() -> None: + occurrence = _occurrence() + batch = _batch("batch-estimate", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + PrepareBatchEvent( + event_id=AdmissionEventId("prepare-estimate"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("prepare-batch"), + expected_aggregate_revision=AggregateRevision(0), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=5, + measurement_kind=MeasurementKind.HOST_ESTIMATE, + authority_source=AuthoritySourceId("authority-test"), + ) + assert "non-authoritative-measurement" in str(exc_info.value) + + +def test_accept_event_rejects_estimate_measurement() -> None: + occurrence = _occurrence() + batch = _batch("batch-estimate-accept", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AcceptInputEvent( + event_id=AdmissionEventId("accept-estimate"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("accept-input"), + expected_aggregate_revision=AggregateRevision(0), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=5, + measurement_kind=MeasurementKind.BYTE_EMERGENCY, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + assert "non-authoritative-measurement" in str(exc_info.value) diff --git a/tests/docs/AGENTS.md b/tests/docs/AGENTS.md index 6f32025d6..6cdb3b84d 100644 --- a/tests/docs/AGENTS.md +++ b/tests/docs/AGENTS.md @@ -25,3 +25,4 @@ Documentation integrity, link validity, and naming convention tests. | `test_output_budget_protocol_decision.py` | Ratchet ADR-0005 limits, accepted gaps, operational signals, corrections, and forward obligations | | `test_recipe_redelivery_decision.py` | ADR-0004 recipe pull pagination identity and reconstruction contracts | | `test_check_sub_claude_md_script.py` | Unit and integration tests for the check_sub_claude_md.py pre-commit hook script | +| `test_context_admission_decision.py` | Ratchet ADR-0007 context-admission authority, evidence, traceability, and downstream ownership | diff --git a/tests/docs/test_context_admission_decision.py b/tests/docs/test_context_admission_decision.py new file mode 100644 index 000000000..7751bf821 --- /dev/null +++ b/tests/docs/test_context_admission_decision.py @@ -0,0 +1,449 @@ +"""Ratchet the accepted context-admission contract decision.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from autoskillit.core import CONTEXT_ADMISSION_COVERAGE + +REPO_ROOT = Path(__file__).resolve().parents[2] +DECISION = REPO_ROOT / "docs/decisions/0007-context-admission.md" +DECISION_INDEX = REPO_ROOT / "docs/decisions/README.md" + +pytestmark = pytest.mark.small + +CODEX_VERSION = "0.145.0" +CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" +REQUIRED_PINNED_CODEX_PATHS = ( + "codex-rs/core/src/session/context_window.rs", + "codex-rs/core/src/context_manager/history.rs", + "codex-rs/features/src/lib.rs", + "codex-rs/core/src/tools/spec_plan.rs", + "codex-rs/core/src/tools/handlers/get_context_remaining.rs", + "codex-rs/app-server-protocol/src/protocol/v2/thread.rs", + "codex-rs/app-server/README.md", + "codex-rs/protocol/src/protocol.rs", + "codex-rs/utils/string/src/truncate.rs", + "codex-rs/hooks/schema/generated", +) +CURRENT_HOOK_COVERAGE_URL = ( + "https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#hooks" +) +EXPECTED_DEPENDENCY_EDGES = { + "#4333 C1 -> #4334 C2", + "#4333 C1 + #4334 C2 + #4335 C3 -> #4336 C4", + "#4333 C1 + #4334 C2 + #4335 C3 -> #4337 C5", + "#4333 C1 + #4334 C2 -> #4338 C8", + "#4319/#4320/#4321/#4322/#4325/#4326/#4327 -> #4335 C3 artifact authority", + "#4334 C2 + #4336 C4 + #4337 C5 + #4324 + #4338 C8 -> #4339 C6", + "#4334 C2 + #4335 C3 + #4271 + #4338 C8 -> #4340 C7", +} + +REQUIRED_HEADINGS = ( + "Context", + "Decision", + "Admission boundary and authority", + "Protocol version 1", + "State, witnesses, and atomic batches", + "Accounting and identity invariants", + "Protected reserve and epoch isolation", + "Producer coverage matrix", + "Authority unavailable and byte ceilings", + "Upstream authority request", + "Privacy and observability", + "Capability decision for Codex 0.145.0", + "Protocol evolution", + "Downstream dependency graph", + "Non-goals", + "Traceability", +) + +TRACEABILITY_TERMS = { + "INV-1": "model-visible admission boundary", + "INV-2": "stable identities", + "INV-3": "atomic reserve/commit/release protocol", + "INV-4": "version-pinned coverage matrix", + "INV-5": "token_budget/get_context_remaining", + "INV-6": "upstream Codex contract", + "INV-7": "privacy-safe observability", + "OUT-1": "versioned admission protocol and state machine", + "OUT-2": "producer/control-point coverage matrix", + "OUT-3": "accounting and identity invariants", + "OUT-4": "failure and reconciliation semantics", + "OUT-5": "authoritative token accounting", + "OUT-6": "upstream Codex request", + "OUT-7": "implementation dependency graph", + "NG-1": "no enforcement or numeric budget defaults", + "NG-2": "retain existing raw per-producer ceilings", + "NG-3": "bytes are not an exact token proxy", + "NG-4": "digest is not an access capability or deduplication identity", + "AC-1": "every model-visible producer", + "AC-2": "idempotent reservation keys and compaction/window reset rules", + "AC-3": "outstanding concurrent calls and protected reserve", + "AC-4": "Codex claims cite tested version and primary sources", + "AC-5": "C2-C8 use the shared accounting contract", +} + +ALLOWED_TRACEABILITY_TARGETS = ( + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "CONTEXT_ADMISSION_COVERAGE", + "reduce_context_admission", + "replay_context_admission", + "test_context_admission_contract.py", + "test_context_admission_coverage.py", + "test_context_admission_reducer.py", + "test_context_admission_state_machine.py", + "test_context_admission_decision.py", + *REQUIRED_HEADINGS, +) + + +@pytest.fixture(scope="module") +def decision_text() -> str: + assert DECISION.exists(), "ADR-0007 must exist" + return DECISION.read_text(encoding="utf-8") + + +def _section(text: str, heading: str) -> str: + lines = text.splitlines() + start = -1 + level = 0 + for index, line in enumerate(lines): + match = re.fullmatch(r"(#{2,6}) (.+)", line) + if match is not None and match.group(2) == heading: + start = index + 1 + level = len(match.group(1)) + break + assert start >= 0, f"missing ADR heading: {heading}" + end = len(lines) + for index in range(start, len(lines)): + match = re.match(r"(#{2,6}) ", lines[index]) + if match is not None and len(match.group(1)) <= level: + end = index + break + return "\n".join(lines[start:end]) + + +def _traceability_rows(text: str) -> dict[str, tuple[str, str]]: + rows: dict[str, tuple[str, str]] = {} + for row_id, requirement, target in re.findall( + r"^\|\s*((?:INV|OUT|NG|AC)-\d+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$", + _section(text, "Traceability"), + flags=re.MULTILINE, + ): + assert row_id not in rows, f"duplicate traceability row: {row_id}" + rows[row_id] = (requirement.strip(), target.strip()) + return rows + + +def test_context_admission_decision_is_indexed(decision_text: str) -> None: + assert "**Status:** Accepted" in decision_text + assert "#4333" in decision_text + assert "C1" in decision_text + assert "0007-context-admission.md" in DECISION_INDEX.read_text(encoding="utf-8") + + +def test_decision_has_the_normative_contract_sections(decision_text: str) -> None: + for heading in REQUIRED_HEADINGS: + assert re.search( + rf"^##+ {re.escape(heading)}\s*$", + decision_text, + flags=re.MULTILINE, + ), f"missing ADR heading: {heading}" + + +def test_decision_references_the_executable_protocol(decision_text: str) -> None: + for required in [ + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "reduce_context_admission", + "replay_context_admission", + "CONTEXT_ADMISSION_COVERAGE", + ]: + assert required in decision_text + protocol = _section(decision_text, "Protocol version 1") + assert re.search(r"\bprotocol version 1\b", protocol, flags=re.IGNORECASE) + + +def test_traceability_freezes_the_entire_issue_scope(decision_text: str) -> None: + rows = _traceability_rows(decision_text) + assert set(rows) == set(TRACEABILITY_TERMS) + for row_id, required_term in TRACEABILITY_TERMS.items(): + requirement, target = rows[row_id] + assert required_term.casefold() in requirement.casefold(), row_id + assert any(allowed in target for allowed in ALLOWED_TRACEABILITY_TARGETS), row_id + + +def test_codex_capability_claims_are_version_pinned_to_primary_evidence( + decision_text: str, +) -> None: + capability = _section(decision_text, "Capability decision for Codex 0.145.0") + for required in [ + CODEX_VERSION, + CODEX_REVISION, + "token_budget", + "get_context_remaining", + "PreCompact", + "PostCompact", + ]: + assert required in capability + for source_path in REQUIRED_PINNED_CODEX_PATHS: + assert CODEX_REVISION in capability + assert source_path in capability + assert CURRENT_HOOK_COVERAGE_URL in capability + + +def test_authority_unavailable_behavior_preserves_byte_boundaries( + decision_text: str, +) -> None: + section = _section(decision_text, "Authority unavailable and byte ceilings").casefold() + for required in [ + "watermark_unavailable", + "upstream_gated", + "raw-byte", + "no numeric", + "independent", + ]: + assert required in section + + +def test_decision_freezes_the_complete_producer_matrix(decision_text: str) -> None: + coverage = _section(decision_text, "Producer coverage matrix") + actual_rows = tuple( + tuple(cell.strip().strip("`") for cell in line.strip().strip("|").split("|")) + for line in coverage.splitlines() + if line.startswith("| `") + ) + expected_rows = tuple( + ( + row.surface.name, + row.control_point_owner, + row.observation_state.name, + row.authority_state.name, + ) + for row in CONTEXT_ADMISSION_COVERAGE + ) + + assert actual_rows == expected_rows + + +def test_upstream_request_contains_all_three_authority_parts_and_minimum_fields( + decision_text: str, +) -> None: + request = _section(decision_text, "Upstream authority request") + for required in [ + "atomic snapshot/reservation", + "generated-output maximum", + "synchronous blocking", + "final ordered batch", + "canonical representation manifest", + "receiver fence", + "durable/queryable journal", + "history staging", + "request inclusion", + "provider acceptance", + "output-usage reconciliation", + "rollback", + "truncation/compaction replacement", + "authoritative reconciliation", + "request_id", + "batch_id", + "ordered members", + "reservation IDs", + "thread/turn/agent lineage", + "admission sequence", + "window ID/number", + "model/tokenizer identity", + "snapshot sequence", + "measurement kind/source", + "active/hard-limit/remaining/proposed/max-output counts", + "reserve class", + "representation revision", + ]: + assert required in request + + +def test_privacy_table_freezes_field_governance(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability") + for required in [ + "Runtime/audit fields", + "Lineage and source locator fields", + "Aggregate telemetry fields", + "Forbidden content", + "Purpose", + "Maximum length/cardinality", + "Retention", + "Access", + "Deletion", + "Export", + "opaque", + "lineage", + "source locator", + "content", + "absolute paths", + "bearer tokens", + "content/artifact hashes", + ]: + assert required in privacy + + +REQUIRED_RUNTIME_AUDIT_FIELDS = ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "event_id", + "reservation_id", + "witness_id", + "batch_id", + "request_id", + "reservation_key", + "occurrence_id", + "attempt_id", + "delivery_occurrence_id", + "generation_reservation_id", + "reason_code", + "requested_count", + "available_ordinary_count", + "available_protected_count", + "reserved_count", + "committed_input_count", + "unresolved_input_count", + "retained_unresolved_count", + "maximum_allowance", + "exact_terminal_usage", + "injected_count", + "priority", + "predicted_authoritative_maximum", + "active_count", + "hard_limit", + "remaining_count", + "highest_admitted_dispatch_sequence", + "representation_revision", + "tested_version", + "tested_revision", + "publication_revision", + "checked_at", + "freshness_policy", + "verifier", + "configuration_mode", + "backend", + "control_point_owner", +) + +REQUIRED_LINEAGE_FIELDS = ( + "root_session_id", + "current_session_id", + "root_agent_id", + "current_agent_id", + "parent_agent_id", + "root_thread_id", + "current_thread_id", + "parent_thread_id", + "fork_occurrence_id", + "turn_id", + "producer_surface", + "producer_instance_id", + "tool_call_id", + "model_item_id", + "dispatch_identity", + "source_locator", +) + +REQUIRED_AGGREGATE_FIELDS = ( + "state", + "reason_code", + "version", +) + +REQUIRED_FORBIDDEN_CONTENT = ( + "model content", + "payloads", + "prompts", + "tool results", + "absolute paths", + "bearer", + "credentials", + "API keys", + "session cookies", + "content/artifact hashes", + "sha256:", + "blake2:", + "content:", +) + +CONCRETE_MAXIMA_HINTS = ( + "96 ASCII", + "64 ASCII", + "128 ASCII", + "256 ASCII", + "10 ASCII", + "10⁴", + "64-bit non-negative", + "kebab-case", + "ISO-8601", + "30 days", + "no `", + "no absolute", + "no secrets", + "no home-directory", + "no URLs", +) + + +def test_privacy_table_freezes_complete_runtime_audit_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_RUNTIME_AUDIT_FIELDS if field not in privacy] + assert not missing, f"missing runtime/audit fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_freezes_complete_lineage_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_LINEAGE_FIELDS if field not in privacy] + assert not missing, f"missing lineage fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_freezes_aggregate_telemetry_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_AGGREGATE_FIELDS if field not in privacy] + assert not missing, f"missing aggregate telemetry fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_forbids_complete_content_categories(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability").casefold() + missing = [item for item in REQUIRED_FORBIDDEN_CONTENT if item.casefold() not in privacy] + assert not missing, f"missing forbidden-content categories in ADR-0007: {missing}" + + +def test_privacy_table_requires_concrete_field_maxima(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [hint for hint in CONCRETE_MAXIMA_HINTS if hint not in privacy] + assert not missing, f"ADR-0007 privacy table must specify concrete maxima: {missing}" + + +def test_decision_keeps_issue_non_goals_explicit(decision_text: str) -> None: + non_goals = _section(decision_text, "Non-goals").casefold() + for required in [ + "enforcement", + "numeric budget defaults", + "existing raw per-producer ceilings", + "bytes", + "exact token proxy", + "digest", + "access capability", + "deduplication identity", + ]: + assert required in non_goals + + +def test_dependency_graph_freezes_exact_edges(decision_text: str) -> None: + graph = _section(decision_text, "Downstream dependency graph") + actual_edges = {line.strip() for line in graph.splitlines() if "->" in line} + assert actual_edges == EXPECTED_DEPENDENCY_EDGES diff --git a/tests/fleet/test_dispatch_state_handle.py b/tests/fleet/test_dispatch_state_handle.py index 0cf3c14e9..253d06428 100644 --- a/tests/fleet/test_dispatch_state_handle.py +++ b/tests/fleet/test_dispatch_state_handle.py @@ -141,7 +141,7 @@ def test_dispatch_state_handle_open_continued_rejects_missing_file(self, tmp_pat d = _dispatches_dir(tmp_path) with pytest.raises(FileNotFoundError): - DispatchStateHandle.open_continued(d, "nonexistent-id-abc-123") + DispatchStateHandle.open_continued(d, "00000000-0000-0000-0000-000000000001") def test_dispatch_state_handle_open_continued_succeeds_with_existing_file(self, tmp_path): from autoskillit.fleet.state import DispatchStateHandle @@ -161,8 +161,8 @@ class TestAllResumeCombinationsProduceValidHandle: "resume,prior,expect_fresh", [ (None, None, True), - (None, "abc-def-ghi-jkl-mno", True), - ("sess-1", "abc-def-ghi-jkl-mno", False), + (None, "11111111-1111-1111-1111-111111111111", True), + ("sess-1", "11111111-1111-1111-1111-111111111111", False), ("sess-1", None, True), ("sess-1", "", True), ], @@ -235,7 +235,7 @@ async def test_capture_chain_across_resume_boundary(self, tool_ctx, monkeypatch) dispatches_dir.mkdir(parents=True, exist_ok=True) campaign_id = tool_ctx.kitchen_id - prior_id = "prev-dispatch-abc123" + prior_id = "22222222-2222-2222-2222-222222222222" prev_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prev_state_path, campaign_id, "camp", "", [DispatchRecord(name="dispatch-a")] @@ -295,7 +295,7 @@ async def test_session_chain_accumulates_across_resume(self, tool_ctx, monkeypat dispatches_dir = tool_ctx.temp_dir / "dispatches" campaign_id = tool_ctx.kitchen_id - prior_id = "prior-dispatch-abc123" + prior_id = "33333333-3333-3333-3333-333333333333" prior_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prior_state_path, diff --git a/tests/fleet/test_resume_preflight.py b/tests/fleet/test_resume_preflight.py index 23736930c..5662a691f 100644 --- a/tests/fleet/test_resume_preflight.py +++ b/tests/fleet/test_resume_preflight.py @@ -31,7 +31,7 @@ async def test_missing_jsonl_rejects_dispatch(self, tool_ctx, monkeypatch, tmp_p dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-missing-jsonl" + prior_id = "00000000-0000-0000-0000-000000000101" write_initial_state( dispatches_dir / f"{prior_id}.json", tool_ctx.kitchen_id, @@ -109,7 +109,7 @@ async def test_chain_fallback_proceeds_when_primary_missing( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-for-chain" + prior_id = "00000000-0000-0000-0000-000000000102" prev_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prev_state_path, @@ -229,7 +229,7 @@ async def test_resume_rejected_when_prior_dispatch_succeeded(self, tool_ctx, mon dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-succeeded" + prior_id = "00000000-0000-0000-0000-000000000103" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -291,7 +291,7 @@ async def test_resume_proceeds_when_prior_dispatch_resumable( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-resumable" + prior_id = "00000000-0000-0000-0000-000000000104" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -345,7 +345,7 @@ async def test_resume_proceeds_when_prior_dispatch_not_in_state( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-other-name" + prior_id = "00000000-0000-0000-0000-000000000105" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -388,7 +388,7 @@ async def test_resume_proceeds_when_prior_state_missing(self, tool_ctx, monkeypa lambda **_: _make_no_sentinel(), ) - prior_id = "prior-dispatch-nonexistent" + prior_id = "00000000-0000-0000-0000-000000000106" await execute_dispatch( tool_ctx=tool_ctx, diff --git a/tests/test_test_filter_core_cascade.py b/tests/test_test_filter_core_cascade.py index 5f3ffb052..b7ea1f7e1 100644 --- a/tests/test_test_filter_core_cascade.py +++ b/tests/test_test_filter_core_cascade.py @@ -100,6 +100,7 @@ def test_all_entries_present(self) -> None: "_type_backend", "_type_recipe_delivery", "_type_recipe_sections", + "_type_context_admission", "_type_dispatch_identity", "_type_figure_spec", "_type_session_env", @@ -122,6 +123,7 @@ def test_all_entries_present(self) -> None: "closure_hashing", "path_containment", "closure_verifier", + "context_admission", "_delivery_bounds", } assert set(MODULE_CASCADE_CORE.keys()) == expected_stems