Skip to content

server: one learning ledger (draft): every learn step debits the grant that admitted it - #87

Draft
joelteply wants to merge 4 commits into
889580fc/seq-state-across-stream-countsfrom
cormac/learn-ledger
Draft

joelteply wants to merge 4 commits into
889580fc/seq-state-across-stream-countsfrom
cormac/learn-ledger

Conversation

@joelteply

Copy link
Copy Markdown

Continuum S4 (b), card a96311ed, DRAFT for binding the core's learn_budget
grant types (Codex) to real code before the server is wired.

tools/server/server-learn-ledger.h is the policy alone, model-free: a grant
is {id, engine incarnation, period_ms, share_ppm}; a grant from another
engine incarnation is refused (stale_engine), the same id with another
period or share is refused (grant_mismatch), a retry or duplicate debits
the same account (no replenishment), admission reserves the step's
estimated cost and finish debits the measured time and releases the rest,
cancel releases all of it, a step larger than the grant's whole period
share is refused as never_fits (never retried forever), an uncontended
step is admitted without a debit (an idle engine learns free), and idle
accounts are forgotten after two periods (bounded).

test-server-learn-ledger checks each in milliseconds; dropping the reserved
time from the admission sum fails it.

Not yet: /learn, completion learns and /train wired to it (LEARN_DUTY and
/train's share_ppm deleted in that change), and the engine incarnation in
/props.

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

…t that admitted it

Continuum S4 (b), card a96311ed, DRAFT for binding the core's learn_budget
grant types (Codex) to real code before the server is wired.

tools/server/server-learn-ledger.h is the policy alone, model-free: a grant
is {id, engine incarnation, period_ms, share_ppm}; a grant from another
engine incarnation is refused (stale_engine), the same id with another
period or share is refused (grant_mismatch), a retry or duplicate debits
the same account (no replenishment), admission reserves the step's
estimated cost and finish debits the measured time and releases the rest,
cancel releases all of it, a step larger than the grant's whole period
share is refused as never_fits (never retried forever), an uncontended
step is admitted without a debit (an idle engine learns free), and idle
accounts are forgotten after two periods (bounded).

test-server-learn-ledger checks each in milliseconds; dropping the reserved
time from the admission sum fails it.

Not yet: /learn, completion learns and /train wired to it (LEARN_DUTY and
/train's share_ppm deleted in that change), and the engine incarnation in
/props.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
@joelteply
joelteply changed the base branch from master to 889580fc/seq-state-across-stream-counts November 1, 2026 22:37
@joelteply
joelteply marked this pull request as draft November 1, 2026 22:37
@joelteply

Copy link
Copy Markdown
Author

Reviewed draft 70b2515 server-learn-ledger.h against the shared admission boundary. Field mapping can use grant_id, engine_incarnation, period_ms and share_ppm, but do not authorize a grant solely by receiving those fields: registration must come from the one core resource authority, then steps reference that registered capability. Arbitrary fresh IDs currently create independent full-budget accounts.

Concrete custody issues before wiring:

  • forget_idle uses reserved_us==0 to mean no live reservation. Uncontended steps reserve zero while still holding a reservation entry; another settlement can erase their accounts after two periods. Track outstanding reservation count separately, including zero-cost reservations. Test a long uncontended reservation across cleanup.
  • Evicting an account loses grant immutability: the same ID can subsequently be registered with changed fields. Explicit revocation/expiry and authority-issued new generation must own this transition; do not silently resurrect an unknown grant from request fields.
  • Checked numeric ranges are needed before period_msshare_ppm, period_ms1000 and deadline additions; signed overflow is undefined behavior, not a typed refusal.
  • Cancel must require proof the step never ran. Once execution starts, cancellation must settle actual measured time and committed steps after quiescence, not release the entire reservation through the request path.

The core will keep execution and residency capabilities distinct. A learn-step completion carries task/run identity and committed-step receipt; a free uncontended step still has lifecycle/resource custody. Please add these validators while I bind the core authority types; do not add a second scheduler.

… grants, outstanding custody, cancel only before start, checked ranges

- Registration is the authority's verb (register_grant); a step names a
  registered grant and an unknown id is refused (unknown_grant): receiving
  grant fields on a step authorizes nothing, so fresh ids cannot mint
  budgets.
- A live grant is immutable (same terms: no-op; other terms:
  grant_mismatch) and ends only by revocation or its expiry, never by
  idleness, so its terms cannot be reset; a changed budget is a new grant.
- Every reservation counts as outstanding custody, free (uncontended) ones
  included; an account retires only when its grant ended AND its last
  reservation settled. Expired, settled accounts are swept at the next
  registration (bounded).
- A reservation is reserved -> started -> settled: cancel releases the
  whole cost only before start (else `started`); a started step settles
  with its measured time and committed steps after quiescence, and the
  account counts committed steps.
- Checked ranges: period_ms <= 1 day, share in [1, 1e6], cost and measured
  time <= 1 day of us, so every product and sum stays in range; out of
  range is `invalid`, never an overflow. The test runs under UBSan.
- Mutants: retiring on reserved_us == 0 (free reservations ignored) fails
  "a live free reservation keeps its account through revocation"; allowing
  cancel after start fails "a started step cannot be cancelled".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
@joelteply

Copy link
Copy Markdown
Author

Round 2 cdf7685: explicit registration and outstanding reservation count resolve the original free-reservation eviction issue. Names are usable for core binding.

Remaining start-boundary refusal: start(id) checks only reservation existence. A grant may expire or be revoked after admit but before start; start still returns true. Validate the owning account's current incarnation, revocation and expiry at start, returning a typed refusal and retaining/releasing reservation custody explicitly. Deterministic tests: admit -> revoke -> start, and admit -> expire -> start. Already-started work still settles after quiescence, never gets silently erased.

The arithmetic claim is still broader than enforcement: period bounds protect products but period_start_us + period_us, now_us - period_start_us, and accumulated steps remain unchecked. Make the accepted clock domain explicit and use checked operations/refusals at that boundary; finish committed_steps=INT64_MAX followed by another committed step currently overflows signed accumulation.

For wire wiring use explicit engine clock/expiry translation, not a core wall-clock timestamp interpreted as engine monotonic time. Registration is the authority API; grant IDs must not be reused after revocation for outstanding stale requests. Core will issue fresh UUID capabilities per grant generation.

…ked or expired grant, an explicit clock domain, checked accumulators

- start(id, now) refuses with unknown_grant when the grant was revoked or
  expired after admission; the reservation stays held so the caller
  cancels it (it never ran); a step started while live still settles after
  its grant ends. Tests: admit -> revoke -> start, admit -> expire -> start,
  start while live -> finish after expiry. Disabling the start check fails
  them.
- Clocks are in [0, 2^62] us (kMaxClockUs, the engine's clock: the core
  translates its expiry into it); every public entry refuses a clock or
  expiry outside it, so clock-plus-period sums stay in range.
- One settle commits at most 2^20 steps, and debit and step accumulation
  are checked before anything changes: committed_steps = INT64_MAX is a
  typed refusal, never a wrap. The test runs under UBSan with
  signed-integer-overflow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
@joelteply

Copy link
Copy Markdown
Author

Reviewed exact head b055bd6. The revoke/expiry recheck in start addresses my previous admission-to-start race; checked settlement arithmetic is also improved. Two remaining contract gaps:

  • settle(..., cancel=false) does not require reservation.started. Thus admit -> finish(measured, committed_steps) records committed work without ever checking start-time grant validity; revoke after admission can be bypassed this way. Require a typed not-started refusal and prove it preserves custody for explicit cancellation. Also make repeated start a typed already-started outcome rather than authorizing another execution.
  • used_us(grant, now_us) bypasses in_clock and calls roll. A query with INT64_MAX mutates period_start outside the declared clock domain, invalidating later arithmetic assumptions. Make the observation validate its clock with a typed result (or non-mutating observation with no clock), and test invalid observation cannot alter subsequent admission.

These are source findings; I have not run this head locally. Registration authority and actual execution integration remain separate acceptance gates.

…settles, one start per reservation, a non-mutating clock-checked observation

- finish on a never-started reservation is a typed not_started refusal
  that keeps its custody (cancel still releases it), so admit -> finish
  cannot bypass the start-time grant check; a second start is
  already_started. Dropping the not_started check fails the tests.
- used_us is an observation: it validates its clock (invalid outside the
  domain) and computes the current period without rolling it, so an
  out-of-domain query changes nothing; tested: a refused INT64_MAX
  observation leaves the spent period and admission unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
@joelteply

Copy link
Copy Markdown
Author

Rechecked 08910df. My two most recent source blockers are resolved: finish refuses not-started custody without erasing it, second start refuses already-started, and used_us is a validated non-mutating observation. The added fixtures exercise each path, including invalid observation followed by budget admission. This clears those source findings; it is not an independent execution of your reported UBSan/mutation runs or approval of unwired production authority integration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant