Skip to content

feat(services): shadow liveness in UserMeter - #1119

Merged
kody-bot merged 26 commits into
mainfrom
cursor/meter-do-38c8
Aug 1, 2026
Merged

kody-bot merged 26 commits into
mainfrom
cursor/meter-do-38c8

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add schema-v5 per-service liveness shadow rows to UserMeter
  • dual-write PackageServiceInstance lifecycle, restore, heartbeat, stop, error, and purge projections
  • serialize/defer shadow hops through ctx.waitUntil
  • keep D1 package_service_states as sole count, discovery, and service_start authority
  • expose first-page-only shadow state in account export

Validation

Deployment notes

Phase A expand only. The authority flip remains a separate high-risk PR after a full 24-hour stale-window soak, parity review, and cold-bootstrap validation.

System recap — extends User meter and package services (medium risk)

Mode: recap · Base: main @ 6a849eaf · Head: 3a0d41d8

Classification: extends — adds non-authoritative package-service liveness shadowing while preserving D1 enforcement authority.

Primitives touched

Primitive Group Impact
user-meter Storage & infrastructure extends — schema-v5 service liveness shadow
package-services User-facing assistant primitives extends — serialized lifecycle and heartbeat dual-write
account-export User-facing assistant primitives extends — additive first-page service shadow

System map

PackageServiceInstance keeps D1 authoritative and shadows ordered lifecycle state into UserMeter.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	service["package-services<br/>Package services"]:::extended
	d1["d1-app-db<br/>D1 app database"]:::untouched
	meter["user-meter<br/>User meter"]:::extended
	service -->|"awaited authoritative lifecycle + heartbeat"| d1
	service -->|"serialized waitUntil shadow queue"| meter
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Invariants

  • D1 remains sole count/discovery/service-start authority
  • shadow writes are per-user, ordered, deferred, and non-rejecting
  • 1-hour heartbeat and 24-hour staleness semantics are unchanged

Conductor report

  • STATUS: done
  • What shipped: UserMeter schema-v5 package-service liveness shadow/cutover support with serialized deferred writes; D1 remains authoritative.
  • Risk: medium — additive dual-write/export only; authority flip remains high risk.
  • Merged/deployed: yes / yes; PR #1119, post-merge validation, and production deploy succeeded.
  • Sibling-track spill: merged and preserved mailbox-do and cron-restructure changes; no additional email/scheduled-lane edits.
  • Dependencies: authority flip requires 24h soak/parity and cold bootstrap. Slice 5 deletion fencing is next and will stop green + ready-for-review without self-merge.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added package-service liveness tracking to account exports and usage data.
    • Included package-service states in initial and first-page exports with pagination support.
    • Added lifecycle synchronization, cleanup, running-service counts, and bootstrap support.
    • Added staleness handling while retaining authoritative service data in the primary system.
  • Documentation

    • Updated architecture and entitlement documentation covering shadows, exports, purging, and authority boundaries.
  • Tests

    • Added coverage for lifecycle transitions, pagination, isolation, stale updates, exports, failures, and cleanup.

cursoragent and others added 22 commits July 31, 2026 22:46
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
# Conflicts:
#	packages/worker/src/email/inbound.ts
#	packages/worker/src/email/outbound.ts
#	packages/worker/worker-configuration.d.ts

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
# Conflicts:
#	docs/contributing/architecture/data-storage.md
#	packages/worker/src/account/export.node.test.ts
#	packages/worker/src/account/export.ts

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds schema v5 package-service liveness shadows to UserMeter. It adds state operations, pagination, counting, bootstrap, export, purge, and RPC support. Package-service lifecycle projections mirror D1 state through non-blocking UserMeter writes.

Changes

Package-service shadow lifecycle

Layer / File(s) Summary
UserMeter state model and RPCs
packages/worker/src/entitlements/user-meter-do.ts, packages/worker/src/test-support/user-meter.ts, packages/worker/src/entitlements/user-meter.workers.test.ts
UserMeter stores validated package-service shadows with monotonic updates, pagination, running counts, bootstrap, deletion, purge, export, and RPC support.
Package-service lifecycle mirroring
packages/worker/src/package-runtime/package-service.ts, packages/worker/src/package-runtime/package-service.node.test.ts
Package-service lifecycle projections schedule best-effort UserMeter upserts and deletions. Failures are logged without failing lifecycle responses or D1 updates.
Export propagation and pagination
packages/worker/src/account/export.ts, packages/worker/src/account/export.node.test.ts, packages/worker/src/account/user-owned-surfaces.ts
Account exports count and return package-service shadows on the first UserMeter page. Continuation pages omit the shadows.
Authority and retention documentation
docs/contributing/architecture/data-storage.md, docs/contributing/architecture/entitlements.md, docs/contributing/architecture/primitives.yaml
Architecture documentation records schema v5, D1 authority, mirroring, export and purge behavior, heartbeat staleness, retention, and identity metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PackageService
  participant D1
  participant UserMeter
  participant DurableObject
  PackageService->>D1: Update package_service_states
  PackageService->>UserMeter: Schedule shadow upsert
  UserMeter-->>PackageService: Apply or log failure
  PackageService->>UserMeter: Schedule shadow deletion during purge
  PackageService->>DurableObject: Delete durable package state
Loading

Possibly related PRs

  • kentcdodds/kody#1115: Extends the same UserMeter, export, purge, and architecture patterns.
  • kentcdodds/kody#1118: Adds related UserMeter shadow, export, purge, and schema-evolution behavior.
  • kentcdodds/kody#965: Uses package_service_states for package-service lifecycle, export, deletion, and discovery state.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding package-service liveness shadows to UserMeter.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/meter-do-38c8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cursoragent and others added 2 commits August 1, 2026 07:58
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
# Conflicts:
#	docs/contributing/architecture/data-storage.md

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot marked this pull request as ready for review August 1, 2026 08:13
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1119.kody-a99.workers.dev

Worker: kody-pr-1119
D1: kody-pr-1119-db
KV: kody-pr-1119-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
packages/worker/src/package-runtime/package-service.node.test.ts (1)

982-1000: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for shadow write ordering during purge.

This block drains the purge tasks in scheduling order, so it cannot detect a reordered delete. handlePurgeRequest schedules a stopped shadow upsert and then a shadow delete as two independent waitUntil tasks. Add a test that settles the delete before the upsert and asserts the shadow row is absent. See the related comment on packages/worker/src/package-runtime/package-service.ts lines 461-469.

Do you want me to generate this test?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/package-runtime/package-service.node.test.ts` around
lines 982 - 1000, Extend the purge test around handlePurgeRequest to settle the
shadow-delete waitUntil task before the stopped-shadow-upsert task, rather than
draining tasks in scheduling order. Assert that packageServiceShadowRow remains
undefined after this reordered settlement and retain the existing delete
assertion.
packages/worker/src/entitlements/user-meter-do.ts (2)

1171-1191: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the bootstrap batch size.

bootstrapPackageServiceStates iterates the whole input.states array and performs one read plus one write per entry. The input size is not capped. A large caller batch can exhaust the Durable Object request CPU budget and leave the shadow table partially seeded. Add an explicit maximum batch size and reject larger inputs, so callers page the cold bootstrap.

♻️ Proposed cap
+const maxPackageServiceBootstrapStates = 500
+
 	/** Cutover-support bulk seed; same monotonic guard as upsert. */
 	async bootstrapPackageServiceStates(input: {
 		states: ReadonlyArray<{
@@
 	}): Promise<UserMeterPackageServiceBootstrapResult> {
+		if (input.states.length > maxPackageServiceBootstrapStates) {
+			throw new Error(
+				`UserMeter bootstrapPackageServiceStates accepts at most ${maxPackageServiceBootstrapStates} states per call.`,
+			)
+		}
 		let applied = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/entitlements/user-meter-do.ts` around lines 1171 - 1191,
Update bootstrapPackageServiceStates to enforce an explicit maximum size for
input.states before processing any entries, rejecting batches above that limit
and preserving the existing applied/skipped upsert behavior for valid batches.
Define or reuse a named cap near the method rather than silently truncating the
input.

1262-1269: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the first-page shadow inventory.

listAllPackageServiceRows reads every shadow row and returns them in one export page. Unlike storageBytesShadow, this inventory grows with the number of package services for the user. countUserMeterExportEntries in packages/worker/src/account/export.ts (lines 211-219) adds that full length to the section item count, so one page can exceed the requested pageSize by an unbounded amount. Consider reading the inventory with a hard LIMIT and reporting truncation, or paging it with listPackageServiceStates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/entitlements/user-meter-do.ts` around lines 1262 - 1269,
The first-page shadow inventory in the export flow can exceed the requested
pageSize because listAllPackageServiceRows returns every package-service row at
once. Update the shadow handling around includeShadows and
packageServiceStatesShadow to bound or page these rows using the existing
listPackageServiceStates path, and ensure countUserMeterExportEntries reports
the resulting truncation or pagination consistently with the emitted entries.
packages/worker/src/package-runtime/package-service.ts (1)

490-492: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use distinct log labels for the upsert and delete failures.

Both handlers log package-service-user-meter-shadow-failed. An operator cannot tell a failed mirror write from a failed mirror delete. A failed delete leaves an orphan shadow row, so it needs its own label. Also include packageId and serviceName in the log so the failed row is identifiable.

Also applies to: 519-521

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 490 -
492, Update the error handlers around the upsert and delete operations to use
distinct log labels, with the delete path indicating a shadow-delete failure
rather than reusing the upsert label. Include packageId and serviceName in both
console.warn calls so the affected shadow row is identifiable.
packages/worker/src/test-support/user-meter.ts (1)

236-286: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mirror the Durable Object input validation in the stub.

The stub validates status only. The real UserMeterBase also rejects empty or over-long packageId and serviceName through assertPackageServiceId, and rejects an empty sourceUpdatedAt through assertSourceUpdatedAt. Node tests that use this stub therefore pass with inputs that the production Durable Object rejects. Add the same identifier and timestamp checks so the double keeps parity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/test-support/user-meter.ts` around lines 236 - 286,
Update upsertPackageServiceState to mirror UserMeterBase validation by calling
the existing assertPackageServiceId checks for packageId and serviceName and
assertSourceUpdatedAt for sourceUpdatedAt before status processing or state
mutation. Preserve the existing status validation and upsert behavior while
ensuring empty and over-long identifiers and empty timestamps are rejected
consistently.
docs/contributing/architecture/data-storage.md (1)

267-269: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Qualify the store in both authority statements.

package_service_states has two stores. The D1 table is authoritative. The UserMeter schema-v5 table is a shadow. The text at Lines 267-269 does not repeat the UserMeter qualifier. Line 531 says D1 remains sole authority for reads without limiting reads to package-service liveness. UserMeter daily_counters remain authoritative, so this wording can mislead future cutover work.

Use explicit store and read-scope names. This preserves the authority split stated in docs/contributing/architecture/entitlements.md Lines 127-132 and 233-267.

Proposed wording
- UserMeter `storage_bytes_state` (schema v4) and `package_service_states` (schema v5) are
+ UserMeter `storage_bytes_state` (schema v4) and UserMeter `package_service_states` (schema v5) are

- D1 remains sole authority for reads, running counts, discovery, and
+ D1 remains sole authority for package-service liveness reads, running counts, discovery, and

Also applies to: 529-532

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/architecture/data-storage.md` around lines 267 - 269,
Qualify both authority statements in the architecture documentation: identify
the UserMeter schema-v5 `package_service_states` table as a shadow while keeping
the D1 `package_service_states` table authoritative, and limit “D1 remains sole
authority for reads” to package-service liveness reads. Preserve UserMeter
`daily_counters` as authoritative and align the wording with the authority split
in the Entitlements documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/account/export.ts`:
- Around line 302-306: Update the documentation for packageServiceStatesShadow
in the UserMeter export type to state that continuation user_meter pages return
null rather than omitting the field, matching the existing branch behavior and
export.node.test.ts expectation. Do not change the implementation or test.

In `@packages/worker/src/entitlements/user-meter-do.ts`:
- Around line 288-295: Update assertSourceUpdatedAt to validate that
sourceUpdatedAt is a canonical ISO-8601 UTC timestamp, not merely a non-empty
string, so values used by upsertPackageServiceState and
countRunningPackageServices preserve lexicographic ordering. Reject malformed,
offset-based, or non-UTC inputs while continuing to return valid new
Date().toISOString()-style values.

In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 461-469: Serialize shadow upserts and deletes per instance instead
of starting independent waitUntil tasks. Update
schedulePackageServiceStateShadow and schedulePackageServiceShadowDelete to
append each shadow operation to a shared promise chain in scheduling order,
while retaining the existing userMeterNamespace guard and waitUntil lifecycle
handling.

---

Nitpick comments:
In `@docs/contributing/architecture/data-storage.md`:
- Around line 267-269: Qualify both authority statements in the architecture
documentation: identify the UserMeter schema-v5 `package_service_states` table
as a shadow while keeping the D1 `package_service_states` table authoritative,
and limit “D1 remains sole authority for reads” to package-service liveness
reads. Preserve UserMeter `daily_counters` as authoritative and align the
wording with the authority split in the Entitlements documentation.

In `@packages/worker/src/entitlements/user-meter-do.ts`:
- Around line 1171-1191: Update bootstrapPackageServiceStates to enforce an
explicit maximum size for input.states before processing any entries, rejecting
batches above that limit and preserving the existing applied/skipped upsert
behavior for valid batches. Define or reuse a named cap near the method rather
than silently truncating the input.
- Around line 1262-1269: The first-page shadow inventory in the export flow can
exceed the requested pageSize because listAllPackageServiceRows returns every
package-service row at once. Update the shadow handling around includeShadows
and packageServiceStatesShadow to bound or page these rows using the existing
listPackageServiceStates path, and ensure countUserMeterExportEntries reports
the resulting truncation or pagination consistently with the emitted entries.

In `@packages/worker/src/package-runtime/package-service.node.test.ts`:
- Around line 982-1000: Extend the purge test around handlePurgeRequest to
settle the shadow-delete waitUntil task before the stopped-shadow-upsert task,
rather than draining tasks in scheduling order. Assert that
packageServiceShadowRow remains undefined after this reordered settlement and
retain the existing delete assertion.

In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 490-492: Update the error handlers around the upsert and delete
operations to use distinct log labels, with the delete path indicating a
shadow-delete failure rather than reusing the upsert label. Include packageId
and serviceName in both console.warn calls so the affected shadow row is
identifiable.

In `@packages/worker/src/test-support/user-meter.ts`:
- Around line 236-286: Update upsertPackageServiceState to mirror UserMeterBase
validation by calling the existing assertPackageServiceId checks for packageId
and serviceName and assertSourceUpdatedAt for sourceUpdatedAt before status
processing or state mutation. Preserve the existing status validation and upsert
behavior while ensuring empty and over-long identifiers and empty timestamps are
rejected consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a26de87-65f0-4277-9847-bf059fbb5b03

📥 Commits

Reviewing files that changed from the base of the PR and between faf6d2b and 393b2a4.

📒 Files selected for processing (11)
  • docs/contributing/architecture/data-storage.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/primitives.yaml
  • packages/worker/src/account/export.node.test.ts
  • packages/worker/src/account/export.ts
  • packages/worker/src/account/user-owned-surfaces.ts
  • packages/worker/src/entitlements/user-meter-do.ts
  • packages/worker/src/entitlements/user-meter.workers.test.ts
  • packages/worker/src/package-runtime/package-service.node.test.ts
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/test-support/user-meter.ts

Comment thread packages/worker/src/account/export.ts
Comment thread packages/worker/src/entitlements/user-meter-do.ts
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
# Conflicts:
#	docs/contributing/architecture/data-storage.md

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
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.

3 participants