Repository navigation
Add run records: per-user execution history off the shared D1 writer - #955
Conversation
Replace package_runtime_runs inference with an authoritative package_service_states D1 projection written on service lifecycle transitions and heartbeaten while running, so concurrency quota is not derived from stranded history rows and survives the runs-table move. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Introduce the run-records primitive: a user-scoped SQLite Durable Object for execution history, a never-blocking begin/finish service API with eager vs on-failure persistence, retention inside the DO, and workers runtime coverage for the shared contract. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Stop reporting sandbox/workflow user-code throws as platform bugs by throwing UserCodeError at known boundaries and dropping it in beforeSend, while keeping string-match filters as backstops. Also record inline and package workflow runs with workflowId via begin/finishRunRecord. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Surface per-user run records in a failures-first Activity page with status/surface filters, summary counts, detail logs, and cursor pagination. Repoint Jobs recent runs to listRunRecords and link each run into Activity. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Include run records in account export/deletion, union RunLog storage ids with legacy D1 sources so StorageRunner buckets are not leaked, leave DR explicitly out of scope for observability data, and keep legacy D1 retention lanes draining old package_runtime_* rows. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Add a runs capability domain (run_list, run_get, run_summary) over the per-user RunLog DO, delete package_debug_* capabilities, and repoint job_get recent_runs plus webhook_delivery_list onto listRunRecords. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Webhook run records store camelCase httpStatus/payloadBytes without an outcome field; map 4xx errors to rejected and other failures to failed. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Delete package-runtime-debug D1 writers and wire execute, jobs, apps, services, retrievers, invocations, and webhooks through beginRunRecord/ finishRunRecord. Close coverage gaps for ad-hoc execute (on-failure) and standalone kody.json jobs, pass package-app console logs through the runtime bridge, stop writing jobs.run_history_json, and replace webhook_deliveries inserts with webhook run records. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
The ledger test hardcoded prefix 0095, which collides with the new package_service_states migration and would break on every future migration. Derive both the expected prefixes and the monotonic-addition fixture name from the live ledger instead. Admin usage stub now answers the package_service_states count query that replaced the package_runtime_runs history scan. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Add the architecture doc and primitives map entry for per-user RunLog history, garden stale storage/entitlement/webhook docs, and publish end-user Activity guidance. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Webhook delivery history now comes from run records, so the D1-backed listWebhookDeliveries* readers were unreachable. Register the RunLog DO in the user-owned surface registry so the out-of-band DO inventory stays complete, and point the package-app crash page at Account -> Activity. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Track packageSecrets.get values on the runtime bridge, redact logs and errors at packageRuntimeRunFinish before persistence, and schedule finish via waitUntil so app_fetch/app_realtime responses are not blocked. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Keep finish cheap with a stored run counter and every-Nth/alarm prune, reconcile stale running rows, add recordRunRecord plus finish waitUntil, and support name list filters with covering indexes. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
The allowlist still named the deleted package-runtime-debug warnings, so the run-record and activation warnings that replaced them escaped the console spy and turned an incidental missing-table warning into a failed package run. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Pin that an unhandled surface yields a null handle and that a failing activation milestone write still leaves the observed run successful. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Record inbound webhook deliveries with one recordRunRecord RPC (real startedAt + waitUntil off the response path) and always store an explicit outcome. Push webhook name filtering into listRunRecords so delivery list no longer under-reports when a package has multiple webhooks. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…rror Keep the outer workflow begin/finish (workflowId + pre-invocation failures) and have resolveInvocationRuntimeSurface return null for package-workflow so the inner invoke does not open a second row. Only mark sandbox/4xx package workflow failures as UserCodeError; known infrastructure codes and other 5xx stay ordinary Errors for Sentry. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…ervices Activation now increments a durable per-package success counter instead of counting retention-pruned history, so package_activated can still be awarded. Account deletion/export enumerate package services from package_service_states (unioned with legacy runs), and PackageServiceInstance projects state on restore so the inventory converges after upgrade. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Self-terminate the RunLog alarm when nothing is due, skip alarm storage on hot finishes via in-isolate caches, and skip DDL on schema-version matches so parallel workers tests are not dominated by per-finish getAlarm/setAlarm. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Subscription dispatch was awaiting per-run finish writes on the single-threaded RunLog DO, which serialized concurrent package handlers and timed out under the full workers suite. Thread optional waitUntil through runBundledModuleWithRegistry and the invocation callers that have an ExecutionContext so observability stays off the critical path. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Each RunLog Durable Object RPC costs roughly 400ms in the vitest workers pool while the production finish path measures ~0.4ms, so this suite was timing out under full-validate contention. The previous explicit 15s cap was also below the 20s CI default. Budget it like the other DO-heavy suites, pipeline independent seeds, and drop clears that were redundant because every test mints a fresh user id. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
User modules resolve console from globalThis, so a lexical const console shim in evaluate() never ran. Override globalThis.console like fetch, restore it in finally for worker reuse, and cover the capture path end to end through RunLog. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
The sandbox flattens console output to strings and encodes the level as a leading marker, so every persisted line landed as level 'log'. The Activity page renders that level in a gutter and colours by it, so warn and error lines were indistinguishable from ordinary output. Parse the marker back into the structured level the schema already supports. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Package subscription dispatch boots the real Worker Loader sandbox, which costs seconds per run, while the shared default is 5s locally. These suites flaked under a loaded validate even though the production write path measures well under a millisecond. Budget them like the other sandbox-executing suites. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
|
Important Review skippedToo many files! This PR contains 127 files, which is 27 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (132)
You can disable this status message by setting the ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
🔎 Preview deployed: https://kody-pr-955.kody-a99.workers.dev Worker: Mocks:
|
Clear the idle-confirmed fast path on every start/finish write and schedule the soonest age/stale due-time so warm low-traffic RunLogs still self-prune without scanning pending work on every finish. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
| status: statusFilterToRunStatus(statusFilter), | ||
| surface: surfaceFilterToRunSurface(surfaceFilter), | ||
| since, | ||
| }, |
There was a problem hiding this comment.
Activity list capped seven days
Medium Severity
loadAccountActivityData applies the same seven-day since window to listRunRecords as to the summary tiles, so /account/activity only lists runs from the last week even though the page states records are kept about thirty days and supports cursor pagination. Runs between seven and thirty days old can still load by direct id via getRunRecord, but they never appear in the sidebar list or “load more” pages.
Reviewed by Cursor Bugbot for commit ab1e78f. Configure here.
Align count eviction with age prune by deleting only success then error rows. Stale running still converges because reconcile demotes them to error before the cap pass. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 26a1a44. Configure here.
| status: run.status, | ||
| durationMs: run.durationMs ?? 0, | ||
| error: run.errorMessage, | ||
| })) |
There was a problem hiding this comment.
Running jobs show fake finish time
Medium Severity
After recent runs come from RunLog, in-flight job rows can have a null finishedAt. Mapping null to startedAt makes the Jobs table “Finished” column show the start time while status is still running, which reads as a completed run at a glance.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 26a1a44. Configure here.
| status: run.status, | ||
| durationMs: run.durationMs ?? 0, | ||
| error: run.errorMessage, | ||
| })) |
There was a problem hiding this comment.
Running jobs show zero duration
Low Severity
Recent runs from RunLog leave durationMs null while status is running. Coercing null to 0 makes the Jobs duration column show 0 ms for active executions instead of an in-progress or unknown duration.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 26a1a44. Configure here.
Repoint DR inventory, account deletion, and account export off package_runtime_runs onto user_storage_buckets plus manifest/state service enumeration so post-#955 buckets and idle services stay discoverable. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…m run history (#965) * feat(storage): track user storage bucket ownership in D1 Add user_storage_buckets as authoritative state (with a one-time package_runtime_runs backfill), register buckets on StorageRunner mutators with isolate-local dedupe, and cover the table in account export/deletion targets. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(account): stop enumerating storage and services from run history Repoint DR inventory, account deletion, and account export off package_runtime_runs onto user_storage_buckets plus manifest/state service enumeration so post-#955 buckets and idle services stay discoverable. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(account-export): keep discovery paging on D1 keyset SQL durable_object_summaries pages again over user_storage_buckets and entity/state tables without per-page manifest fetches. Manifest-inclusive enumeration stays on deletion and one-shot full export inventory. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(account-export): align discovery counts and cheap section probes Make storage_runners counts match D1 discovery paging, probe ownership and package-service lookups without full inventory rebuilds, batch the service-stage base check, and surface manifest degradation in deletion warnings so incomplete purges cannot look clean. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(account-export): treat malformed service storage ids as not found Guard decodeURIComponent on caller-supplied service: storage ids so a bare % returns the normal not-found path instead of throwing URIError. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(storage): do not register ownership on clearStorage Account deletion clears StorageRunner DOs then deletes user_storage_buckets. Registering on clearStorage could fire-and-forget an upsert that recreates rows for an already-deleted user. Registration stays on genuine write paths. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> * fix(account): restore RunLog storage ids and legacy service deletion Union listRunRecordStorageIds into export count, ownership probes, and discovery paging so RunLog-only buckets stay exportable and count≡paging. Restore package_runtime_runs on the deletion path only (issue #956), and dedupe package-manifest loads within a single inventory request. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>


Users could not see why their own jobs, apps, or executions failed — that detail lived only in Workers logs and Sentry. Meanwhile Sentry was collecting user-code errors nobody at Kody can act on.
This adds one run records primitive that both halves needed, and consolidates the four private run logs that had grown up around it.
Why one primitive
"Something ran, here is the status and error" was implemented seven times, in whatever shape each feature needed. A package-defined job execution wrote a
jobs.run_history_jsonentry, apackage_runtime_runsrow, and an Analytics Engine datapoint — three records of one event, three retention policies, two readable surfaces, no shared shape.Two rules decide what merged:
runningforever. That is now a real state table.package_invocationsandstripe_webhook_eventsare correctness mechanisms and stay in D1 untouched.Why not D1
Run records are per-event writes, and D1 is a single shared writer. This repeats the decision already made for usage metering, which
record-usage.tsstates explicitly: "a per-event D1 upsert would serialize every metered request on D1's single writer." That decision lived only in a code comment, which is whypackage_runtime_runswas later built in D1 doing exactly what the comment forbids. It is now an invariant inprimitives.yaml.Records live in a per-user
RunLogDurable Object. Each user's history is its own SQLite with no cross-tenant contention, there is nouser_idcolumn because the DO is the user, and retention self-prunes per user instead of an hourly cron scanning a shared table.What users get
/account/activity— failures first, with the captured console logs, filters by surface and status, and cursor pagination. Agents get the same data viarun_list/run_get/run_summary.Coverage gaps closed: ad-hoc
execute, standalonekody.jsonjobs, and inline workflows recorded nothing before because the old store required apackage_id.Two bugs found while verifying
const consoleinsideevaluate(), but user code is a separate imported module, so it resolved the global instead. Every execution returnedlogs: []. Pre-existing onmain(reproduced there); fixed the same wayfetchwas already handled. Without this the Activity page would have shipped showing "No log lines were captured" for every run.recordSuccessfulPackageRuncounted successes frompackage_runtime_runs, which this PR stops writing, sopackage_activatedbecame unreachable. Now backed by a durable counter.Reviewing this
Start with
packages/worker/src/run-records/types.ts— it is the contract everything else codes against. Thenservice.ts(worker API),run-log-do.ts(the store).Nothing is dropped.
package_runtime_runs,package_runtime_logs,jobs.run_history_json, andwebhook_deliveriesstop being written but remain in place, so this is revertible. A follow-up drops them; the legacy read arms are commented as such.Deliberately out of scope, and documented rather than left ambiguous: run records are not covered by DR backup (observability data with 30-day self-retention), and cross-user error queries are now a sampled Analytics Engine question rather than a D1 scan. Sentry keeps the errors Kody must act on, the DO keeps the errors the user must act on.
Verification
npm run validategreen: 1405 tests across 420 files, 21 Playwright E2E, MCP E2E, plusprimitives:checkandmigrations:check.Manually verified end to end against a real MCP session: two genuinely failing
executecalls produced run records with their console output, and a successful one correctly persisted nothing.Two independent review passes (hard-to-reverse/security, and performance) ran before this was opened; their blockers are fixed in-branch.
System recap — adds a new primitive (high risk)
Mode: recap · Base:
main@6fed00ac· Head:59a1e511Classification: adds — introduces the
run-recordsprimitive and two new invariants; several existing primitives change shape around it.Primitives touched
run-recordsRunLogDO,runscapability domainjobsrun_history_json; keepslast_run_*as statewebhookswebhook_deliveriesworkflowsUserCodeErrornarrowedentitlementspackage_service_states, not historypackage-appspackage-servicescapabilities-executecapability-registryrunsdomain added,package_debug_*removedapp-ui/account/activityaccount-exportrun_recordsexport section, DO purge on deletionusage-meteringd1-app-db0095,0096scheduled-cronSystem map
Execution surfaces stop writing private D1 run logs and report through one per-user Durable Object, which the UI and capability graph read.
Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).
Before / after
package_runtime_runs(shared writer)RunLogDO SQLite/account/activityInvariants
Adds two, both previously unwritten decisions:
state-vs-history— entity rows hold current state; run records hold history. Never derive state from history.no-per-event-shared-writes— per-event writes go to Analytics Engine or per-user DO SQLite, never the shared D1 writer.per-user-isolationis strengthened rather than touched: the store is keyed by user id with nouser_idcolumn to filter on.