Skip to content

feat(db,gateway): R13 one-use budget reservations: durable store + atomic consume (SCHEMA CHANGE, approved under operator item 26) - #402

Open
LamaSu wants to merge 13 commits into
masterfrom
feat/r13-budget-reservations
Open

LamaSu wants to merge 13 commits into
masterfrom
feat/r13-budget-reservations

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Current state (2026-10-06, pcc-composition)


Round 3 (2026-09-29): astra's pack-45 findings (DO-NOT-SHIP on ac3db65), each REPRODUCED first, then fixed at 638bc3d

Under the review loop, the new tests were run at 45d0cde (byte-identical store and parser to ac3db65) BEFORE any change: store 9 failed, parser 3 failed.

  • A (HIGH), repeated reads. Every issue and consume input, including the nested parent, is now read ONCE into an owned copy.
    • Reproduced: the amount was read 4 times and 100 USDC inserted.
    • Reproduced: consume's checks ran on one reservation and consumed ANOTHER principal's.
  • B (HIGH), schema repair. It now runs in ONE immediate transaction and uses a RECORDED schema version (pcc_schema_versions, v2), never SQL text.
    • Reproduced: an interleaved writer's committed row was lost.
    • Reproduced: a correct, differently written table was refused.
  • C (MEDIUM), direct writers. The insert guard refuses a reused id, and id is NOT NULL.
    • Reproduced: INSERT OR REPLACE reopened a consumed row, and a NULL id was accepted.
  • D (MEDIUM), the parser. parseSealedDeal enforces the compiler's assurance structure (bad-assurance).
    • Reproduced: a forged tier-2 deal with no program was consumed under a floor-2 reservation.
  • Not here: the compiler's reclaimAt above uint64 (reproduced) goes to feat(spec): accepted-plan compiler — composition half of MS-01 (R12) #351's next round. It is fail-closed at consume today.
  • Evidence: db 231, spec 912, gateway 3066; mutations 10 of 10 killed.
  • Delta pack: 92-composition-r13-r3-6dadaf8b.md. Migration 0004 is regenerated from the runtime DDL.

Schema change: approved under operator-queue item 26

(Historical note, kept as written before the approval:)
This PR adds the table budget_reservations. Once merged and deployed, the gateway's runtime migration (migrateDatabase) creates it at startup. The design is operator decision #2240, amended by #2301 (payer, parent binding, a currency check on consume, the app-level digest recompute, an exact ceiling) and #2302 (min_tier). The steward recommends approve as amended. Nothing is applied until you merge and deploy. If you change the design, this draft changes with it.

Round 2 (the operator's cross-family review, 2026-09-28)

The operator's ChatGPT review of #402/#410/#415 at 0b8adda9 was DO-NOT-SHIP (H1, H2, H3, M4, M5, M6). This PR fixes the store side of every finding except M4 (that one's #410's); the routes follow in #410 and #415.

  • H1 — child terms come from the sealed deal, never the caller. issue now takes parent: { reservationId, unit } only. Inside its BEGIN IMMEDIATE transaction it reads the consumed parent, re-checks sha256(stored deal) == stored digest, parses the deal strictly and requires it to be that reservation's deal, then finds the exact unit and derives its operator, net n and reclaimAt. The identity checks come first, so a non-operator only ever sees the not-found family. A child also fits its own request's ceiling.
  • H2 — durable expiry, and the store's own clock. The clock is injected at construction and read inside each transaction; no caller passes now. Before an issue frees an expired reservation's share, it moves that reservation issued → expired in the same transaction, in both the request scope and the sibling scope. A consume that finds an expiry records it; consume requires issued. Tested on two connections, in both orders, and for siblings.
  • H3 — the migration and the runtime schema now agree. 0004_budget_reservations.sql is BUDGET_RESERVATIONS_DDL, statement for statement (test-asserted). migrateDatabase runs ensureBudgetReservationsSchema, which rebuilds an EMPTY table of another shape and refuses to boot on one that holds rows. Test: 0004, then the runtime migration, then a real consume.
  • M5 — the table refuses what its invariants forbid. CHECKs: amount > 0 and ≤ uint256; a canonical digest; consumed ⇔ digest ⇔ deal ⇔ consumed_at; created < expires; integer times and tier. Triggers: inserts only as issued, and a child only under a consumed parent; updates only issued → consumed | expired | released, with no term changed; no DELETE. SQL has no sha256, so the trusted readers re-check the hash, parse, and check the reservation.
  • M6 — the consume seals only a real deal, for this reservation. New @pcc/spec parseSealedDeal checks: canonical bytes; the domain; exact keys; lowercase hex; at most 64 units, 16 per job, 16 legs per unit and 16 KiB per unit; the f/n arithmetic; legs == n; total == sum of g; jobId = planId:operator; milestones; stepId = keccak(node); a bijective binding; planHash == canonicalPlanHash. The consume takes the request, currency, payers, obligation and lowest tier FROM THE DEAL — the input is now { reservationId, principal, dealDigest, dealPreimage }. The gateway adapter throws when the store refuses the server's own compiled deal: a generic 500, never a 409.

The sealed deal itself (amendment #3231, steward condition #3235). budget_reservations gains consumed_deal_json, written in the same transaction as the digest (450b575). The stored bytes are acceptedDealDigest's own canonical PREIMAGE, and consume refuses unless sha256(bytes) == digest (deal-preimage-mismatch). SQL has no sha256, so the table cannot check the hash itself; its CHECKs hold presence and size. The bytes are never on the reservation object — only server-side sealedDealPreimage(id) reads them, for MC 9's parent terms, funding and VCR delivery — and are capped at 1 MiB, enforced by both the store and a table CHECK. The gateway adapter (af14e2a) recomputes the digest as sha256 of #351's acceptedDealPreimage(plan) and hands the store both the digest and the preimage bytes; the store re-checks that they hash to each other before keeping the bytes as the sealed deal.

Tests: spec 909/909, db 222/222, gateway R13 129/129. The new files typecheck. New tests cover every finding, including the reviewer's exact inputs: a MiB of y with its hash, an invented unit with a forged operator/net/reclaim, 0004 then migrate then consume, and the skewed-clock interleaving. 39 mutation checks: 38 killed. The one survivor is EQUIVALENT — SD14, the parser's one-job-per-operator check: two jobs with one operator share a jobId, so their unit refs collide and the bijective binding check refuses the deal; it's kept for its clearer diagnosis. A first-run survivor, ST6 (the sibling expiry sweep), was a TEST GAP: every sibling test shared a request; siblings under two requests now kill it.

Round-2 pack: 45-composition-r13-402-410-415-r2-ac3db658.md (covers #402, #410 and #415).

Known cost of the stack: until #351 → #355 → #356 → #357 → #391 merges up, #402's diff against #391 temporarily includes #351's 9d7686a.


Was stacked on #391 (the R9 route) → #357 → #356 → #355 → #351; all of them are on master now. Reconciliation row R13 (Gate B keystone; MUST-CLOSE 7 and 8). Retarget the stack to master before merge (rule 4).

What it does

@pcc/store: BudgetReservationStore (packages/db/src/repositories/budget-reservations.ts). It is self-contained over the raw SQLite handle; the shared IRepositories aggregate is untouched.

  • Exact money. SQLite has no numeric(78,0), so amounts are canonical decimal TEXT and compared as BigInt in code, never in SQL and never as a float. A test with values around 2^200 would fail under float rounding.
  • issue, in ONE BEGIN IMMEDIATE transaction:
    • The request's issued and consumed top-level reservations, plus the new one, must fit the request's authorized ceiling (fix(gateway): honest operator-relay evidence, claim authentication, immutable authorized ceiling (LO-GW-4/3a, R-06) #335 R-06, passed in by the server in exact base units).
    • MC 9 child authority (#2301, hardened in round 2 / H1). issue takes parent: { reservationId, unit } only — never caller-supplied terms. Inside the transaction it re-reads the consumed parent, re-checks its sealed-deal digest, and derives the unit's operator, net n and reclaimAt from THAT deal. The parent must be consumed and in the same currency. All children of one unit fit its net n, none outlives its reclaimAt, and the child inherits max(parent's floor, its own) — and now also fits its own request's ceiling.
  • consume, in ONE BEGIN IMMEDIATE transaction (round 2, M6): takes { reservationId, principal, dealDigest, dealPreimage } — never a caller-supplied payer or obligation. parseSealedDeal (new @pcc/spec, a strict total parser of the accepted-deal preimage) checks the deal's canonical bytes, domain, exact keys, and unit/leg/size bounds, and the request, currency, every job's payer, the obligation and the lowest tier all come FROM THE DEAL. The store still re-checks principal, issued-and-unexpired and floor against the stored row, then seals the deal digest: the UPDATE is conditional on state = 'issued', and exactly one row must change.
  • Table constraints (SQL CHECK): canonical digits only (1–78, no leading zero), the known states, consumed ⇔ digest present, min_tier in 0..3, and both halves of a parent binding or neither — extended in round 2 (M5) with created < expires, integer times/tier, and insert/update/no-DELETE triggers (see above).

Gateway: services/reservation-store.ts implements the trace stand-in's consume PROTOCOL, made durable.

  • loadReservation feeds the seam.
  • consumeReservation feeds the route. Before touching the store it refuses a plan it did not compile:
  • Then the atomic store consume runs. Since round 2 (M6), the store parses the preimage and takes the request, currency, every payer, the obligation and the lowest tier FROM THE DEAL. A RESEALED plan passes integrity but not authority: another payer, principal, expiry or floor is still refused, with the same answers as before.
  • If the store refuses the server's OWN compiled deal as malformed (invalid-deal, wrong-reservation, a preimage mismatch or invalid input), the adapter throws. That is a generic 500, never a client-facing 409.

Tests

  • @pcc/store: 13 tests against real SQLite.

    • Exact uint256 round-trip; malformed input.
    • The ceiling: exact at the boundary; consumed counts; released frees its share; per request; float-proof at 2^200.
    • Exactly-once consume; every A-for-B refusal; expired; over the maximum; below the floor.
    • Two connections on one file: while one holds the write lock the other cannot interleave, and exactly one consume wins.
    • The table's CHECKs reject raw bad rows.
    • MC 9 child rules; createStore runs the DDL idempotently.
  • The db package's full suite passes with the new migration.

  • Gateway: 5 tests.

    • The whole route on the durable store: 200 seals the recomputed digest; a second accept is 409; of two concurrent accepts, exactly one is sealed.
    • loadReservation mapping.
    • The protocol's refusals, and authority after a reseal, including one job of several paying from another wallet.
  • Mutation checks, 20: 18 killed, 2 survivors explained.

    • Store (14): 12 killed.

    • The 2 survivors are layered concurrency defenses that single-threaded tests cannot tell apart:

      • BEGIN IMMEDIATE vs deferred. Under a deferred transaction, SQLite's snapshot isolation makes a racing read-then-write fail with SQLITE_BUSY rather than double-consume.
      • The UPDATE's state = 'issued' guard, which is redundant under the immediate lock.

      Each is kept as defence in depth.

    • Adapter (6): all killed. The per-job payer survivor led to the one-job-of-several test.

    (These are round-1 counts, scoped to what round 1 added. Round 2's own numbers — spec 909/909, db 222/222, gateway R13 129/129, 39 mutation checks/38 killed — are in the Round 2 section above and supersede these as the head's totals.)

Self-review fixes (e6975e4), before the cross-family round

  • Expired reservations stop holding money. An issued reservation past its expiry no longer holds its share of the request ceiling (or of its parent unit, for a child). It can never be consumed, and no housekeeping runs, so the share was held forever.
  • No parent payer for a child. A child may not name the parent's payer (#2301: never the parent payer's credentials).
  • 2 tests; 4 mutation checks, all killed.

Issue and read routes: #410, stacked on this PR.

Cross-family status: round 1 was DO-NOT-SHIP at 0b8adda9 (see Round 2 above, which fixes it). Round 2 goes through the operator's ChatGPT review as pack 45-composition-r13-402-410-415-r2-ac3db658.md.

Not in this PR

pcc-composition 8a0f4de0, goal pcc-reconciliation.

🤖 Generated with Claude Code

https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz

LamaSu and others added 3 commits September 24, 2026 15:47
…atomic consume (DRAFT: awaiting operator decision #2240)

This is the Gate B keystone (MUST-CLOSE 7 and 8). The schema is operator
decision #2240, as amended by #2301 (payer, parent binding, currency check,
app-level digest recompute, exact ceiling) and #2302 (min_tier). It must
not merge before the operator decides queue item 26. Once merged and
deployed, the table is created by the runtime migration.

@pcc/store BudgetReservationStore is self-contained over the raw SQLite
handle:
- Amounts are canonical decimal TEXT compared as BigInt, never a float and
  never in SQL.
- issue runs in ONE BEGIN IMMEDIATE transaction. Issued and consumed
  top-level reservations must fit the request's ceiling. For MC 9 children:
  the parent must be consumed, in the same currency, and issued to the
  parent unit's operator; all children of a unit fit its net n and none
  outlives its reclaimAt; the child's floor is max(parent's, its own).
- consume runs in ONE BEGIN IMMEDIATE transaction. It re-checks principal,
  request, currency, every payer, issued and unexpired, the maximum and the
  floor, then seals the digest. The UPDATE is conditional on
  state = 'issued', and exactly one row must change.
- SQL CHECKs cover canonical digits, the known states, consumed <=> digest,
  min_tier in 0..3, and both halves of a parent binding or neither.

The gateway's services/reservation-store.ts is the trace stand-in's consume
protocol, made durable. Before the store it refuses wrong-binding,
digest-mismatch (the digest is RECOMPUTED from content) and
obligation-mismatch. A resealed plan is still refused by the store's
authority checks.

Tests: db 11 (the db package suite passes with the new migration) and
gateway 5, including the whole route on the durable store with exactly one
of two concurrent accepts sealed. Full gateway suite: 3049 passed, 6
skipped; only the known capture suites fail to load.

Mutation checks: 20 in all, 18 killed. The 2 store survivors are layered
concurrency defenses single-threaded tests cannot separate: IMMEDIATE vs
deferred, and the conditional UPDATE guard. Each is kept as defence in
depth.

pcc-composition 8a0f4de0, goal pcc-reconciliation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…he parent payer (self-review of #402)

Two flaws found in a self-review before the cross-family round:
- An issued reservation past its expiry kept its share of the request
  ceiling, and a child kept its share of the parent unit, until
  housekeeping marked it expired. No housekeeping runs, so the share was
  held forever, although consume already refuses it. Money is now held by
  consumed reservations and by issued ones that are unexpired at the
  moment of issue.
- A child reservation could name the parent's payer. #2301: a child never
  uses the parent payer's credentials. The operator funds its subcontract
  from its own wallet, so this is now refused as
  child-payer-is-parent-payer.

Tests: db 13/13 (2 new) and gateway 5/5. 4 mutation checks, all killed:
expired still holding, the child payer check, consumed not counting, and
the expiry boundary.

pcc-composition 8a0f4de0, goal pcc-reconciliation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu and others added 4 commits September 24, 2026 16:22
…nt #3231, steward conditions #3235)

budget_reservations gains consumed_deal_json, which the consume writes in
the same transaction as the digest.
- (a) The stored bytes are acceptedDealDigest's canonical PREIMAGE, and
  the consume refuses unless sha256(bytes) == the digest
  (deal-preimage-mismatch). A table CHECK makes consumed <=> bytes present.
- (b) The bytes are never part of the reservation object. Only
  sealedDealPreimage(id), server-side, reads them (for MC 9's parent
  terms, funding and VCR delivery).
- (c) At most 1 MiB, enforced by the store and by a table CHECK.

The gateway adapter passes the preimage in the next commit, once #351
exports acceptedDealPreimage.

Tests: db 16/16 (3 new; the helpers now use real preimage/digest pairs).

pcc-composition 8a0f4de0, goal pcc-reconciliation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…mage (amendment #3231)

The consume protocol recomputes the digest as sha256 of #351's
acceptedDealPreimage(plan). The carried digest must equal it
(digest-mismatch otherwise). The store receives both the recomputed digest
and the preimage bytes, re-checks that they hash to each other, and keeps
the bytes as the sealed deal (steward condition (a), #3235).

Tests: gateway reservation-store 5/5; after a real accept, the stored
bytes hash to exactly the sealed digest and parse as the deal. Route tests
17/17. 4 mutation checks on the store's amendment logic, all killed: the
hash unchecked, the bytes not stored, no size bound in the store, the bytes
exposed on the reservation object.

pcc-composition 8a0f4de0, goal pcc-reconciliation.

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

pcc-composition 8a0f4de0, goal pcc-reconciliation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu added a commit that referenced this pull request Sep 24, 2026
… deal (ChatGPT review of 0b8adda)

The operator's cross-family review (ChatGPT, 2026-09-28) of #402/#410/#415 at 0b8adda was DO-NOT-SHIP. This commit fixes the store side of every finding. The routes follow in #410 and #415.

- H1: child terms come from the sealed deal, never the caller.
  - `issue` takes `parent: { reservationId, unit }` only. Inside its IMMEDIATE transaction it:
    1. reads the consumed parent;
    2. re-checks sha256(stored deal) == stored digest;
    3. parses the deal strictly and requires it to be that reservation's deal;
    4. finds the exact unit and derives its operator, net n and reclaimAt.
  - The identity checks come first, so a non-operator only ever sees the not-found family.
  - A child also fits its own request's ceiling.
- H2: durable expiry, and the store's own clock.
  - The clock is injected at construction and read inside each transaction; no caller passes `now`.
  - Before an issue frees an expired reservation's share, it moves that reservation issued -> expired in the same transaction, in both the request scope and the sibling scope.
  - A consume that finds an expiry records it. Consume requires 'issued'.
  - So a skewed clock cannot consume a share that was already re-issued. This is tested on two connections, in both orders and for siblings.
- H3: 0004_budget_reservations.sql is BUDGET_RESERVATIONS_DDL, statement for statement (test-asserted).
  - migrateDatabase runs ensureBudgetReservationsSchema. That rebuilds an EMPTY table of another shape and refuses to boot on one that holds rows.
  - Test: 0004, then the runtime migration, then a real consume.
- M5: the table refuses what its invariants forbid.
  - CHECKs: amount > 0 and <= uint256; a canonical digest; consumed <=> digest <=> deal <=> consumed_at; created < expires; integer times and tier.
  - Triggers: inserts only as issued, and a child only under a consumed parent; updates only issued -> consumed | expired | released, with no term changed; no DELETE.
  - SQL has no sha256, so the trusted readers re-check the hash, parse, and check the reservation.
- M6: the consume seals only a real deal, for this reservation.
  - New @pcc/spec `parseSealedDeal` checks: canonical bytes; the domain; exact keys; lowercase hex; at most 64 units, 16 per job, 16 legs per unit and 16 KiB per unit; the f/n arithmetic; legs == n; total == sum of g; jobId = planId:operator; milestones; stepId = keccak(node); a bijective binding; planHash == canonicalPlanHash.
  - The consume takes the request, currency, payers, obligation and lowest tier FROM THE DEAL. The input is now { reservationId, principal, dealDigest, dealPreimage }.
  - The gateway adapter throws when the store refuses the server's own compiled deal: a generic 500, never a 409.

Tests: spec 909/909, db 222/222, and gateway R13 129/129. The new files typecheck. New tests cover every finding, including the reviewer's exact inputs: a MiB of 'y' with its hash, an invented unit with a forged operator/net/reclaim, 0004 then migrate then consume, and the skewed-clock interleaving.

39 mutation checks: 38 killed. The one survivor is EQUIVALENT: SD14 (the parser's one-job-per-operator check). Two jobs with one operator share a jobId, so their unit refs collide and the bijective binding check refuses the deal. The check is kept for its clearer diagnosis.
A first-run survivor, ST6 (the sibling expiry sweep), was a TEST GAP: every sibling test shared a request. Siblings under two requests now kill it.

pcc-composition 8a0f4de0, goal pcc-reconciliation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… one transaction, refuses a reused id, and the parser enforces assurance structure (astra, round 2 of R13)

astra's review of ac3db65 (pack 45) returned DO-NOT-SHIP. Every finding was
REPRODUCED first at 45d0cde (the review loop): 12 new tests fail there and
pass here.

A (HIGH): issue() re-read the caller's maxAmountBaseUnits for validation,
both ceilings and the insert, so astra's getter (1n three times, then 100n)
passed with 1 and inserted 100 USDC. consume() also re-read reservationId
through its final UPDATE: checks on the caller's reservation consumed SOMEONE
ELSE'S (reproduced), and the stored bytes could differ from those hashed.
Every field, the nested parent reference included, is now read ONCE into an
owned, frozen copy.

B (HIGH): ensureBudgetReservationsSchema inspected, counted, dropped and
created with no transaction. A committed row landing between the count and
the drop was lost (reproduced by interleaving a second connection). It now
runs as ONE immediate transaction. Compatibility is a RECORDED version
(pcc_schema_versions, written by the DDL itself), never SQL text, which had
refused a correct, differently written table (reproduced).

C (MEDIUM): with recursive triggers off, INSERT OR REPLACE reopened a
consumed reservation, and a NULL id was accepted. Both were reproduced. The
insert guard now refuses a reused id, and id is NOT NULL (schema version 2).

D (MEDIUM): parseSealedDeal accepted tier 2 with no program, tier 0 with a
program, and two distinct programs. The store then honoured a forged tier-2
deal under a floor-2 reservation (reproduced). The parser now refuses all
three as "bad-assurance", mirroring the compiler (v2: at most one program;
per job from accepted-deal v3).

Migration 0004 is regenerated from the runtime DDL, statement for statement.

Not in this commit: the compiler accepts reclaimAt above uint64, which the
parser refuses (reproduced). The fix belongs to #351, which has a queued
pack; today the mismatch is fail-closed at consume.

Tests: db 231, spec 912, gateway 3056 passed. The capture suites do not load
without a built @pcc/verifier dist. Mutations: 10 of 10 killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 29, 2026
…3-issue-route

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 29, 2026
…nto feat/mc9-child-reservations

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu and others added 4 commits October 3, 2026 17:53
)

#351 (59ee4ae, on master) holds an omitted quote to the gross floor: under an
economics split, the legs to a node's own payout address must carry at least
the net of its quote, and no quote means the whole gross. The three sealed-deal
fixtures that use a split stated no quote, so on master they no longer compile
(operator-below-quote). The lane's merge-order fold build found it: #402's own
head passes 14/14, the fold fails 3.

Each fixture now quotes exactly what its operator keeps: 9 of 10 USDC under the
10% royalty split (its net equals the operator's leg), and gross minus 15 under
the 16-leg split. The assertions are unchanged; 14/14 both on this branch and
on the fold with master's floor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…creates the table (#402, pack 92 MEDIUM)

The exported BUDGET_RESERVATIONS_DDL (and migrations/0004, statement for
statement) ran CREATE TABLE IF NOT EXISTS and then stamped version 2
unconditionally. Run directly against an existing older table, it left the
table as it was but certified it as v2, and ensureBudgetReservationsSchema
then trusted the false record (reproduced: the reviewer's 4 steps, and a v1
table with the same column names re-stamped from 1 to 2).

The stamp now runs BEFORE the CREATE, and only when no budget_reservations
table exists yet, so it is written only by the run that creates the table.
An existing table, of any age, is never certified by the DDL; the guarded
path alone decides about it. No table, column, index or trigger changes.

Tests: both reproductions (the DDL and the 0004 file), the v1 re-stamp, and
the record surviving a second run. db 235/235; 2/2 mutants killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
#402, pack 252 MEDIUM)

astra 252 (SHIP-WITH-FIXES; the fixture quotes are closed): the presence
checks compared sqlite_master.name = 'budget_reservations' case-sensitively,
while SQLite resolves table names case-insensitively. Reproduced first at
698de5b: a v1 table named Budget_Reservations, recorded as v1, was re-stamped
to 2 by the DDL and by migration 0004, and ensureBudgetReservationsSchema
neither refused nor rebuilt it.

Every sqlite_master name check now uses COLLATE NOCASE: the DDL's guard, the
same guard in 0004, and both lookups in the guarded path (the table and the
version table). NOCASE folds ASCII letters, as SQLite's identifier resolution
does. No table, column, index or trigger change.

Tests: the case-variant re-stamp through both DDL forms, the guarded path
refusing (rows) and rebuilding (empty) a case-variant table, and a
case-variant version table read correctly. db 238/238; 4/4 mutants killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…/r13-budget-reservations (plain merge, no edits; retarget to master; fresh CI)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZpG7S6AyDzTEJBQH8up44
@LamaSu LamaSu changed the title feat(db,gateway): R13 one-use budget reservations — durable store + atomic consume (DRAFT: awaiting operator decision #2240; SCHEMA CHANGE) feat(db,gateway): R13 one-use budget reservations: durable store + atomic consume (SCHEMA CHANGE, approved under operator item 26) Oct 7, 2026
@LamaSu
LamaSu changed the base branch from feat/agent-plans-route to master October 7, 2026 00:16
@LamaSu
LamaSu marked this pull request as ready for review October 7, 2026 00:16
LamaSu added a commit that referenced this pull request Oct 7, 2026
…sue-route (plain merge, no edits)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZpG7S6AyDzTEJBQH8up44
LamaSu added a commit that referenced this pull request Oct 7, 2026
…at/mc9-child-reservations (plain merge, no edits)

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

This branch had an error being deployed

1 failed deployment
trusted-checks — 238da2a6 Deployed Oct 7, 2026 by LamaSu via post-verdicts #202
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant