Skip to content

Scale hardening: R2 email blobs, derived usage rollups, bounded cron/DO work, cheap search paths - #669

Merged
kody-bot merged 15 commits into
mainfrom
cursor/scale-fixes-tier1-tier2-6b11
Jul 8, 2026
Merged

kody-bot merged 15 commits into
mainfrom
cursor/scale-fixes-tier1-tier2-6b11

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 8, 2026 •

Copy link
Copy Markdown
Owner

Implements the approved Tier 1 and Tier 2 fixes from the scaling audit.

What changed

D1 size & write amplification (Tier 1)

  • Raw email MIME moves to a new EMAIL_BLOBS R2 bucket (email-raw:v1:{userId}/{messageId}, migration 0054); D1 keeps metadata plus raw_mime_key, with inline fallback when the binding is missing. Reads go through loadRawMime, deletes remove the blob, account deletion purges user blobs, and CI resource tooling provisions the bucket.
  • recordUsage writes only the Analytics Engine data point in production; usage_rollups is now a derived aggregate recomputed hourly from the AE SQL API (aggregateUsageRollups), with a @epic-web/cachified KV-backed read cache on the admin usage pages. Local dev without AE keeps the direct D1 upsert.

Retention & reconcile throughput (Tier 1)

  • Hourly retention drains batches round-robin under a 20s time budget and covers previously-exempt growth tables: email_messages (365d, R2-blob aware), entitlement_daily_counters (400d), usage_rollups (24mo). The whole-table ROW_NUMBER() prune is replaced with a bounded cap-pair pass. Supporting indexes land in migration 0055.
  • Artifact push reconcile loops batches under a 60s budget with an unchanged-HEAD fast path (no RepoSession DO spin-up for unchanged sources). Scheduled lanes are failure-isolated: each lane logs and reports to Sentry independently instead of one rejection failing the cron.

Latent bombs & DO hot spots (Tier 2)

  • stable_user_id: the scan fallback now write-backs healed ids, and POST /__maintenance/backfill-stable-user-ids backfills legacy rows (keyset paged). Docs drift about inbound-email scanning corrected.
  • JobManager processes at most 25 due jobs per alarm and re-arms in 1s while a backlog remains; runRepoChecks caps source walks at 2,000 files / 15MiB with an actionable check failure; package service auto-start retries use exponential backoff with jitter (5s → 15min cap, persisted across eviction).
  • Package search ranks D1 projections fused with the already-written package vectors and hydrates full source only for returned matches; memory search queries Vectorize first and hydrates top hits by id; community search pre-filters in SQL and bounds candidates at 500.
  • Account export and maintenance reindex are keyset-paginated at the SQL level (500/200 rows per query).
  • Cold start: the static capability registry builds lazily, and OG image rendering (satori + resvg wasm) loads dynamically. Legacy retriever scope-index KV blobs are no longer written; stale retriever manifest revisions and source-snapshot KV keys are pruned.

Notes for reviewers

  • email_messages.raw_mime stays readable for legacy rows; new inbound mail stores the blob key instead.
  • The AE aggregation no-ops without USAGE_EVENTS + CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN.
  • Recommended post-deploy one-time call (idempotent): POST /__maintenance/backfill-stable-user-ids with the maintenance bearer secret.
System recap — adds a new primitive (high risk)

Mode: recap · Base: main @ 27b28a4 · Head: 0a0a859

Classification: adds — introduces the email-blobs-r2 storage primitive (new R2 bucket + frozen key contract); several existing primitives are extended. primitives.yaml is updated in this PR.

Primitives touched

Primitive Group Impact
email-blobs-r2 storage adds — new R2 bucket, email-raw:v1:{userId}/{messageId} key contract
email assistant extends — raw MIME offload, blob-aware reads/deletes
usage-metering storage extends — AE-only writes, hourly derived rollups, KV read cache
scheduled-cron surfaces extends — lane isolation, time budgets, usage aggregation lane
d1-app-db storage extends — migrations 0054 (raw_mime_key) and 0055 (indexes)
jobs assistant extends — 25 due jobs per alarm + backlog re-arm
repo-sessions runtime extends — source-walk caps in repo checks
package-services assistant extends — crash-loop exponential backoff
mcp-server surfaces extends — search uses projections + package/memory vectors
community-listings assistant extends — bounded SQL-prefiltered search
account-export assistant extends — SQL keyset pagination
vectorize-search storage composes — package/memory vectors now queried by search
bundle-artifacts-kv storage extends — snapshot pruning, legacy retriever blob retired
capability-registry assistant extends — lazy static registry build
entitlements auth composes — daily-counter retention only

System map

flowchart LR
	mcpServer["mcp-server"]:::extended
	scheduledCron["scheduled-cron"]:::extended
	email["email"]:::extended
	emailBlobs["email-blobs-r2"]:::added
	usageMetering["usage-metering"]:::extended
	d1AppDb["d1-app-db"]:::extended
	jobs["jobs"]:::extended
	repoSessions["repo-sessions"]:::extended
	packageServices["package-services"]:::extended
	vectorize["vectorize-search"]:::touched
	bundleKv["bundle-artifacts-kv"]:::extended
	accountExport["account-export"]:::extended
	community["community-listings"]:::extended
	appUi["app-ui"]:::untouched
	email --> emailBlobs
	email --> d1AppDb
	scheduledCron --> usageMetering --> d1AppDb
	scheduledCron --> d1AppDb
	scheduledCron --> repoSessions
	mcpServer --> vectorize
	mcpServer --> community
	jobs --> d1AppDb
	packageServices --> d1AppDb
	accountExport --> d1AppDb
	mcpServer --> bundleKv
	appUi --> accountExport
	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

Before / after

Path Before After
Inbound email raw MIME inline email_messages.raw_mime (D1) R2 blob + raw_mime_key (inline fallback)
recordUsage (prod) AE data point + D1 upsert per event AE data point only; hourly derived rollup
Retention 1 batch/table/hour (~1,850 rows) round-robin loop under 20s budget + 3 new policies
Reconcile 50 sources per 5-min tick batches under 60s budget, unchanged-HEAD fast path
Due jobs all due jobs in one alarm 25/alarm + 1s backlog re-arm
Package search full source of every package per query D1 projections + package vectors; hydrate top matches
Account export SELECT * whole tables rowid keyset pages of 500

Invariants

  • per-user-isolation: preserved — R2 keys are user-prefixed (email-raw:v1:{userId}/...), package/memory vector queries filter by userId, and account deletion covers the new blob surface.
  • canonical-repo-source: untouched; snapshot KV pruning only removes non-current-commit derived artifacts.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Raw email MIME can now be offloaded to R2, with an automatic inline fallback when blob storage isn’t available.
    • Account deletion, email/system-email retention, and message cleanup now remove stored raw MIME blobs when possible.
    • Added a maintenance endpoint to backfill legacy stable user IDs.
  • Improvements

    • Usage metering now derives hourly rollups from Analytics Engine; admin usage reads can be KV-cached when available.
    • Community listing search/browse and D1 export paging now use bounded, keyset-style retrieval.
    • Scheduled jobs run in isolated lanes with time/batch budgeting.
  • Bug Fixes

    • Blob deletion failures degrade safely with warnings, without blocking related row cleanup.

cursoragent and others added 9 commits July 8, 2026 07:11
Store inbound raw MIME in R2 (email-raw:v1:{userId}/{messageId}) with
raw_mime_key on email_messages, falling back inline when the binding is
missing. Reads go through loadRawMime, deletes remove the blob, account
deletion enumerates and purges user blobs, and CI resource tooling
provisions the bucket.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
recordUsage writes only the AE data point when USAGE_EVENTS is bound;
usage_rollups become an hourly derived aggregate (aggregateUsageRollups
over the AE SQL API) with a cachified KV read cache on the admin usage
pages. The stable-user-id scan fallback now writes back healed ids and a
maintenance endpoint backfills legacy rows.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Hourly retention now drains batches round-robin under a 20s budget, adds
policies for email_messages (365d, R2-blob aware), entitlement daily
counters (400d) and usage_rollups (24mo), replaces the whole-table
ROW_NUMBER prune with a bounded cap-pair pass, prunes stale source
snapshot KV keys, batches system-email deletes, and stops writing the
legacy retriever scope index blob. Adds supporting indexes.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Cap due jobs at 25 per JobManager alarm with a 1s backlog re-arm, cap
runRepoChecks source walks at 2000 files / 15MiB with an actionable
check failure, and give package service auto-start retries exponential
backoff with jitter (5s doubling to 15min, persisted across eviction).

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Package search ranks D1 projections fused with the existing package
vectors and hydrates full source only for returned matches; memory
search queries Vectorize first and hydrates top hits by id; community
search pre-filters in SQL and bounds candidates at 500.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Account export reads keyset-paged rowid windows (500 rows per query)
instead of whole tables, and the memory/job/package reindex drivers
process keyset-paged batches of 200 with merged result reporting.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Scheduled lanes log and report to Sentry independently instead of
rethrowing, usage aggregation joins the cron, artifact reconcile loops
batches under a 60s budget with an unchanged-HEAD fast path, the static
capability registry builds lazily, and OG image rendering (satori +
resvg wasm) loads dynamically.

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 Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb9c4e68-6434-46d0-ad8b-ee5b52a0f1b7

📥 Commits

Reviewing files that changed from the base of the PR and between df72712 and b776471.

📒 Files selected for processing (11)
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/app/retention.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/email/system-email.ts
  • packages/worker/src/email/system-email.workers.test.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.node.test.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/repo/entity-sources.ts
  • packages/worker/src/usage/aggregate-rollups.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.ts
📝 Walkthrough

Walkthrough

This PR adds R2-backed raw email MIME storage, expands retention pruning and usage rollups, converts several listings and reindex flows to keyset pagination, isolates scheduled cron lanes, adds stable-user-id healing/backfill, and updates supporting CI/config/docs.

Changes

Email raw MIME R2 storage

Layer / File(s) Summary
Schema and repository storage
packages/worker/migrations/0054-...sql, packages/worker/src/email/repo.ts, packages/worker/src/email/types.ts, packages/worker/src/email/test-schema.ts
Adds raw_mime_key, offloads raw MIME to R2 with inline fallback, and resolves or deletes blobs through the email repository.
Inbound, outbound, admin, and capability wiring
packages/worker/src/email/inbound.ts, packages/worker/src/email/outbound.ts, packages/worker/src/app/admin-system-email-data.ts, packages/worker/src/mcp/capabilities/*, packages/worker/src/package-invocations/service.ts, tests
Threads EMAIL_BLOBS through email ingestion, send, admin, and attachment access paths, with offload/fallback coverage.
Account deletion blob cleanup
packages/worker/src/app/account-deletion.ts, packages/worker/src/app/account-deletion.node.test.ts
Enumerates and best-effort deletes raw MIME blobs during account deletion, reporting deletedEmailBlobs and warnings.
Config, docs, and CI provisioning
packages/worker/worker-configuration.d.ts, packages/worker/wrangler.jsonc, docs/contributing/architecture/*, tools/ci/*
Adds EMAIL_BLOBS bindings, updates architecture docs, and provisions R2 buckets in CI tooling.

Retention scheduler and pruning

Layer / File(s) Summary
Constants and policies
packages/worker/migrations/0055-...sql, packages/worker/src/app/retention.ts
Adds retention indexes, constants, policy inventory updates, and a memory exemption.
Per-table prune logic
packages/worker/src/app/retention.ts, packages/worker/src/email/system-email.ts, tests
Reworks package-runtime pruning, bundle-artifact snapshot cleanup, email-message pruning, and system-email retention with blob-aware deletion.
Round-robin retention runner
packages/worker/src/app/retention.ts
Rewrites pruneRetention into a time-budgeted scheduler that tracks batches per table and budget exhaustion.
Retention tests and docs
packages/worker/src/app/retention.node.test.ts, docs/contributing/architecture/data-storage.md
Expands fixtures and assertions, and documents the new scheduler and policy behavior.

Usage metering and admin caching

Layer / File(s) Summary
KV cache adapter
packages/worker/src/kv-cachified.ts, packages/worker/package.json
Adds a versioned KV-backed cache implementation and dependency.
Rollup aggregation
packages/worker/src/usage/aggregate-rollups.ts, packages/worker/src/usage/aggregate-rollups.node.test.ts
Implements Analytics Engine SQL rollup recomputation and its tests.
Event sink selection
packages/worker/src/usage/record-usage.ts, packages/worker/src/usage/record-usage.workers.test.ts, packages/worker/src/mcp/executor.node.test.ts
Changes recordUsage to write to Analytics Engine when available and fall back to direct D1 rollups locally.
Admin usage caching
packages/worker/src/app/admin-usage-data.ts, packages/worker/src/app/admin-usage-data.node.test.ts
Wraps admin usage rollup reads with KV-backed caching keyed by user and month.
Docs
docs/contributing/architecture/usage-metering.md, docs/contributing/architecture/primitives.yaml
Updates usage-metering docs to describe the derived rollup model and caching behavior.

Pagination, search, and lazy hydration

Layer / File(s) Summary
Reindex result merging
packages/worker/src/mcp/capabilities/reindex-batches.ts
Adds mergeVectorReindexResults to combine per-page reindex outcomes.
Job/memory/package reindex pagination
packages/worker/src/jobs/job-reindex.ts, packages/worker/src/mcp/memory/memory-reindex.ts, packages/worker/src/mcp/memory/repo.ts, packages/worker/src/package-registry/package-reindex.ts, packages/worker/src/package-registry/repo.ts, tests
Replaces full-table reindexing with keyset-paged batches merged into one result.
Lazy static registry
packages/worker/src/mcp/capabilities/registry.ts, tests
Replaces eager registry construction with a memoized getStaticRegistry().
Online package search
packages/worker/src/mcp/tools/search.ts, tests
Adds bounded vector similarity querying, reciprocal-rank fusion, and lazy top-match hydration.
Bounded community search
packages/worker/src/community/repo.ts, packages/worker/src/community/service.ts, tests
Adds SQL-filtered, chunked, bounded candidate queries replacing unbounded listing loads.
Account export pagination
packages/worker/src/app/account-export.ts, tests, docs
Replaces full-table export loads with rowid keyset pagination.

Stable user id healing and backfill

Layer / File(s) Summary
Scan fallback healing
packages/worker/src/user-id.ts, tests, docs/contributing/architecture/entitlements.md, docs/contributing/architecture/data-storage.md
Adds write-back of computed stable ids when the scan fallback finds a row with NULL stable_user_id.
Maintenance backfill endpoint
packages/worker/src/maintenance-handler.ts, packages/worker/src/index.ts, tests
Adds paged backfill and a maintenance handler route for legacy stable ids.

Scheduled cron, job alarms, and reconcile batching

Layer / File(s) Summary
Cron lane isolation
packages/worker/src/index.ts, tests
Runs cron tasks as independent lanes via Promise.allSettled with per-lane error handling.
Job backlog rearming
packages/worker/src/jobs/manager-do.ts, tests, packages/worker/src/jobs/repo.ts, tests
Caps due-job batches and rearms the alarm near-immediately when backlog remains.
Reconcile batching
packages/worker/src/jobs/reconcile-artifacts-pushes.ts, tests, docs/contributing/architecture/data-storage.md
Adds a time-budgeted, multi-batch external reconcile loop and per-source handling.

Package runtime, repo checks, and manifest cache

Layer / File(s) Summary
Package service backoff
packages/worker/src/package-runtime/package-service.ts, tests
Persists failure counts and computes exponential backoff with jitter for auto-start retries.
Repo publish-check caps
packages/worker/src/repo/checks.ts, tests
Caps file count and total bytes materialized during publish checks.
Manifest cache cleanup
packages/worker/src/package-retrievers/manifest-cache.ts, tests, docs
Removes legacy combined scope index writes in favor of per-entry KV keys.
Lazy OG image import
packages/worker/src/app/handlers/community-detail.tsx
Defers renderCommunityOgImage loading to a dynamic import.

Estimated code review effort: 5 (Critical) | ~150 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the PR’s main themes: email blob storage, derived usage rollups, bounded cron/DO work, and search cost reductions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/scale-fixes-tier1-tier2-6b11

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.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review July 8, 2026 07:28
Comment thread packages/worker/src/usage/aggregate-rollups.ts
If R2 listing or creation fails, deploys drop the EMAIL_BLOBS binding
from the generated Wrangler config and the worker falls back to inline
raw MIME storage, instead of failing the resource-ensure run.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/community/service.ts Outdated
Comment thread packages/worker/src/email/system-email.ts Outdated

@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.

🧹 Nitpick comments (7)
packages/worker/src/repo/checks.ts (1)

908-919: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Byte-cap error message hardcodes "15 MiB".

The message at line 918 hardcodes (15 MiB) while the actual limit is derived from repoChecksSourceMaxTotalBytes. If the constant is ever changed, the message will be misleading. Consider computing the human-readable size from the constant, or remove the hardcoded unit.

♻️ Suggested refactor
-				message: `Repo checks aborted: source root "${sourceRoot || '/'}" exceeds the ${repoChecksSourceMaxTotalBytes}-byte (15 MiB) publish check limit. Remove or shrink large files that should not be published (for example build output, vendored dependencies, or data files) and run the checks again.`,
+				message: `Repo checks aborted: source root "${sourceRoot || '/'}" exceeds the ${repoChecksSourceMaxTotalBytes}-byte (${Math.round(repoChecksSourceMaxTotalBytes / (1024 * 1024))} MiB) publish check limit. Remove or shrink large files that should not be published (for example build output, vendored dependencies, or data files) and run the checks again.`,
🤖 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/repo/checks.ts` around lines 908 - 919, The repo checks
byte-cap error message in checks.ts hardcodes “15 MiB” even though the limit
comes from repoChecksSourceMaxTotalBytes. Update the message in the totalBytes
overflow branch to derive the human-readable size from that constant (or omit
the fixed unit) so the text stays correct if the limit changes; use the existing
check logic around repoChecksSourceMaxTotalBytes and the totalBytes comparison
as the target location.
packages/worker/src/package-retrievers/manifest-cache.node.test.ts (1)

266-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for refresh under a no-list() binding.

This test only reads a pre-seeded legacy index; it never calls refreshPackageRetrieverManifestCache in the no-list() mode. That refresh path is exactly where the legacy index gets deleted while per-entry keys remain unreadable (see manifest-cache.ts Lines 429-434), so the current suite would not catch that regression. Consider a case that refreshes and then asserts listPackageRetrieversForScope still resolves entries when list is unavailable.

🤖 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-retrievers/manifest-cache.node.test.ts` around
lines 266 - 339, The current no-list coverage in manifest-cache.node.test.ts
only verifies reading a preseeded legacy index and misses the refresh path where
the legacy index may be removed. Update the test around
listPackageRetrieversForScope and refreshPackageRetrieverManifestCache to
simulate a no-kv.list binding, run a refresh, and then assert the scope listing
still returns the expected retriever entry. Use the existing helpers like
createKv, legacyKey, and refreshPackageRetrieverManifestCache so the regression
where refresh deletes the legacy index but entries are still unreadable is
caught.
packages/worker/src/usage/aggregate-rollups.ts (2)

115-121: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an explicit timeout to the Analytics Engine fetch call.

The fetch to the Cloudflare SQL API has no timeout. A slow or hung response will consume the cron lane's entire wall-clock budget. Since lanes run via Promise.allSettled, other lanes survive, but the usage aggregation lane would silently fail for that hour. Adding AbortSignal.timeout bounds the damage and allows the next hourly run to catch up.

⏱️ Proposed fix
 	const response = await fetch(url, {
 		method: 'POST',
 		headers: {
 			authorization: `Bearer ${input.apiToken}`,
 		},
 		body: input.query,
+		signal: AbortSignal.timeout(15_000),
 	})
🤖 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/usage/aggregate-rollups.ts` around lines 115 - 121, The
Analytics Engine request in aggregate-rollups’s fetch call has no timeout, so a
hung Cloudflare SQL API response can block the usage aggregation lane for the
whole cron window. Update the fetch path in aggregate-rollups to use an explicit
AbortSignal.timeout (or equivalent abort handling) for the request, and make
sure the timeout value is applied alongside the existing authorization header
and body so the lane fails fast and the next hourly run can recover.

172-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract validRows to avoid duplicating the filter logic.

The (row) => row.user_id && row.metric filter is applied twice — once for statements and once for users. Extracting a validRows variable improves readability and ensures both paths stay in sync if the filter condition changes.

♻️ Proposed refactor
-	const statements = rows
-		.filter((row) => row.user_id && row.metric)
-		.map((row) =>
-			env.APP_DB.prepare(usageRollupAbsoluteUpsertStatement).bind(
-				row.user_id,
-				row.metric,
-				month,
-				toCount(row.event_count),
-				toCount(row.error_count),
-				toCount(row.total_duration_ms),
-				toCount(row.total_cpu_ms),
-				toCount(row.total_bytes),
-				updatedAt,
-			),
-		)
-	const users = new Set(
-		rows.filter((row) => row.user_id && row.metric).map((row) => row.user_id),
-	).size
+	const validRows = rows.filter((row) => row.user_id && row.metric)
+	const statements = validRows.map((row) =>
+		env.APP_DB.prepare(usageRollupAbsoluteUpsertStatement).bind(
+			row.user_id,
+			row.metric,
+			month,
+			toCount(row.event_count),
+			toCount(row.error_count),
+			toCount(row.total_duration_ms),
+			toCount(row.total_cpu_ms),
+			toCount(row.total_bytes),
+			updatedAt,
+		),
+	)
+	const users = new Set(validRows.map((row) => row.user_id)).size
🤖 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/usage/aggregate-rollups.ts` around lines 172 - 189, The
filter logic for valid rollup rows is duplicated in aggregate-rollups handling,
so extract a shared validRows variable before building statements and users. Use
that single filtered collection in the statement generation and in the unique
user count so the condition stays consistent and the code is easier to read; the
relevant logic is in the rows processing inside the rollup aggregation flow.
packages/worker/src/email/system-email.ts (1)

244-264: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider using db.batch() for atomic chunked deletes.

The three DELETE statements (attachments, delivery_events, messages) run as independent D1 operations. If the worker is interrupted between them, child records could be deleted while parent rows remain, leaving temporary inconsistency until the next retention run. Wrapping them in db.batch() executes them as a single transaction with no additional code complexity.

♻️ Proposed refactor
-		await input.db
-			.prepare(
-				`DELETE FROM email_attachments
-				WHERE message_id IN (${placeholders})`,
-			)
-			.bind(...messageIds)
-			.run()
-		await input.db
-			.prepare(
-				`DELETE FROM email_delivery_events
-				WHERE message_id IN (${placeholders})`,
-			)
-			.bind(...messageIds)
-			.run()
-		await input.db
-			.prepare(
-				`DELETE FROM email_messages
-				WHERE id IN (${placeholders})`,
-			)
-			.bind(...messageIds)
-			.run()
+		await input.db.batch([
+			input.db
+				.prepare(
+					`DELETE FROM email_attachments
+					WHERE message_id IN (${placeholders})`,
+				)
+				.bind(...messageIds),
+			input.db
+				.prepare(
+					`DELETE FROM email_delivery_events
+					WHERE message_id IN (${placeholders})`,
+				)
+				.bind(...messageIds),
+			input.db
+				.prepare(
+					`DELETE FROM email_messages
+					WHERE id IN (${placeholders})`,
+				)
+				.bind(...messageIds),
+		])
🤖 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/email/system-email.ts` around lines 244 - 264, The three
DELETE operations for email attachments, delivery events, and email messages are
currently executed as separate D1 calls, which can leave the data temporarily
inconsistent if the worker stops midway. Update the deletion flow in the email
retention logic that uses input.db.prepare(...).bind(...).run() to run those
statements through db.batch() instead, so the attachment, delivery_events, and
messages deletes commit atomically for each chunk. Keep the same messageIds
placeholders and chunking behavior, just group the existing statements into one
batched transaction.
packages/worker/src/mcp/tools/search.ts (2)

929-989: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider wrapping queryPackageVectorScores in a fallback to deterministic scoring on failure.

If embedTextForVectorize or index.query throws (e.g., AI binding rate limit, transient Vectorize outage), the entire searchUnified call fails. A try/catch that returns null on error would let the caller gracefully fall back to cosineSimilarity-based offline scoring, improving resilience without masking the issue (the error can still be logged).

🛡️ Proposed fallback wrapper
 async function queryPackageVectorScores(input: {
 	env: Env
 	query: string
 	rows: Array<PackageSearchRow>
 	limit: number
 }): Promise<Map<string, number> | null> {
 	const index = getCapabilityVectorIndex(input.env)
 	if (!index) return null
+	try {
 		const userIds = Array.from(
 			new Set(input.rows.map((row) => row.record.userId).filter(Boolean)),
 		)
 		const recordIdByVectorId = new Map(
 			input.rows.map(
 				(row) => [savedPackageVectorId(row.record.id), row.record.id] as const,
 			),
 		)
 		const queryVector = await embedTextForVectorize(input.env, input.query)
 		const topK = Math.min(Math.max(input.rows.length, input.limit * 5), 100)
 		const vectorMatches = await index.query(queryVector, {
 			topK,
 			returnMetadata: 'none',
 			filter: {
 				kind: { $eq: 'package' },
 				...(userIds.length === 1 ? { userId: { $eq: userIds[0] } } : {}),
 			},
 		})
 		const scores = new Map<string, number>()
 		for (const match of vectorMatches.matches) {
 			if (typeof match.id !== 'string') continue
 			const recordId = recordIdByVectorId.get(match.id)
 			if (!recordId || scores.has(recordId)) continue
 			scores.set(recordId, match.score)
 		}
 		return scores
+	} catch (error) {
+		console.warn(
+			JSON.stringify({
+				message: 'package vector score query failed, falling back to deterministic scoring',
+				error: error instanceof Error ? error.message : String(error),
+			}),
+		)
+		return null
+	}
 }
🤖 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/mcp/tools/search.ts` around lines 929 - 989, Wrap
`queryPackageVectorScores` with a try/catch so failures from
`embedTextForVectorize` or `index.query` return null and let
`buildPackageCandidates` fall back to deterministic scoring. Keep the fallback
behavior aligned with the existing offline path in `buildPackageCandidates`, and
log the caught error with enough context before returning null.

1436-1443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

rerankingMs now includes hydration I/O time.

hydrateTopPackageMatches (which loads package sources via loadPackageSourceBySourceId) is called between rerankingStart and the rerankingMs calculation. This metric no longer reflects pure reranking time, which could mislead performance analysis. Consider tracking hydration separately.

⏱️ Proposed separate hydration timing
 	const rerankingStart = performance.now()
 	const reranked = rerankCandidates({
 		candidates,
 		intent,
 		limit,
 	})
 	const matches = reranked.map((candidate) => candidate.match)
+	const rerankingMs = Math.max(0, Math.round(performance.now() - rerankingStart))
+	const hydrationStart = performance.now()
 	await hydrateTopPackageMatches({
 		query: intent.normalizedQuery,
 		matches,
 		rows: input.optionalRows.packageRows,
 	})
-	const rerankingMs = Math.max(
-		0,
-		Math.round(performance.now() - rerankingStart),
-	)
+	const hydrationMs = Math.max(0, Math.round(performance.now() - hydrationStart))

Then include hydrationMs in phaseTimings (or fold it into candidateGenerationMs if you prefer not to change the response shape).

Also applies to: 1473-1477

🤖 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/mcp/tools/search.ts` around lines 1436 - 1443, Separate
the hydration I/O from reranking timing in search.ts: `rerankingMs` is currently
measured across `hydrateTopPackageMatches`/`loadPackageSourceBySourceId`, so
update the timing in the `buildPackageCandidates` flow to measure pure reranking
only and record hydration separately. Add a `hydrationMs` entry to
`phaseTimings` (or move that cost into `candidateGenerationMs` if you want to
avoid changing the response shape) and make sure the same adjustment is applied
wherever `rerankingMs` is computed.
🤖 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.

Nitpick comments:
In `@packages/worker/src/email/system-email.ts`:
- Around line 244-264: The three DELETE operations for email attachments,
delivery events, and email messages are currently executed as separate D1 calls,
which can leave the data temporarily inconsistent if the worker stops midway.
Update the deletion flow in the email retention logic that uses
input.db.prepare(...).bind(...).run() to run those statements through db.batch()
instead, so the attachment, delivery_events, and messages deletes commit
atomically for each chunk. Keep the same messageIds placeholders and chunking
behavior, just group the existing statements into one batched transaction.

In `@packages/worker/src/mcp/tools/search.ts`:
- Around line 929-989: Wrap `queryPackageVectorScores` with a try/catch so
failures from `embedTextForVectorize` or `index.query` return null and let
`buildPackageCandidates` fall back to deterministic scoring. Keep the fallback
behavior aligned with the existing offline path in `buildPackageCandidates`, and
log the caught error with enough context before returning null.
- Around line 1436-1443: Separate the hydration I/O from reranking timing in
search.ts: `rerankingMs` is currently measured across
`hydrateTopPackageMatches`/`loadPackageSourceBySourceId`, so update the timing
in the `buildPackageCandidates` flow to measure pure reranking only and record
hydration separately. Add a `hydrationMs` entry to `phaseTimings` (or move that
cost into `candidateGenerationMs` if you want to avoid changing the response
shape) and make sure the same adjustment is applied wherever `rerankingMs` is
computed.

In `@packages/worker/src/package-retrievers/manifest-cache.node.test.ts`:
- Around line 266-339: The current no-list coverage in
manifest-cache.node.test.ts only verifies reading a preseeded legacy index and
misses the refresh path where the legacy index may be removed. Update the test
around listPackageRetrieversForScope and refreshPackageRetrieverManifestCache to
simulate a no-kv.list binding, run a refresh, and then assert the scope listing
still returns the expected retriever entry. Use the existing helpers like
createKv, legacyKey, and refreshPackageRetrieverManifestCache so the regression
where refresh deletes the legacy index but entries are still unreadable is
caught.

In `@packages/worker/src/repo/checks.ts`:
- Around line 908-919: The repo checks byte-cap error message in checks.ts
hardcodes “15 MiB” even though the limit comes from
repoChecksSourceMaxTotalBytes. Update the message in the totalBytes overflow
branch to derive the human-readable size from that constant (or omit the fixed
unit) so the text stays correct if the limit changes; use the existing check
logic around repoChecksSourceMaxTotalBytes and the totalBytes comparison as the
target location.

In `@packages/worker/src/usage/aggregate-rollups.ts`:
- Around line 115-121: The Analytics Engine request in aggregate-rollups’s fetch
call has no timeout, so a hung Cloudflare SQL API response can block the usage
aggregation lane for the whole cron window. Update the fetch path in
aggregate-rollups to use an explicit AbortSignal.timeout (or equivalent abort
handling) for the request, and make sure the timeout value is applied alongside
the existing authorization header and body so the lane fails fast and the next
hourly run can recover.
- Around line 172-189: The filter logic for valid rollup rows is duplicated in
aggregate-rollups handling, so extract a shared validRows variable before
building statements and users. Use that single filtered collection in the
statement generation and in the unique user count so the condition stays
consistent and the code is easier to read; the relevant logic is in the rows
processing inside the rollup aggregation flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc7cafe8-c614-437e-b339-2d133505f6db

📥 Commits

Reviewing files that changed from the base of the PR and between 8d7b77b and a72d101.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (80)
  • docs/contributing/architecture/data-storage.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/primitives.yaml
  • docs/contributing/architecture/usage-metering.md
  • packages/worker/migrations/0054-email-blob-storage.sql
  • packages/worker/migrations/0055-retention-indexes.sql
  • packages/worker/package.json
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/app/account-export.node.test.ts
  • packages/worker/src/app/account-export.ts
  • packages/worker/src/app/admin-system-email-data.ts
  • packages/worker/src/app/admin-usage-data.node.test.ts
  • packages/worker/src/app/admin-usage-data.ts
  • packages/worker/src/app/handlers/community-detail.tsx
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/app/retention.ts
  • packages/worker/src/community/community-scoring.node.test.ts
  • packages/worker/src/community/community-service.node.test.ts
  • packages/worker/src/community/repo.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/email/system-email.ts
  • packages/worker/src/email/system-email.workers.test.ts
  • packages/worker/src/email/test-schema.ts
  • packages/worker/src/email/types.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/index.ts
  • packages/worker/src/index.workers.test.ts
  • packages/worker/src/jobs/job-reindex.node.test.ts
  • packages/worker/src/jobs/job-reindex.ts
  • packages/worker/src/jobs/manager-do.node.test.ts
  • packages/worker/src/jobs/manager-do.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.node.test.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.ts
  • packages/worker/src/jobs/repo.ts
  • packages/worker/src/jobs/repo.workers.test.ts
  • packages/worker/src/kv-cachified.ts
  • packages/worker/src/maintenance-handler.node.test.ts
  • packages/worker/src/maintenance-handler.ts
  • packages/worker/src/mcp/capabilities/admin/admin-system-email-get.ts
  • packages/worker/src/mcp/capabilities/email/email-attachment-get.ts
  • packages/worker/src/mcp/capabilities/registry.node.test.ts
  • packages/worker/src/mcp/capabilities/registry.ts
  • packages/worker/src/mcp/capabilities/reindex-batches.ts
  • packages/worker/src/mcp/executor.node.test.ts
  • packages/worker/src/mcp/memory/memory-reindex.node.test.ts
  • packages/worker/src/mcp/memory/memory-reindex.ts
  • packages/worker/src/mcp/memory/memory-search.ts
  • packages/worker/src/mcp/memory/repo.ts
  • packages/worker/src/mcp/memory/service.node.test.ts
  • packages/worker/src/mcp/memory/service.ts
  • packages/worker/src/mcp/tools/search.node.test.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/package-invocations/service.ts
  • packages/worker/src/package-registry/package-reindex.node.test.ts
  • packages/worker/src/package-registry/package-reindex.ts
  • packages/worker/src/package-registry/repo.ts
  • packages/worker/src/package-retrievers/manifest-cache.node.test.ts
  • packages/worker/src/package-retrievers/manifest-cache.ts
  • packages/worker/src/package-runtime/package-service.node.test.ts
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/repo/checks.node.test.ts
  • packages/worker/src/repo/checks.ts
  • packages/worker/src/usage/aggregate-rollups.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.ts
  • packages/worker/src/usage/record-usage.ts
  • packages/worker/src/usage/record-usage.workers.test.ts
  • packages/worker/src/user-id.ts
  • packages/worker/src/user-id.workers.test.ts
  • packages/worker/worker-configuration.d.ts
  • packages/worker/wrangler.jsonc
  • tools/ci/preview-resources.ts
  • tools/ci/production-resources.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts

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

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

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

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

Mocks:

Comment thread packages/worker/src/usage/aggregate-rollups.ts
Comment thread packages/worker/src/community/service.ts
Comment thread packages/worker/src/jobs/reconcile-artifacts-pushes.ts
Comment thread packages/worker/src/mcp/tools/search.ts

@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.

🧹 Nitpick comments (1)
tools/ci/resource-utils.ts (1)

185-201: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Any wrangler failure (not just missing permissions) silently drops blob offload.

Both the create failure (Line 188) and the post-create re-list failure (Line 195) return available: false, which causes writeGeneratedWranglerConfig to drop the EMAIL_BLOBS binding and quietly fall back to inline MIME in D1. A transient network/API hiccup during a production deploy would therefore silently change the raw-email storage path without any hard failure — easy to miss and potentially costly (D1 row-size/cost implications).

Consider scoping the fail-soft to genuine authz failures (e.g. inspect createResult.stderr for permission/authentication markers) and treating other failures as fatal, so unexpected errors surface instead of degrading silently.

Separately, the re-list guard on Line 195 flips available: false even after a successful create, which can leave an orphaned bucket while the binding is dropped (it self-heals on the next run when the list succeeds). Worth a brief comment noting that intent.

🤖 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 `@tools/ci/resource-utils.ts` around lines 185 - 201, The R2 bucket
provisioning path in createR2BucketName is too broadly fail-soft, causing
non-auth wrangler errors to silently disable blob offload. Update the
createResult and post-create list checks to distinguish genuine
permission/authentication failures from transient or unexpected errors by
inspecting createResult.stderr (or equivalent error details), and only return
available: false for the authz case; let other failures surface as fatal. Also
add a short comment near the namesAfterCreate availability check to explain that
it intentionally marks the bucket unavailable if the re-list cannot verify
creation.
🤖 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.

Nitpick comments:
In `@tools/ci/resource-utils.ts`:
- Around line 185-201: The R2 bucket provisioning path in createR2BucketName is
too broadly fail-soft, causing non-auth wrangler errors to silently disable blob
offload. Update the createResult and post-create list checks to distinguish
genuine permission/authentication failures from transient or unexpected errors
by inspecting createResult.stderr (or equivalent error details), and only return
available: false for the authz case; let other failures surface as fatal. Also
add a short comment near the namesAfterCreate availability check to explain that
it intentionally marks the bucket unavailable if the re-list cannot verify
creation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f15c3b9-b3b3-4bd7-9826-4a0a4eaeeaba

📥 Commits

Reviewing files that changed from the base of the PR and between a72d101 and 243d615.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • docs/contributing/architecture/primitives.yaml
  • packages/worker/package.json
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/app/account-export.node.test.ts
  • packages/worker/src/app/account-export.ts
  • packages/worker/src/app/admin-system-email-data.ts
  • packages/worker/src/app/admin-usage-data.ts
  • packages/worker/src/app/handlers/community-detail.tsx
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/email/system-email.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/index.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.ts
  • packages/worker/src/maintenance-handler.ts
  • packages/worker/src/mcp/capabilities/reindex-batches.ts
  • packages/worker/src/mcp/memory/service.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/package-invocations/service.ts
  • packages/worker/src/package-registry/package-reindex.ts
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/repo/checks.ts
  • packages/worker/src/user-id.ts
  • packages/worker/wrangler.jsonc
  • tools/ci/preview-resources.ts
  • tools/ci/production-resources.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts
💤 Files with no reviewable changes (3)
  • packages/worker/package.json
  • packages/worker/wrangler.jsonc
  • packages/worker/src/index.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/entitlements/service.ts
🚧 Files skipped from review as they are similar to previous changes (26)
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/package-invocations/service.ts
  • packages/worker/src/package-registry/package-reindex.ts
  • packages/worker/src/repo/checks.ts
  • tools/ci/production-resources.ts
  • packages/worker/src/app/handlers/community-detail.tsx
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/jobs/reconcile-artifacts-pushes.ts
  • packages/worker/src/user-id.ts
  • packages/worker/src/app/admin-system-email-data.ts
  • tools/ci/preview-resources.ts
  • packages/worker/src/app/account-export.node.test.ts
  • docs/contributing/architecture/primitives.yaml
  • packages/worker/src/mcp/memory/service.ts
  • packages/worker/src/email/system-email.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/maintenance-handler.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/app/account-export.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/app/admin-usage-data.ts
  • tools/ci/resource-utils.node.test.ts
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/mcp/capabilities/reindex-batches.ts

…allbacks

Bound the usage aggregation query to the current UTC month on both
sides and time out the AE fetch; delete system-email R2 blobs before
their rows so failures can retry; run the community semantic fallback
when the LIKE prefilter matched nothing; degrade package search to
lexical scoring when the vector query fails; derive the repo-checks
byte-cap message from its constant.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/app/retention.ts Outdated
Comment thread packages/worker/src/app/retention.ts
…dvancing prunes

The hourly rollup aggregation now deletes stale current-month rows
absent from the AE result; email and system-email retention advance a
keyset cursor past blob-skipped rows so a blocked head cannot wedge the
queue; retention hasMore is based on selected rather than deleted rows;
artifact reconcile pages strictly forward within a tick even when
cursor writes fail; trade-off comments document the lean package search
projection and the community browse candidate bound.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1df1f6c. Configure here.

Comment thread packages/worker/src/usage/aggregate-rollups.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot merged commit 924c624 into main Jul 8, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/scale-fixes-tier1-tier2-6b11 branch July 8, 2026 09:25
cursor Bot pushed a commit that referenced this pull request Jul 8, 2026
* Fix D1 SQLITE_BUSY lock contention from parallel cron lanes

Scale hardening (#669) runs multiple D1 write-heavy scheduled lanes in
parallel (retention, reconcile, usage aggregation, system-email retention).
Those crons can contend for the same SQLite lock and surface as
D1_ERROR: NOSENTRY database is locked: SQLITE_BUSY in Sentry.

- Run scheduled cron lanes sequentially so they no longer hammer D1 at once
- Add runD1WithRetry for transient SQLITE_BUSY with exponential backoff
- Wrap retention D1 reads/writes with the retry helper

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

* Format d1-retry.ts

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.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