Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3c21148
perf(entitlements): cache enforcement plan lookups
cursoragent Jul 31, 2026
7d5d777
style(docs): format entitlement cache notes
cursoragent Jul 31, 2026
0cd468d
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Jul 31, 2026
59a8238
feat(entitlements): add per-user quota meter
cursoragent Aug 1, 2026
8ebf663
feat(account): inventory UserMeter lifecycle
cursoragent Aug 1, 2026
4a60dd9
test(entitlements): provide UserMeter fixtures
cursoragent Aug 1, 2026
73a8e37
test(entitlements): clean meter test imports
cursoragent Aug 1, 2026
8b001bf
style(mcp): format meter gateway tests
cursoragent Aug 1, 2026
df32168
fix(email): read daily usage from UserMeter
cursoragent Aug 1, 2026
ae07b85
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
e6622a4
docs(entitlements): clarify meter cutover semantics
cursoragent Aug 1, 2026
7628aaf
fix(entitlements): address UserMeter review feedback
cursoragent Aug 1, 2026
468ee51
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
a8cba2b
test(entitlements): add RunLog admin fixture
cursoragent Aug 1, 2026
edfd973
test(run-log): isolate usage counts per user
cursoragent Aug 1, 2026
2307d37
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
b18246f
feat(entitlements): shadow storage bytes in UserMeter
cursoragent Aug 1, 2026
eaaa509
docs(entitlements): inventory storage shadow
cursoragent Aug 1, 2026
3c13b53
fix(entitlements): make storage reserves fail closed
cursoragent Aug 1, 2026
68256ea
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
e454e76
feat(services): shadow liveness in UserMeter
cursoragent Aug 1, 2026
9f7be03
docs(services): inventory liveness shadow
cursoragent Aug 1, 2026
581b896
perf(services): defer liveness shadow writes
cursoragent Aug 1, 2026
393b2a4
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
12e7c02
fix(services): serialize liveness shadows
cursoragent Aug 1, 2026
3a0d41d
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
194d5b5
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
5e8d1b9
feat(account): shadow deletion leases in UserMeter
cursoragent Aug 1, 2026
2e92f03
docs(account): inventory deletion shadow
cursoragent Aug 1, 2026
c566c54
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
62f6078
fix(account): reconcile and sanitize deletion shadows
cursoragent Aug 1, 2026
1116e07
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
d20696d
feat(account): move write leases into UserMeter
cursoragent Aug 1, 2026
6c2effd
docs(account): describe UserMeter lease authority
cursoragent Aug 1, 2026
afe0bb2
test(account): provide authoritative meter fixtures
cursoragent Aug 1, 2026
29dc257
test(account): wire meter into workers fixtures
cursoragent Aug 1, 2026
11814af
fix(account): clean partial lease mirrors
cursoragent Aug 1, 2026
7e88800
Merge remote-tracking branch 'origin/main' into cursor/meter-do-38c8
cursoragent Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions docs/contributing/account-write-lease-repair.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Account write leases do not expire. A crashed writer intentionally blocks
account deletion until an administrator verifies that the process is gone and
releases its exact token with an audit reason.

Active leases may live in D1 (email / no-`env` writers) and/or UserMeter
(`authority='do'` when callers supply `USER_METER`). Admin list unions both,
deduped by token.

First inspect active leases:

```javascript
Expand All @@ -24,7 +28,12 @@ await kody.admin_account_write_lease_repair({
})
```

The repair writes `account_write_lease_repairs` before releasing the token.
Wrong-user, stale timestamp, already-released token, or short reason requests
fail closed. Retry account deletion only after inspection shows no active
leases.
The repair writes `account_write_lease_repairs` before releasing the token. D1
leases use an atomic audit-before-delete batch. DO-authority leases use prepare
(stable `repairId`, lease stays held) → audit insert/verify → finalize DO
deletion → clear the temporary D1 rollout mirror. The D1 mirror is never cleared
while the DO lease remains held. Retries after a lost finalize response succeed
when the matching audit exists and the DO lease is already gone (idempotently
clearing any stale D1 mirror). Wrong user, stale timestamp, or short reason
requests fail closed. Retry account deletion only after inspection shows no
active leases.
35 changes: 17 additions & 18 deletions docs/contributing/architecture/data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ Naming matches `RunLog` and `JobManager`: one object per untrimmed stable MCP
column inside the DO because the object identity is the user.

SQLite ownership (schema version tracked in `user_meter_meta`; current version
**6**):
**7**):

- `daily_counters` — authoritative UTC-day counters for `email_sends_per_day`,
`email_receives_per_day`, `execute_calls_per_day`, and
Expand All @@ -611,29 +611,28 @@ SQLite ownership (schema version tracked in `user_meter_meta`; current version
(`listPackageServiceStates`, `countRunningPackageServices`,
`bootstrapPackageServiceStates`) mirror D1 semantics for future parity review
only.
- `deletion_state` / `account_write_leases` — **shadow** of D1
`users.deleting_at` and active `account_write_leases` (singleton
`deleting_at`; lease rows `token` / `holder` / `acquired_at`). Added in schema
v6. Populated by optional best-effort dual-writes from
`account/deletion-state.ts` after successful D1 mark/acquire/release/repair;
never read for fencing, list, repair, or drain in expand-phase slice 5 Phase
A. On mark, after D1 `deleting_at` is set, the helper loads active D1 leases
and calls `shadowReplaceDeletionState` (DO-serialized tombstone set/preserve +
exact lease-set replace) so stale unreleased shadow rows are cleared when D1
drain reaches zero and active D1 leases are preserved. `purge()` preserves an
existing deleting tombstone across `deleteAll` so a later cutover cannot
reopen writes. Cutover-support RPCs (`readDeletionState`, `listWriteLeases`,
`countActiveWriteLeases`, `bootstrapDeletionState`) exist for future parity
review only; account export emits a sanitized `deletionShadow` without raw
token/holder.
- `deletion_state` / `account_write_leases` — deletion tombstone plus write
leases (singleton `deleting_at`; lease rows `token` / `holder` / `acquired_at`
/ `authority` (`do`|`legacy`) / `pending_repair_id`). Added as shadow in
schema v6; schema v7 is the authority cutover. When callers supply
`USER_METER`, `authority='do'` rows are authoritative for acquire/held/release
and admin union list; `authority='legacy'` rows are the D1 lease snapshot
replaced by `markDeleting`. During rolling deploy, DO-authority acquires also
mirror the same token into D1 `account_write_leases` so Phase-A marks that
only read D1 still observe them (temporary; not the final hot path). D1
`users.deleting_at` remains the permanent point gate; email paths omit `env`
and keep exact D1 leases. `purge()` preserves an existing deleting tombstone
across `deleteAll`. Account export emits a sanitized `deletionShadow` without
raw token/holder.

Retention is self-enforced inside the DO: every read/write path
opportunistically deletes counter and claim rows older than seven UTC days
(`userMeterDailyCounterRetentionDays`). Enforcement only needs the current day;
the window covers timezone edge cases, recent account exports, and inbound
retries. Shadow storage-byte and package-service liveness rows are not
time-pruned. Deletion-fence lease shadows are bounded by the D1-backed mark
replace path above rather than time retention.
time-pruned. Deletion-fence legacy lease rows are bounded by the D1 snapshot
replace on `markDeleting` rather than time retention; DO-authority rows clear on
release/repair/purge.

**Expand-phase D1 mirrors (daily counters only):** enforcement and point reads
are authoritative in UserMeter for daily counters. D1
Expand Down
132 changes: 68 additions & 64 deletions docs/contributing/architecture/entitlements.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,12 @@ v5 adds an optional per-service shadow table as **best-effort future-cutover
support only** — see
[Package service liveness — UserMeter shadow](#package-service-liveness--usermeter-shadow-expand-phase-slice-4-phase-a).

**D1 account-deletion write fencing** (`users.deleting_at` /
`account_write_leases`) stays authoritative for fencing, list, repair, and
drain. UserMeter schema v6 adds an optional deletion-fence / write-lease shadow
as **best-effort future-cutover support only** — see
[Account-deletion write fencing — UserMeter shadow](#account-deletion-write-fencing--usermeter-shadow-expand-phase-slice-5-phase-a).
**Account-deletion write fencing** uses a split authority model after slice 5
Phase B: D1 `users.deleting_at` remains the permanent point gate; callers that
supply `USER_METER` treat UserMeter (`authority='do'`) as authoritative for
lease acquire/held/release/drain count, while email/transition callers that omit
`env` keep the exact D1 `account_write_leases` path. See
[Account-deletion write fencing — UserMeter authority](#account-deletion-write-fencing--usermeter-authority-expand-phase-slice-5-phase-b).

StorageRunner bucket `estimatedBytes` and the per-bucket inventory in
`user_storage_buckets` stay a **separate** quota component. StorageRunner write
Expand Down Expand Up @@ -282,60 +283,63 @@ remains on D1.
**Account purge:** `UserMeter.purge()` clears package-service shadow rows with
the rest of DO state via `deleteAll`.

### Account-deletion write fencing — UserMeter shadow (expand phase slice 5, Phase A)

D1 `users.deleting_at`, `account_write_leases`, and
`account_write_lease_repairs` remain the **sole authority** for deletion
fencing, lease acquire/release, active-lease list, audited repair, and deletion
drain waits in this PR. Nothing in Phase A switches `withAccountWriteLease`,
`markAccountDeleting`, `listActiveAccountWriteLeases`, or
`repairAccountWriteLease` onto UserMeter reads — public errors, ALS nested-lease
reuse, and D1 `waitUntil` release semantics stay identical when optional `env`
is omitted.

UserMeter schema **v6** adds a singleton `deletion_state.deleting_at` tombstone
plus per-token `account_write_leases` rows (`token`, `holder`, `acquired_at`) as
**best-effort shadow / future-cutover support only**. User scope is the DO
identity — there is no `user_id` column. Shadow rows are never read for fencing,
list, repair, or drain in this slice.

**Optional dual-write from `account/deletion-state.ts`:** after a **successful**
D1 mark/acquire/release/repair, callers may pass optional `env?: UserMeterEnv`
(and `waitUntil` when available). The helper schedules a best-effort UserMeter
shadow (`shadowReplaceDeletionState` on mark, `shadowAcquireWriteLease`,
`shadowReleaseWriteLease`). After D1 `deleting_at` is set, mark loads the
authoritative active D1 lease rows and calls `shadowReplaceDeletionState`, which
under DO serialization sets/preserves the tombstone then deletes and re-inserts
exactly that lease set — so a prior failed shadow release cannot leave stale
rows, and a deletion retry with zero D1 leases clears the shadow set. Acquire
shadows start during the write; ordered release shadows run after authoritative
D1 release. When `waitUntil` is provided, shadows detach through it; when
omitted, mark/release/repair shadows are awaited (still non-rejecting) so
middleware and other non-`waitUntil` callers cannot return with a stale shadow
lease or missing tombstone. Missing `USER_METER` is a no-op; failures log
`account-deletion-user-meter-shadow-failed` and never alter D1 results or
errors. Mark is monotonic (first `deleting_at` wins). Acquire is rejected when a
deleting tombstone is present and is idempotent for an already-held token.
Cutover/list RPCs (`listWriteLeases`, `bootstrapDeletionState`) may still carry
token/holder for parity review.

**Account export:** `UserMeter.exportCounters` returns additive `deletionShadow`
on the first page only (`startAfter` absent); later pages return `null`. The
export shape is sanitized (`deletingAt`, `activeWriteLeaseCount`, and
`writeLeases` with `acquiredAt` only — no raw token or holder). Section totals
count the tombstone (when set) plus `activeWriteLeaseCount`; the field is
explicitly non-authoritative — authoritative fencing remains on D1.

**Account purge:** `UserMeter.purge()` clears counters, claims, storage shadow,
package-service shadow, and write-lease shadow via `deleteAll`, then restores
any pre-existing deleting tombstone so a later authority cutover cannot reopen
writes for a purged account.

Non-email production call sites that hold `env` pass it into deletion-state APIs
(`withAccountWriteLease`, `markAccountDeleting`, `repairAccountWriteLease`) and
forward existing `waitUntil` when available. Email paths stay untouched in this
slice (`system:email` skip unchanged). Assert-only readers
(`assertAccountWritable*`, `listActiveAccountWriteLeases`) remain D1-only.
### Account-deletion write fencing — UserMeter authority (expand phase slice 5, Phase B)

UserMeter schema **v7** stores `account_write_leases.authority` (`do` |
`legacy`; rows created under v6 default to `legacy`) and `pending_repair_id` for
audit-safe DO repair.

**Split authority:**

- D1 `users.deleting_at` remains the **permanent point gate** (auth projection /
purge failures fail closed). Assert-only readers (`assertAccountWritable*`)
stay D1-only. D1 `account_write_leases` / `account_write_lease_repairs` tables
are **not** retired.
- When `env.USER_METER` is supplied (all non-email Phase-A-wired callers),
UserMeter is **authoritative** for lease acquire / held / release via
`acquireWriteLease` / `assertWriteLeaseHeld` / `releaseWriteLease`
(`authority='do'`). Missing binding or DO failures **fail closed**.
- When `env` is omitted (email transition), retain the **exact D1 lease path**
(including `active_write_count`, ALS nested reuse, and `waitUntil` release).

**Temporary rolling-version D1 mirror (not the final hot path):** while older
isolates may still run Phase-A mark (D1-only lease counts), every DO-authority
acquire also inserts the **same** `token` / `holder` / `acquiredAt` into D1
`account_write_leases` and bumps `active_write_count` with the existing atomic
batch + lost-response reconcile. If that D1 mirror cannot be confirmed, the DO
lease is released and acquire fails closed. Authoritative release clears the DO
lease first, then the D1 mirror/`active_write_count` (same `waitUntil`
detachment semantics) so a stale D1 row may overblock old marks but never
underblock. After old isolates drain, remove this mirror (and later the D1 lease
inventory) from the hot path.

**`markAccountDeleting`:** always `COALESCE`s D1 `deleting_at` first, then loads
live D1 leases (including temporary DO mirrors). With `env`, calls authoritative
`markDeleting` which sets/preserves the DO tombstone, replaces **only legacy**
rows with that exact D1 snapshot, preserves DO-authority rows, and returns the
deduped-by-token union count used for drain waits. Without `env`, returns the D1
lease count unchanged (so Phase-A marks observe DO acquires via the mirror).

**Admin list / repair:** `listActiveAccountWriteLeases(db, userId, env?)` with
`env` unions live D1 leases + DO-authority leases (dedupe by token, same
`acquired_at, token` order); without `env` exact D1 behavior. DO repair is
audit-first and idempotent: prepare (stable `repairId`, lease stays held) →
insert/verify D1 audit → finalize exact pending DO repair → then idempotently
clear matching D1 mirror/count. The D1 mirror is never cleared while the DO
lease remains held. Finalize failure without commit leaves DO + D1 mirror held
and fails closed; retry resumes the pending repair. Retry after finalize commit
/ lost response returns success when the matching audit exists and the DO lease
is absent (clears any stale D1 mirror; never falls through to a mismatched D1
atomic batch). Post-write held checks treat pending repair as held until
finalize, then surface `AccountWriteLeaseLostError`. Pure D1 leases keep the
existing atomic audit-before-release batch.

**Account export / purge:** first-page sanitized `deletionShadow` still omits
raw token/holder. `purge()` clears leases/counters/shadows via `deleteAll` then
restores any deleting tombstone; D1 `deleting_at` remains the gate.

Public errors, ALS nested-lease reuse, holder strings, and `waitUntil` release
detachment stay parity-compatible with the pre-cutover surface.

### Future package-service authority flip (contract follow-up)

Expand All @@ -361,11 +365,11 @@ Only then do reads, reserves, and reconciliation switch to UserMeter-first with
D1 as mirror/cursor. Until that flip, shadow divergence is acceptable; D1
remains the contract.

**Remaining UserMeter expand roadmap:** slice 5 Phase A — account-deletion write
fencing shadow (this PR; D1 authority unchanged; non-email dual-write wired).
Package-service and storage authority flips are separate high-risk contract
follow-ups after soak/parity review. Deletion-fence authority flip is a separate
high-risk contract follow-up after shadow/D1 parity review.
**Current UserMeter cutover:** slice 5 Phase B moves non-email write leases into
UserMeter authority while retaining the D1 `deleting_at` gate and temporary
same-token rollout mirrors; email keeps its D1 lease path. Package-service and
storage authority flips remain separate high-risk contract follow-ups after
soak/parity review.

**Daily-counter mirror retirement:** dropping D1 `entitlement_daily_counters`
waits until reporting-off-D1 work merges and mirror parity is verified in
Expand Down
Loading
Loading