Skip to content

Make inbound email delivery durable and idempotent - #891

Merged
kody-bot merged 40 commits into
mainfrom
cursor/review-fix-email-durability-70f3
Jul 23, 2026
Merged

kody-bot merged 40 commits into
mainfrom
cursor/review-fix-email-durability-70f3

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • adds a durable, idempotent inbound email boundary with one quota charge, fenced D1/message/attachment/finalization writes, stable user-scoped MIME storage, and scheduled repair/cleanup
  • composes merged account-lifecycle write leases so non-system inbound persistence/effects/reconciliation cannot race account deletion; system:email remains exempt
  • prevents deleted/deleting accounts from having usage rollups recreated from retained Analytics or delivery events
  • resolves existing charged deliveries before storage-byte/stored-count/current-day quota gates so partial retries repair without recharging
  • fingerprints provider + normalized envelope sender + recipient + MIME, preserving retry dedupe while separating distinct SMTP senders
  • releases transient pre-execution artifact claims for retry, keeps business failures terminal, and dead-letters subscription effects after three failed attempts
  • adds migration 0089-email-delivery-reconciliation-index.sql with index/query-plan coverage for global scheduled reconciliation
  • persists retention-safe usage month/bytes/duration on delivery events and merges current/previous Analytics months without destructive ingestion-gap updates

Validation

  • focused email/account/invocation/usage/migration suites: 58 tests passed before full gate
  • npm run validate: passed on latest main
    • unit/worker: 1,270 tests
    • Playwright: 17 tests
    • Validate and Deploy Preview Resources CI checks passed
  • final blocker-only review: no remaining blockers

Remaining risks

  • byte-identical MIME from the same provider, envelope sender, and recipient is intentionally deduped for 48 hours because Cloudflare Email Routing exposes no delivery-attempt identifier
  • stale package invocation reclaim provides convergent at-least-once execution with fenced terminal state; package handlers must make external-world effects idempotent for ambiguous crash recovery
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ d38cac51 · Head: 6c464fc0

Classification: extends — changes inbound email durability/idempotency, retention-safe usage, subscription invocation recovery, and scheduled reconciliation while composing merged account write leases.

Primitives touched

Primitive Group Impact
email assistant extends — bounded ledger, fenced storage/cleanup/effects, rollout adoption
email-blobs-r2 storage extends — generation-safe cleanup and user-key validation
usage-metering storage extends — live-user current/previous Analytics plus durable event usage
scheduled-cron surfaces extends — indexed owner reconciliation with dead-letter exclusion
account write lease storage composes — deletion-safe user write boundary
package invocation idempotency runtime extends — artifact retry release, stale CAS reclaim, claim-token finalization

System map

Inbound delivery and its effects run under the merged account write lease; scheduled reconciliation uses indexed delivery-event filters and preserves usage after message retention without resurrecting deleted accounts.

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
	email["email<br/>Email"]:::extended
	lease["account-write-lease<br/>Account write lease"]:::touched
	d1["d1-app-db<br/>D1 app database"]:::untouched
	blobs["email-blobs-r2<br/>Email blob storage (R2)"]:::extended
	usage["usage-metering<br/>Usage metering"]:::extended
	cron["scheduled-cron<br/>Scheduled handler"]:::extended
	invoke["package-invocations<br/>Package invocation idempotency"]:::extended
	email -->|"leased fenced delivery/effect writes"| lease
	lease -->|"ledger, message, attachment, quota"| d1
	email -->|"provider + envelope keyed MIME generation"| blobs
	cron -->|"indexed oldest-due owner reconciliation"| email
	email -->|"bounded retry/dead-letter subscriptions"| invoke
	email -->|"retention-safe month/bytes/duration"| usage
	usage -->|"live-user current + previous month merge"| d1
	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

  • Every non-system user persistence/effect/reconciliation path holds the merged account write lease.
  • Existing charged deliveries bypass current storage growth gates and never recharge.
  • Usage upserts and retained-event aggregation require a live, non-deleting user; orphan rollups are removed.
  • Cleanup generations are write-closed; stale cleaners cannot delete successor MIME.
  • Subscription retries are bounded and observable; dead-lettered deliveries leave the reconciliation queue.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added a durable inbound email delivery pipeline with deterministic deduplication and idempotent retry handling.
    • Added usage-effect recording and inbound subscription event dispatch with retry and dead-letter behavior.
    • Added a scheduled reconciliation job for stale inbound deliveries and pending effects.
  • Bug Fixes
    • Prevented duplicate messages/threads/attachments and ensured quota isn’t double-charged during ambiguous failures.
    • Improved recovery and legacy delivery adoption, including orphan storage cleanup.
    • Hardened inbound processing around raw MIME size limits and more resilient subscription discovery/dispatch retries.
  • Chores
    • Added a database index to speed reconciliation and extended usage rollups to cover durable inbound email usage.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2d122e8-c719-442b-b14b-17f195ab342b

📥 Commits

Reviewing files that changed from the base of the PR and between b4c8bfc and 4b27d8e.

📒 Files selected for processing (11)
  • packages/worker/src/email/delivery-queue.node.test.ts
  • packages/worker/src/email/inbound-delivery.ts
  • packages/worker/src/email/inbound-effects.node.test.ts
  • packages/worker/src/email/inbound-effects.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/package-subscriptions.node.test.ts
  • packages/worker/src/email/package-subscriptions.ts
  • packages/worker/src/email/parser.node.test.ts
  • packages/worker/src/email/parser.ts
  • packages/worker/src/email/reconcile-inbound-deliveries.ts
  • packages/worker/src/usage/aggregate-rollups.workers.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/worker/src/usage/aggregate-rollups.workers.test.ts
  • packages/worker/src/email/parser.ts
  • packages/worker/src/email/reconcile-inbound-deliveries.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/inbound-delivery.ts

📝 Walkthrough

Walkthrough

Changes

The PR adds a durable inbound email pipeline with deterministic delivery IDs, D1/R2 deduplication, quota charging, storage fencing, idempotent retries, effect dispatch, stale-work reconciliation, scheduled cleanup, package invocation recovery, and inbound usage rollups.

Inbound delivery lifecycle

Layer / File(s) Summary
Delivery identity, state, and quotas
packages/worker/src/email/inbound-delivery.ts
Adds deterministic delivery identity, deduplication, legacy adoption, quota charging, leases, rejection, finalization, and recovery.
Fenced idempotent inbound storage
packages/worker/src/email/parser.ts, packages/worker/src/email/repo.ts, packages/worker/src/email/service.ts, packages/worker/src/email/inbound.ts
Separates raw MIME reading from parsing and stores inbound data through delivery leases and repository fences.
Effects and reconciliation
packages/worker/src/email/inbound-effects.ts, packages/worker/src/email/reconcile-inbound-deliveries.ts, packages/worker/src/index.ts
Adds usage/subscription effect processing, stale delivery recovery, orphan cleanup, dedupe pruning, and a scheduled reconciliation lane.
Subscription dispatch reliability
packages/worker/src/email/package-subscriptions.ts, packages/worker/src/package-invocations/admin-package-subscriptions.ts
Preserves discovery failures and classifies pre-execution infrastructure failures for retry.
Inbound validation
packages/worker/src/email/*workers.test.ts, packages/worker/src/email/*node.test.ts
Covers durable retries, deduplication, quota behavior, ambiguous commits, system inbox handling, parser limits, and reconciliation indexing.

Package invocation recovery

Layer / File(s) Summary
Invocation claims and result fencing
packages/worker/src/package-invocations/idempotent-module-invocation.ts, packages/worker/src/package-invocations/repo.ts, packages/worker/src/package-invocations/module-artifacts.ts
Adds polling, stale claim recovery, claim-timestamp fencing, claim release, and transient artifact failure detection.
Invocation recovery validation
packages/worker/src/package-invocations/service.node.test.ts
Tests stale recovery, fresh polling, conditional updates, and transient failure recovery.

Inbound usage rollups

Layer / File(s) Summary
D1 inbound usage aggregation
packages/worker/src/usage/aggregate-rollups.ts
Merges durable inbound email usage with current and previous month Analytics Engine data and removes rollups for non-live users.
Usage aggregation validation
packages/worker/src/usage/aggregate-rollups.node.test.ts, packages/worker/src/usage/aggregate-rollups.workers.test.ts
Verifies month-qualified queries, merged usage, live-user filtering, cleanup, and aggregation after message deletion.

Migration index

Layer / File(s) Summary
Reconciliation index and migration ledger
packages/worker/migrations/0089-email-delivery-reconciliation-index.sql, tools/migration-ledger.json, tools/check-migrations.node.test.ts, packages/worker/src/email/inbound-reconciliation-index-migration.node.test.ts
Adds the delivery reconciliation index, records it in the migration ledger, updates migration numbering fixtures, and validates query-plan usage.

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

Sequence Diagram(s)

sequenceDiagram
  participant EmailHandler
  participant InboundDelivery
  participant StorageService
  participant R2
  participant D1
  participant Effects
  EmailHandler->>InboundDelivery: build, deduplicate, and charge delivery
  EmailHandler->>InboundDelivery: claim storage lease
  EmailHandler->>StorageService: store idempotent inbound email
  StorageService->>R2: persist raw MIME
  StorageService->>D1: persist fenced message and attachments
  StorageService->>InboundDelivery: finalize received state
  EmailHandler->>Effects: schedule delivery effects
  Effects->>D1: record usage and effect state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main change: making inbound email delivery durable and idempotent.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/review-fix-email-durability-70f3

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.

@cursor
cursor Bot changed the base branch from main to cursor/review-fix-account-lifecycle-aeba July 23, 2026 00:07
@cursor
cursor Bot force-pushed the cursor/review-fix-email-durability-70f3 branch from 6469c7a to f2c2407 Compare July 23, 2026 00:14
@kody-bot
kody-bot marked this pull request as ready for review July 23, 2026 00:23
@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

1 similar comment
@kentcdodds

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

🧹 Nitpick comments (2)
packages/worker/src/email/reconcile-inbound-deliveries.ts (1)

44-119: 🚀 Performance & Scalability | 🔵 Trivial

Consider an index to keep the 5-minute discovery scan cheap.

The due_users CTE filters email_delivery_events primarily by provider/event_type and then evaluates several json_extract(detail_json, ...) predicates. As delivery-event volume grows, this scheduled scan can become a hot, mostly-full scan every cron tick. A composite index aligned with the three union branches (e.g. (provider, event_type, created_at)) would let the planner seed each branch by its selective columns before the JSON predicates run.

Since reconcileAfter/state/dedupeExpiresAt/effect fields live inside detail_json, they can't be indexed directly without generated/expression columns; the covering win here is the provider/event_type/created_at prefix plus keeping LIMIT-bounded ordering on created_at.

🤖 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/reconcile-inbound-deliveries.ts` around lines 44 -
119, The reconciliation query’s three due-user branches lack an index aligned
with their provider, event type, and timestamp filters. Add or reuse a composite
index on email_delivery_events covering provider, event_type, and created_at,
and verify the due_users query can use it while retaining the existing JSON
predicates and ordering.
packages/worker/src/email/package-subscriptions.ts (1)

223-230: 🩺 Stability & Availability | 🔵 Trivial

Consider a bounded-retry / dead-letter path for permanent discovery failures.

Throwing whenever discoveryErrors.length > 0 couples one user's broken package manifest to the whole delivery's subscription effect. Since processInboundDeliveryEffectsWithLeaseHeld defers with subscriptionEffectRetryAt (+15m) and reconciliation re-runs, a permanently unresolvable manifest will retry indefinitely with no attempt cap or dead-letter. Transient failures benefit; permanent ones become perpetual reconciliation churn per delivery.

Consider distinguishing permanent vs transient discovery failures, or capping retries after N attempts and marking the effect terminally failed.

🤖 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/package-subscriptions.ts` around lines 223 - 230,
Update processInboundDeliveryEffectsWithLeaseHeld and the subscription-effect
reconciliation flow so permanent discovery failures do not retry indefinitely:
distinguish permanent from transient errors or enforce a bounded attempt count,
then mark exhausted/permanent effects terminally failed or dead-lettered while
preserving subscriptionEffectRetryAt retries for transient failures.
🤖 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/package-invocations/idempotent-module-invocation.ts`:
- Around line 110-174: Move the in_progress polling logic surrounding
lookupInvocation and the pollDeadline outside the
withAccountWriteLease/invokeSavedPackageModule write-lease scope, so duplicate
requests can poll while the original write is active. Keep stale-claim handling
and resolveExistingInvocation behavior unchanged, and ensure the lease only
covers the necessary write operations rather than the polling wait.

---

Nitpick comments:
In `@packages/worker/src/email/package-subscriptions.ts`:
- Around line 223-230: Update processInboundDeliveryEffectsWithLeaseHeld and the
subscription-effect reconciliation flow so permanent discovery failures do not
retry indefinitely: distinguish permanent from transient errors or enforce a
bounded attempt count, then mark exhausted/permanent effects terminally failed
or dead-lettered while preserving subscriptionEffectRetryAt retries for
transient failures.

In `@packages/worker/src/email/reconcile-inbound-deliveries.ts`:
- Around line 44-119: The reconciliation query’s three due-user branches lack an
index aligned with their provider, event type, and timestamp filters. Add or
reuse a composite index on email_delivery_events covering provider, event_type,
and created_at, and verify the due_users query can use it while retaining the
existing JSON predicates and ordering.
🪄 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: f934f109-e93f-4c2e-8e7c-093f1f2b1a66

📥 Commits

Reviewing files that changed from the base of the PR and between e24fa85 and f2c2407.

📒 Files selected for processing (19)
  • packages/worker/src/email/inbound-delivery.ts
  • packages/worker/src/email/inbound-effects.ts
  • packages/worker/src/email/inbound-entitlements.workers.test.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/package-subscriptions.ts
  • packages/worker/src/email/parser.ts
  • packages/worker/src/email/reconcile-inbound-deliveries.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/email/system-email.workers.test.ts
  • packages/worker/src/index.ts
  • packages/worker/src/index.workers.test.ts
  • packages/worker/src/package-invocations/admin-package-subscriptions.ts
  • packages/worker/src/package-invocations/idempotent-module-invocation.ts
  • packages/worker/src/package-invocations/repo.ts
  • packages/worker/src/package-invocations/service.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.ts

Comment on lines +110 to 174
const pollDeadline = Date.now() + packageInvocationPollBudgetMs
while (
existing.status === 'in_progress' &&
!isStaleInvocation(existing.updated_at, new Date()) &&
Date.now() < pollDeadline
) {
await new Promise((resolve) =>
setTimeout(resolve, packageInvocationPollIntervalMs),
)
existing = await lookupInvocation()
if (!existing) break
}
if (!existing) {
return buildJsonErrorResponse({
status: 500,
code: 'idempotency_conflict_unresolved',
message: 'Package invocation disappeared while polling.',
idempotencyKey: input.idempotencyKey,
})
}
if (existing.status !== 'in_progress') {
return resolveExistingInvocation({
record: existing,
requestHash,
source: input.source,
topic: input.topic,
status: 'in_progress',
},
})
} catch (error) {
console.error(
'package invocation idempotency persistence failed',
error,
)
return buildJsonErrorResponse({
status: 500,
code: 'idempotency_persistence_failed',
message:
'Unable to persist the package invocation idempotency record. Please retry.',
idempotencyKey: input.idempotencyKey,
idempotencyKey: input.idempotencyKey,
})
}
const now = new Date()
if (!isStaleInvocation(existing.updated_at, now)) {
return resolveExistingInvocation({
record: existing,
requestHash,
idempotencyKey: input.idempotencyKey,
})
}
const reclaimedAt = now.toISOString()
const reclaimed = await tryClaimStalePackageInvocation({
db: input.env.APP_DB,
id: existing.id,
userId: input.actor.userId,
expectedUpdatedAt: existing.updated_at,
staleBefore: new Date(
now.getTime() - packageInvocationStaleAfterMs,
).toISOString(),
now: reclaimedAt,
})
if (!reclaimed) {
const current = await lookupInvocation()
if (!current) {
return buildJsonErrorResponse({
status: 500,
code: 'idempotency_conflict_unresolved',
message: 'Stale package invocation reclaim conflicted.',
idempotencyKey: input.idempotencyKey,
})
}
return resolveExistingInvocation({
record: current,
requestHash,
idempotencyKey: input.idempotencyKey,
})
}
invocationId = existing.id
claimUpdatedAt = reclaimedAt
}

@coderabbitai coderabbitai Bot Jul 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect withAccountWriteLease semantics: is it a per-user mutex or advisory?
fd -e ts account-deletion-state
ast-grep outline "$(fd -e ts account-deletion-state | head -n1)" --items all
rg -nP -C4 'function withAccountWriteLease' "$(fd -e ts account-deletion-state | head -n1)"

Repository: kentcdodds/kody

Length of output: 1089


🏁 Script executed:

#!/bin/bash
set -u

printf '--- account-deletion-state outline and implementation ---\n'
ast-grep outline packages/worker/src/app/account-deletion-state.ts --view expanded || true
cat -n packages/worker/src/app/account-deletion-state.ts | sed -n '1,180p'

printf '\n--- idempotent-module-invocation relevant context ---\n'
ast-grep outline packages/worker/src/package-invocations/idempotent-module-invocation.ts --view expanded || true
cat -n packages/worker/src/package-invocations/idempotent-module-invocation.ts | sed -n '1,230p'

Repository: kentcdodds/kody

Length of output: 14043


🌐 Web query:

D1 database transaction read committed concurrent writes read locks release after commit SQLite docs

💡 Result:

Cloudflare D1 is a serverless SQL database built on SQLite [1]. Because D1 operates as a managed service on the Cloudflare Workers platform, it abstracts away the low-level locking and concurrency mechanisms typically found in a standard SQLite environment [2][1]. 1. Isolation and Consistency: While standard SQLite in WAL (Write-Ahead Logging) mode provides snapshot isolation [2][3], Cloudflare D1's consistency model is distinct due to its global nature and read replication [4][2]. In the presence of read replicas, D1 provides sequential consistency for applications using the Sessions API [4][5]. Without the Sessions API—or in configurations where requests are routed arbitrarily across replicas—D1 offers read-committed consistency, which guarantees only that queries read committed data [5][2]. 2. Locks and Concurrent Writes: In standard SQLite, write transactions use locks to serialize access; a writer must acquire an exclusive lock, which forces readers to wait or causes other writers to fail/wait depending on the configuration [6][3]. However, D1 manages these complexities transparently [5]. Application developers do not interact with raw SQLite read/write locks, nor do they manage manual lock releases [5][7]. 3. Transactional Behavior: D1 handles write concurrency and transactional integrity at the service level [5]. If your application requires atomic operations or data integrity guarantees during concurrent writes (such as the classic "read-modify-write" race condition), you should utilize D1's Batch API or the Sessions API to ensure that related operations are processed in a consistent, sequential order [4][5][2]. You cannot explicitly manage read locks to prevent concurrent modifications (e.g., SELECT... FOR UPDATE) as you might in other relational databases, because D1 abstracts the underlying locking mechanism [5]. In summary, D1 does not expose the SQLite locking interface to users. You should rely on its high-level abstractions—specifically Sessions and Batches—to ensure data consistency rather than attempting to manage locks manually [4][5][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- idempotency and repo implementations ---'
for f in $(git ls-files | grep -E 'packages/worker/src/package-invocations/(idempotency|repo)\.ts$'); do
  echo "FILE: $f"
  wc -l "$f"
  ast-grep outline "$f" --view expanded || true
  sed -n '1,260p' "$f"
done

printf '%s\n' '--- usages of invokeSavedPackageModule and withAccountWriteLease ---'
rg -n "invokeSavedPackageModule|withAccountWriteLease" packages/worker/src packages/worker -g '*.ts' -g '*.tsx'

Repository: kentcdodds/kody

Length of output: 17550


Move the stale polling window outside the per-user write lease.

withAccountWriteLease increments users.active_write_count for the entire invokeSavedPackageModule body, so a concurrent duplicate request enters withAccountWriteLease first and blocks until the first write path returns. The in_progress poll is only reachable after that other request has completed and released the lease; under contention it effectively serializes duplicate invocations behind this + up to 1s polling timeout. Poll after the lease completes, or return with a retryable error from this path while another same-user write is active.

🤖 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-invocations/idempotent-module-invocation.ts`
around lines 110 - 174, Move the in_progress polling logic surrounding
lookupInvocation and the pollDeadline outside the
withAccountWriteLease/invokeSavedPackageModule write-lease scope, so duplicate
requests can poll while the original write is active. Keep stale-claim handling
and resolveExistingInvocation behavior unchanged, and ensure the lease only
covers the necessary write operations rather than the polling wait.

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.

Triaged against #890's implementation: withAccountWriteLease is not a per-user mutex. It atomically increments active_write_count so deletion waits, but concurrent writers continue normally. Polling inside the lease therefore does not block the original invocation; moving it outside would instead open an account-deletion race during the invocation lifecycle. No change needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread packages/worker/src/email/inbound.ts Outdated
Comment thread packages/worker/src/email/inbound-delivery.ts
Comment thread packages/worker/src/email/inbound-delivery.ts Outdated
Comment thread packages/worker/src/email/inbound-effects.ts
Comment thread packages/worker/src/email/service.ts
Comment thread packages/worker/src/email/inbound.ts
Comment thread packages/worker/src/email/inbound.ts
Comment thread packages/worker/src/email/inbound-effects.ts
Comment thread packages/worker/src/email/inbound.ts Outdated
@cursor
cursor Bot force-pushed the cursor/review-fix-email-durability-70f3 branch from 48f663e to c720892 Compare July 23, 2026 03:55
Comment thread packages/worker/src/email/inbound.ts
@cursor
cursor Bot force-pushed the cursor/review-fix-account-lifecycle-aeba branch from 40e22ca to e78e4cf Compare July 23, 2026 04:02
@cursor
cursor Bot force-pushed the cursor/review-fix-email-durability-70f3 branch from 3d38a9d to fcbb20d Compare July 23, 2026 04:07

@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 b6c2c8d. Configure here.

Comment thread packages/worker/src/email/package-subscriptions.ts
Base automatically changed from cursor/review-fix-account-lifecycle-aeba to main July 23, 2026 06:20
@cursor
cursor Bot force-pushed the cursor/review-fix-email-durability-70f3 branch from 6d378b6 to b4c8bfc Compare July 23, 2026 06:26
@github-actions

github-actions Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

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

Worker: kody-pr-891
D1: kody-pr-891-db
KV: kody-pr-891-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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/worker/src/email/package-subscriptions.ts (1)

242-247: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

dispatchEmailDeliverySubscriptionEvents drops discoveryErrors, unlike its sibling.

loadMatchingEmailSubscriptions now surfaces discoveryErrors for manifest-load failures, and dispatchInboundEmailSubscriptionEvents (Lines 183-232) correctly checks them and throws "dispatch was incomplete" so the caller can retry. Here the same discovery-error information is discarded — only subscriptions is destructured — so a package whose manifest failed to load during a delivery-status update silently never receives that webhook, and nothing signals the dispatch as incomplete for reconciliation/retry.

🐛 Proposed fix: surface discovery errors the same way the inbound path does
-	const { subscriptions } = await loadMatchingEmailSubscriptions({
+	const { subscriptions, discoveryErrors } = await loadMatchingEmailSubscriptions({
 		env: input.env,
 		baseUrl,
 		userId: input.message.userId,
 		topic: emailDeliveryUpdatedTopic,
 	})

Then, after the invocation loop, throw when discoveryErrors.length > 0, mirroring the "dispatch was incomplete" pattern used in dispatchInboundEmailSubscriptionEvents.

🤖 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/package-subscriptions.ts` around lines 242 - 247,
Update dispatchEmailDeliverySubscriptionEvents to retain discoveryErrors from
loadMatchingEmailSubscriptions, process all successfully loaded subscriptions as
before, then after the invocation loop throw the same “dispatch was incomplete”
error used by dispatchInboundEmailSubscriptionEvents when discoveryErrors.length
is greater than zero.
packages/worker/src/email/parser.ts (1)

111-130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the 512 KB raw MIME cap on the direct parsing path.

inbound.ts now calls readForwardableEmailRawMime/parseForwardableEmailRawMime directly, and the only pre-parse rejection is the email_message_bytes entitlement check. With Cloudflare inbound messages potentially much larger, add the maxRawMimeBytes guard before returning from readForwardableEmailRawMime/parseForwardableEmailRawMime or switch these direct call sites back through parseForwardableEmailMessage.

🤖 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/parser.ts` around lines 111 - 130, Preserve the 512
KB raw MIME limit for direct callers of readForwardableEmailRawMime and
parseForwardableEmailRawMime. Add the maxRawMimeBytes validation to the shared
direct parsing flow before parsing or returning the raw MIME, or route
inbound.ts through parseForwardableEmailMessage so the existing guard is always
applied.
🧹 Nitpick comments (1)
packages/worker/src/email/package-subscriptions.ts (1)

138-152: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Unbounded concurrency for manifest loads, unlike the chunked admin dispatcher.

admin-package-subscriptions.ts's discovery loop uses mapSettledInChunks for bounded concurrency, but this loads all savedPackages manifests concurrently via a flat Promise.allSettled. Likely low risk since this is scoped to one user's saved packages, but worth aligning with the chunked pattern if a user can accumulate many saved packages.

🤖 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/package-subscriptions.ts` around lines 138 - 152,
The manifest discovery in the savedPackages flow launches every
loadPackageManifestBySourceId call concurrently through Promise.allSettled.
Replace the flat mapping with the existing mapSettledInChunks pattern used by
the admin subscription discovery, preserving the current callback behavior and
settled-result handling while applying bounded concurrency.
🤖 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/usage/aggregate-rollups.workers.test.ts`:
- Around line 58-65: Scope both fixture mutations in the test cleanup by userId:
add the user_id predicate to the DELETE on email_messages and the UPDATE on
email_delivery_events, binding userId alongside messageId in each statement.
Preserve the existing cleanup behavior while ensuring both writes target only
the current user’s rows.

---

Outside diff comments:
In `@packages/worker/src/email/package-subscriptions.ts`:
- Around line 242-247: Update dispatchEmailDeliverySubscriptionEvents to retain
discoveryErrors from loadMatchingEmailSubscriptions, process all successfully
loaded subscriptions as before, then after the invocation loop throw the same
“dispatch was incomplete” error used by dispatchInboundEmailSubscriptionEvents
when discoveryErrors.length is greater than zero.

In `@packages/worker/src/email/parser.ts`:
- Around line 111-130: Preserve the 512 KB raw MIME limit for direct callers of
readForwardableEmailRawMime and parseForwardableEmailRawMime. Add the
maxRawMimeBytes validation to the shared direct parsing flow before parsing or
returning the raw MIME, or route inbound.ts through parseForwardableEmailMessage
so the existing guard is always applied.

---

Nitpick comments:
In `@packages/worker/src/email/package-subscriptions.ts`:
- Around line 138-152: The manifest discovery in the savedPackages flow launches
every loadPackageManifestBySourceId call concurrently through
Promise.allSettled. Replace the flat mapping with the existing
mapSettledInChunks pattern used by the admin subscription discovery, preserving
the current callback behavior and settled-result handling while applying bounded
concurrency.
🪄 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: 8ac0619e-17c9-4bd0-a71d-914581d146f4

📥 Commits

Reviewing files that changed from the base of the PR and between f2c2407 and b4c8bfc.

📒 Files selected for processing (25)
  • packages/worker/migrations/0089-email-delivery-reconciliation-index.sql
  • packages/worker/src/email/inbound-delivery.ts
  • packages/worker/src/email/inbound-effects.ts
  • packages/worker/src/email/inbound-entitlements.workers.test.ts
  • packages/worker/src/email/inbound-reconciliation-index-migration.node.test.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/package-subscriptions.ts
  • packages/worker/src/email/parser.ts
  • packages/worker/src/email/reconcile-inbound-deliveries.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/email/system-email.workers.test.ts
  • packages/worker/src/index.ts
  • packages/worker/src/index.workers.test.ts
  • packages/worker/src/package-invocations/admin-package-subscriptions.ts
  • packages/worker/src/package-invocations/idempotent-module-invocation.ts
  • packages/worker/src/package-invocations/module-artifacts.ts
  • packages/worker/src/package-invocations/repo.ts
  • packages/worker/src/package-invocations/service.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.node.test.ts
  • packages/worker/src/usage/aggregate-rollups.ts
  • packages/worker/src/usage/aggregate-rollups.workers.test.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/worker/src/index.ts
  • packages/worker/src/package-invocations/admin-package-subscriptions.ts
  • packages/worker/src/email/system-email.workers.test.ts
  • packages/worker/src/index.workers.test.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/package-invocations/repo.ts
  • packages/worker/src/email/inbound-effects.ts
  • packages/worker/src/usage/aggregate-rollups.ts
  • packages/worker/src/email/repo.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/reconcile-inbound-deliveries.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/inbound-delivery.ts

Comment thread packages/worker/src/usage/aggregate-rollups.workers.test.ts Outdated
@kody-bot
kody-bot merged commit a7355eb into main Jul 23, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/review-fix-email-durability-70f3 branch July 23, 2026 07:09
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