Skip to content

feat(run-store): add an execution-snapshot store decorator behind an off-by-default dial - #4765

Draft
d-cs wants to merge 13 commits into
mainfrom
feat/snapshot-store-decorator-tri-13449
Draft

feat(run-store): add an execution-snapshot store decorator behind an off-by-default dial#4765
d-cs wants to merge 13 commits into
mainfrom
feat/snapshot-store-decorator-tri-13449

Conversation

@d-cs

@d-cs d-cs commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a RunStore decorator that mirrors execution snapshots into Redis alongside Postgres, plus the orphan-key sweep and the fault-injection suite that prove the write protocol converges after a crash. Nothing constructs it, so merging this changes no behaviour: the configuration, the production wiring and the Redis client all arrive in later work.

The execution-state log is the hottest table in the run graph, and moving it out of Postgres has to happen without a big-bang cutover. This is the attachment point for that: a decorator that wraps the existing storage interface and intercepts only the methods that touch snapshots, so none of the many callers change.

Design

Write order is the correctness property, and the two orders differ on purpose.

A transition writes Postgres first and Redis second. A crash in the gap leaves a run whose latest snapshot is stale, which is the state the heartbeat stall watchdog already heals in production today.

A birth writes Redis first and Postgres second. A crash there leaves an unreachable key for a run that does not exist. Postgres first would instead leave a run with no snapshot at all, which the engine treats as a hard error, so the run would be stuck.

Each order is chosen so the state a crash leaves behind is the harmless one. A lost cross-store write is never recovered by a transaction or an outbox; recovery is always the existing stall and repair job. A failed append retries, then hands the run to that job, and never rethrows, because Postgres has already committed and a throw would turn a healable gap into a caller-visible error.

Inside a transaction the Redis half is staged and flushed only after the commit, so a rollback cannot leave Redis holding a transition that never happened.

Reads are shape matched. Two of the snapshot reads take arbitrary Prisma arguments, and a key-value store cannot answer an arbitrary query, so the decorator recognises exactly the shapes the engine sends and delegates everything else. A miss falls back to Postgres, which is also how runs created before any cutover keep working.

The sweep reaps under two rules, because neither can see what the other leaves behind. A finished run whose keyspace never received its completion expiry gets one applied. A keyspace with no run row at all, past an age threshold, is deleted; that is a crashed birth, which is non-terminal so it carries no expiry and has no run row, so the first rule can never match it.

Inertness

Three independent reasons this is a no-op if merged alone:

  • Nothing constructs the decorator or the Redis store outside tests.
  • No configuration reaches it, so the dial stays at its off position, which is a pass-through that makes no Redis call.
  • The existing Postgres store gains an off-by-default flag and two optional input fields. Both default to today's behaviour, and only the decorator would ever supply them.

Notes for review

The snapshot id and the creation instant are both minted by the decorator and written into both stores, so one snapshot has one identity and one timestamp wherever it is read. Without that, the two stores disagree on values that later tooling has to compare, and the cursor for a snapshot window resolved from one store misfilters the window walked in the other.

Three defects in this work passed the full existing test suites before being found by review rather than by a test: the decorator wrote no wait cycle at all, the snapshot window dropped the ordering used to give each completed waitpoint its position in a batch, and the two stores stamped different creation times. The common cause was that no test drove a snapshot that actually carried waitpoints, and that the parity suite compared a timestamp against a value it had just read back from the row it was checking. Both gaps now have tests.

d-cs added 12 commits August 24, 2026 14:48
The decorator that dual-writes snapshots to Redis has to own the snapshot id, or
the same snapshot carries a different id in each store and the comparator chases
a difference that is not real.

Four of the six snapshot input types had no id field, so four write sites could
not carry one. Add it to CompletionSnapshotInput, ExpireSnapshotInput,
RescheduleSnapshotInput and CreateExecutionSnapshotInput, and thread it through
every nested create. createCancelledRun built its create inline and dropped the
id its input already carried; it now passes it too.

The field is optional everywhere, so an absent id still falls through to
Prisma's @default(cuid()) and no existing caller changes.
…ators

RunStore has 71 members. A decorator that intercepts a dozen of them should not
restate the other 59 forwarders alongside its real logic, and hand-writing them
invites a typo no test would catch.

Generate the base from the interface instead. The generator also emits the
member-name lists, so the suite can assert that the class and the interface hold
exactly the same members: a method added to RunStore and not to the base fails a
test rather than becoming a silent hole in the decorator.

The one data property on the interface becomes a getter over the delegate, read
live rather than captured, so a delegate whose client changes is not cached.
…parity tests

No nested write site returns the snapshot it created: createRun returns the run,
expireParkedRun returns a count, and the rest return a selected TaskRun. So the
Redis entry is built from each site's own input plus the caller-minted id.

That means every value Postgres derives rather than receives has to be
reproduced: the DEQUEUED-to-PENDING rewrite, the four values lockRunToWorker
hard-codes, the three rescheduleRun defaults, and the engine column default a
completion leaves unset.

The parity suite covers all ten physical write sites, comparing the built entry
against the row Postgres actually wrote. It caught the dropped id in
createCancelledRun.
…write

The last dial position makes the Redis store the sole snapshot writer, so
Postgres has to stop writing snapshot rows without changing anything else it
does. One constructor flag does that across all ten write sites.

With it off, the nine nested creates are omitted and the run mutation still
lands; createExecutionSnapshot echoes its input in the shape callers expect
rather than inserting; and the completed-waitpoint join inserts are skipped,
since they would otherwise link to a row that no longer exists.

Defaults to true, so every existing caller and test is unaffected.
A decorator over any RunStore that also writes execution snapshots to Redis. It
overrides only the methods that touch a snapshot and inherits the rest.

Write order is the correctness property, and the two orders differ on purpose. A
transition writes Postgres first: a crash in the gap leaves a stale latest
snapshot, which the heartbeat stall watchdog already heals. A birth writes Redis
first: a crash there leaves an unreachable key for a run that does not exist,
where Postgres-first would leave a run with no snapshot at all and no way to
read one. Each order is chosen so the crash state is the harmless one.

A failed transition append retries three times, then hands the run to the repair
job. It never rethrows, because Postgres has already committed and a throw would
turn a healable gap into a caller-visible error. A failed birth append is
survivable before redis-only, where Postgres still holds the snapshot, and
refuses at redis-only, where it would otherwise create a run with no snapshot
anywhere; refusing works only because the birth append comes first.

None of the four non-failure append outcomes enqueues a repair: an absent
keyspace is every pre-cutover run's transitions, a fork means another writer
advanced the head, a duplicate is a retry that landed, and a cycle mismatch is
the store refusing an untrustworthy pointer on purpose.

At mode off the decorator makes no Redis call and builds no entry.
…ore handles

Proves the deferral from inside the transaction callback rather than assuming it:
a staged append is absent from Redis while the transaction is open and present
once it commits, and a rollback leaves both stores agreeing the transition never
happened.
The engine resolves its since-cursor to a createdAt before it asks for the
window, so the snapshot id is gone by then and the id-addressed read cannot
serve it. Adding a cursor-addressed read is the alternative to changing the
engine's read path, which stays untouched.

The cursor is exclusive and keeps the same-millisecond blind spot the Postgres
read has. Matching it is the requirement, not an oversight: a Redis read that is
more correct than the Postgres read shows up as divergence during compare mode,
which exists to surface real defects. Closing the blind spot needs seq ordering
on both sides and belongs after the cutover.

The walk goes newest-first and stops at the first entry at or before the cursor,
so its length is the length of the answer rather than the run's history.

This adds a read operation. It does not touch the append script, the keyspace,
or the write-ordering protocol.
…back

Two of the five snapshot reads take arbitrary Prisma arguments, and a key-value
store cannot answer an arbitrary query. Only three production call sites exist,
all in the engine's executionSnapshotSystem, and both generic ones send a single
fixed shape, so the decorator recognises exactly those shapes and delegates
everything else. Each matcher rejects an argument object carrying a key it does
not know, because a query that has drifted must be answered correctly by
Postgres rather than approximately from Redis.

A miss is the coexistence path, not an error: a pre-cutover run or expired
history falls back to Postgres. The entry supplies every scalar column, and the
checkpoint and waitpoint rows are read back through the delegate only when the
entry says they exist, so the common read of a running run makes no Postgres
call at all.

Which runs read from Redis is a hash of the run id, so a run does not change
store between two reads of one poll, two instances of the same dial agree, and
raising the dial only ever adds runs to the cohort.
Two rules, because neither can see what the other leaves behind. A terminal run
whose keyspace never received the completion expiry gets one applied, so it
reaps on the schedule a healthy terminal append would have set. A keyspace with
no run row at all, past an age threshold, is deleted outright — that is a
crashed birth, which is non-terminal so it carries no expiry and has no run row,
so the first rule can never match it.

It never reaps on an unknown answer: a live run is left alone however old its
keyspace, a young orphan is left for the birth that may still be in flight, and
a batch whose run lookup failed is skipped rather than treated as absent.

Run rows are resolved through the run store rather than a raw client, because
under the run-ops split a run can live on either database and a raw lookup would
report a live run as an orphan.

Nothing schedules this. The engine's worker has to run it, and run-store cannot
reach the engine.

Also moves the decorator suites onto the worker-scoped container fixture. The
per-test one boots a Postgres and a Redis container for every test, which is
what the replication tests need and these do not; the sweeper suite alone went
from repeated two-minute timeouts to ten seconds.
…eads on

The engine's own flows, driven against the decorator with every snapshot read
served from Redis, injected through the store seam that runStoreInjectability
already proves. Same flows, same expectations, different store underneath — the
point is that nothing in the engine has to know, so no existing suite changes.

Covers a run driven to completion, the execution data at each step, a
since-window wider than the fifty cap, and a pre-cutover run with no keyspace
falling back to Postgres.

The environment-boundary test asserts parity rather than a fixed shape: whatever
Postgres answers for a foreign environment, Redis has to answer the same, or the
tenant boundary behaves differently once reads move over.
…oth stores

Three defects, all of which passed the existing suites because no test drove a
snapshot that actually carried waitpoints, and because the parity suite compared
createdAt against a value it had just read back from the row.

The decorator never passed a cycle to the append, so no wp:<cycleSeq> key was
written for any snapshot and the completed-waitpoint side of Redis was
permanently empty. It now mints a cycle when the id set differs from the current
head and carries the previous cycleSeq forward when it does not, so a resume
writes the record set once and the copy-forwards that follow write no key at
all.

The since-window hydration returned an empty completedWaitpointOrder. That
column is not the join: the engine reads it off the head row as the oracle that
gives each completed waitpoint its position in a batch, so an empty order
resumed every batched triggerAndWait with an undefined index.

Seven of the eight write sites stamped the entry from the app clock while
Postgres stamped its own column default, so the two stores held different
instants for one snapshot. The decorator now supplies createdAt, and an equal
updatedAt, at every site, and the standalone path supplies it too rather than
reading the row back. Beyond making the field comparable, this aligns the
since-window: the cursor is resolved from one store and applied in the other,
and two different instants misfilter that window.

The parity suite gains an independent clock-provenance guard, and a case proving
an absent instant still takes the database default, which is what keeps the
store's behaviour unchanged while the decorator is off.
…oint

The generator that emits the pass-through store base is a runnable script, not
dead code, and the same glob covers any script added there later.
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1d4eb7b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds a Redis-backed execution snapshot decorator with rollout modes, sampled reads, transaction staging, retries, repair handling, and waitpoint-cycle support. PostgreSQL snapshot writes can be disabled while preserving run mutations. Snapshot IDs and timestamps can be shared across stores. Redis gains timestamp-window reads and orphan sweeping. Generated delegation infrastructure keeps RunStore forwarding synchronized. Integration tests cover parity, lifecycle reads, crash recovery, stale snapshots, retries, and cleanup.

Merge Risk: 🟡 Moderate · up to 1d4eb

The snapshot orphan sweeper does not honor custom key prefixes, so configured stores can retain orphaned Redis data, and the test suite still uses mocks where the repository requires Testcontainers; merge should wait for these bounded correctness and validation issues to be fixed or explicitly accepted.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the design and scope well, but it omits the template's issue, checklist, testing, changelog, and screenshots sections. Add the required template sections, including an issue reference, completed checklist, explicit testing steps, changelog entry, and screenshots or a clear statement that none apply.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new run-store execution-snapshot decorator and its off-by-default rollout dial.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
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 feat/snapshot-store-decorator-tri-13449

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.

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

Actionable comments posted: 6

🧹 Nitpick comments (6)
internal-packages/run-store/src/snapshotEntry.ts (1)

37-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add development crumbs to the new snapshot routing logic.

  • internal-packages/run-store/src/snapshotEntry.ts#L37-L158: mark entry derivation paths with // @crumbs`` or an @crumbs region.
  • internal-packages/run-store/src/snapshotReadShapes.ts#L36-L95: mark accepted and rejected query-shape branches with // @crumbs`` or an @crumbs region.

As per coding guidelines, “Add crumbs as you write code — not just when debugging.”

Source: Coding guidelines

internal-packages/run-store/src/redisSnapshotStore.ts (1)

502-529: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared window-decoding block.

Lines 502-529 duplicate lines 443-471 of getSince verbatim, including the headSurvived invariant and the cycle-mismatch check. The two paths must stay in agreement. Extract a private helper that takes the reply array and the options, and call it from both readers.

♻️ Sketch of the extraction
+  `#decodeWindow`(
+    reply: string[],
+    runId: string,
+    environmentId: string | undefined
+  ): { entries: SnapshotRead[]; headWaitpointIds: WaitpointIds } {
+    const headOrder = reply[1] ?? "";
+    const rows: SnapshotRead[] = [];
+    let headSurvived = false;
+    for (let i = 2; i + 3 < reply.length; i += 4) {
+      const decoded = this.#decode(
+        [reply[i], reply[i + 1], reply[i + 2], reply[i + 3], ""],
+        environmentId,
+        runId,
+        false
+      );
+      if (decoded) {
+        rows.push(decoded);
+        if (i === 2) headSurvived = true;
+      }
+    }
+    rows.reverse();
+    const head = headSurvived ? rows[rows.length - 1] : undefined;
+    const headWaitpointIds = decodeWaitpointIds(head !== undefined, head ? headOrder : "");
+    if (head) {
+      head.completedWaitpointIds = headWaitpointIds;
+      if (head.cycle) {
+        this.#checkCycleMismatch(runId, head.cycle.count, headWaitpointIds.order.length);
+      }
+    }
+    return { entries: rows, headWaitpointIds };
+  }
internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts (1)

204-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the completed-waitpoint join gate with a non-empty id list.

This test passes completedWaitpointIds: [], so #connectCompletedWaitpoints is a no-op even without the new snapshotWrites gate at PostgresRunStore.ts Line 1312. The test therefore does not prove the gate works. Add a case that seeds a waitpoint and passes its id in completedWaitpointIds, then assert that no _completedWaitpoints join row exists when snapshotWrites is false.

internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts (1)

705-715: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The window hydration issues one round trip per returned entry.

#hydrate runs once per entry in the window. Each call can add a redis.getSnapshotWaitpointIds round trip when read.completedWaitpointIds is absent, and a delegate findExecutionSnapshot Postgres query when entry.checkpointId is set. With take: 50 that is up to 50 extra Redis calls and 50 extra Postgres queries for one read that is meant to avoid Postgres.

Consider hydrating the waitpoint ids and the checkpoint rows for the head entry only, or batching the checkpoint lookup into a single delegate query keyed by the snapshot ids in the window.

internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts (1)

250-261: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The born-terminal test does not assert the completion expiry.

The test name states that the birth applies the completion TTL. The assertions only check that both reads are non-null. Those assertions pass whether or not the birth set an expiry, so the test cannot detect the regression it describes.

Assert the TTL directly on the terminal keyspace and assert its absence on the non-terminal one.

💚 Proposed assertion using a probe client
         const terminal = await redis.getLatest(runId);
         const alive = await redis.getLatest(nonTerminal);
         expect(terminal).not.toBeNull();
         expect(alive).not.toBeNull();
+
+        // The birth is the only write a born-terminal run gets, so it must carry the TTL itself.
+        const terminalTtl = await probe.pttl(`snap:{${runId}}:e`);
+        const aliveTtl = await probe.pttl(`snap:{${nonTerminal}}:e`);
+        expect(terminalTtl).toBeGreaterThan(0);
+        expect(terminalTtl).toBeLessThanOrEqual(COMPLETED_TTL_MS);
+        expect(aliveTtl).toBe(-1);

Create probe with createRedisClient(redisOptions, { onError: () => {} }), as the waitpoint-cycle suite does, and quit it in the finally block.

internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts (1)

69-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

seedRun appends the birth twice.

build() uses mode redis-read or dual-write, so decorated.createRun already appends the birth to Redis before it writes Postgres. The direct redis.append above it repeats that write with the same snapshot id and an earlier createdAt.

Two consequences follow. The keyspace exists before the decorator runs, so every test in this suite would still pass if the decorator stopped writing births. And the Redis entry keeps the fixture's timestamp while Postgres records the decorator's, so the birth loses timestamp parity inside this suite.

♻️ Proposed fix: let the decorator write the birth
-  await redis.append({
-    entry: entryFromCreateRun({ id: snapshot.id, runId, createdAt: new Date() }, snapshot),
-    kind: "birth",
-    isTerminal: false,
-  });
   await decorated.createRun({ data: buildCreateRunData(runId, env), snapshot });
   return runId;

The redis parameter and the entryFromCreateRun import then become unused in this file.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 84c3b0f1-469a-4170-98b4-5c0a5092a064

📥 Commits

Reviewing files that changed from the base of the PR and between cc69ff4 and ea0e17b.

📒 Files selected for processing (32)
  • internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-store/scripts/generateDelegatingRunStore.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/PostgresRunStore.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/delegatingRunStore.ts
  • internal-packages/run-store/src/index.ts
  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/redisSnapshotStore.ts
  • internal-packages/run-store/src/runStoreMethodNames.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotEntry.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/types.ts
  • knip.json

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (31)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Prefer static imports over dynamic imports. Only use dynamic import() when:

  • Circular dependencies cannot be resolved otherwise
  • Code splitting is genuinely needed for performance
  • The module must be loaded conditionally at runtime

Files:

  • internal-packages/run-store/src/index.ts
  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/runStoreMethodNames.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/redisSnapshotStore.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-store/scripts/generateDelegatingRunStore.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-store/src/PostgresRunStore.ts
  • internal-packages/run-store/src/delegatingRunStore.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
  • internal-packages/run-store/src/snapshotEntry.ts
  • internal-packages/run-store/src/types.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • internal-packages/run-store/src/index.ts
  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/runStoreMethodNames.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/redisSnapshotStore.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-store/scripts/generateDelegatingRunStore.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-store/src/PostgresRunStore.ts
  • internal-packages/run-store/src/delegatingRunStore.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
  • internal-packages/run-store/src/snapshotEntry.ts
  • internal-packages/run-store/src/types.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • internal-packages/run-store/src/index.ts
  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/runStoreMethodNames.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/redisSnapshotStore.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-store/scripts/generateDelegatingRunStore.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-store/src/PostgresRunStore.ts
  • internal-packages/run-store/src/delegatingRunStore.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
  • internal-packages/run-store/src/snapshotEntry.ts
  • internal-packages/run-store/src/types.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Add crumbs as you write code — not just when debugging. Mark lines with
// @Crumbs or wrap blocks in `// `#region` `@crumbs. They stay on the
branch throughout development and are stripped by agentcrumbs strip
before merge.
Do not invent new namespaces — pick from this table or ask first.
Do not flag // @Crumbs markers, `#region `@crumbs blocks, or agentcrumbs
imports in reviews. These are temporary debug instrumentation stripped
before merge. Data logged in crumbs (IDs, names, values) never reaches
production.

Files:

  • internal-packages/run-store/src/index.ts
  • knip.json
  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/runStoreMethodNames.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/redisSnapshotStore.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-store/scripts/generateDelegatingRunStore.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-store/src/PostgresRunStore.ts
  • internal-packages/run-store/src/delegatingRunStore.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
  • internal-packages/run-store/src/snapshotEntry.ts
  • internal-packages/run-store/src/types.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

**/*.{test,spec}.{ts,tsx}: We use vitest exclusively. Never mock anything - use testcontainers instead.
Test files go next to source files (e.g., MyService.ts -> MyService.test.ts).

Files:

  • internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts
  • internal-packages/run-store/src/delegatingRunStore.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts
  • internal-packages/run-store/src/PostgresRunStore.snapshotWrites.test.ts
  • internal-packages/run-store/src/snapshotEntry.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotReadShapes.test.ts
  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.waitpointCycles.test.ts
internal-packages/run-engine/src/engine/tests/**/*.test.ts

📄 CodeRabbit inference engine (internal-packages/run-engine/CLAUDE.md)

Implement tests for RunEngine in src/engine/tests/ using testcontainers for Redis and PostgreSQL containerization

Files:

  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
🧠 Learnings (3)
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.

Applied to files:

  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts
  • internal-packages/run-store/src/types.ts
📚 Learning: 2026-06-16T09:19:47.637Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3960
File: apps/webapp/test/prismaInfrastructureErrorCapture.test.ts:0-0
Timestamp: 2026-06-16T09:19:47.637Z
Learning: In this repo’s Vitest setup, `vitest.config.ts` uses `globals: true`, so identifiers like `vi`, `describe`, `it`, and `expect` are available as globals in Vitest test files. During code review, do not flag missing `vi`/`describe`/`it`/`expect` imports as a runtime error or correctness issue when they’re used in `*.test.ts/tsx` or `*.spec.ts/tsx` files. Explicit imports are still preferred for consistency, but they’re not required for runtime behavior.

Applied to files:

  • internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
📚 Learning: 2026-05-18T14:40:02.173Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3658
File: packages/core/src/v3/realtimeStreams/manager.test.ts:1-147
Timestamp: 2026-05-18T14:40:02.173Z
Learning: In the triggerdotdev/trigger.dev repo, the policy “Never mock anything — use testcontainers instead” should only be enforced for integration tests that interact with real external services (e.g., Redis, Postgres) via actual infrastructure. For unit tests that exercise pure in-memory logic (e.g., cache semantics) it is OK to stub collaborators such as `ApiClient` using Vitest (`vi.fn()`) to assert call counts or control behavior. Do not flag `vi.fn()`-based `ApiClient` stubs in unit tests as violations of the testcontainers policy.

Applied to files:

  • internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts
🪛 OpenGrep (1.26.0)
internal-packages/run-store/scripts/generateDelegatingRunStore.ts

[ERROR] 105-105: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 111-111: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (20)
internal-packages/run-store/src/redisSnapshotStore.ts (2)

743-801: LGTM!


901-909: LGTM!

internal-packages/run-store/src/redisSnapshotStore.sinceCreatedAt.test.ts (1)

1-193: LGTM!

internal-packages/run-store/src/snapshotOrphanSweeper.ts (1)

42-51: LGTM!

Also applies to: 104-128

internal-packages/run-store/src/snapshotOrphanSweeper.test.ts (1)

1-345: LGTM!

internal-packages/run-store/src/PostgresRunStore.ts (1)

121-127: LGTM!

Also applies to: 648-671, 747-748, 766-766, 778-778, 795-795, 834-853, 947-961, 1154-1166, 1286-1326, 1393-1405, 1466-1480, 2000-2001, 2017-2068, 2088-2088

internal-packages/run-store/src/PostgresRunStore.snapshotId.test.ts (1)

1-150: LGTM!

internal-packages/run-store/src/taskRunExecutionSnapshotStore.off.test.ts (1)

10-94: LGTM!

internal-packages/run-store/src/taskRunExecutionSnapshotStore.readCohort.test.ts (1)

13-95: LGTM!

internal-packages/run-store/src/taskRunExecutionSnapshotStore.reads.test.ts (1)

21-397: LGTM!

internal-packages/run-store/src/taskRunExecutionSnapshotStore.births.test.ts (1)

172-174: 🩺 Stability & Availability

Do not add the quit() guard. RedisSnapshotStore.quit() already swallows connection and double-quit errors before returning.

			> Likely an incorrect or invalid review comment.
internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts (1)

639-659: 🩺 Stability & Availability

No validity-filter change is needed

			> Likely an incorrect or invalid review comment.
internal-packages/run-store/src/taskRunExecutionSnapshotStore.staging.test.ts (1)

20-27: LGTM!

Also applies to: 29-53, 67-231

internal-packages/run-store/src/taskRunExecutionSnapshotStore.transitions.test.ts (1)

33-63: LGTM!

Also applies to: 72-97, 136-454

internal-packages/run-store/src/testFixtures/snapshotIdFixture.ts (1)

20-77: LGTM!

Also applies to: 86-167

internal-packages/run-engine/src/engine/tests/helpers/decoratedStore.ts (1)

19-75: LGTM!

internal-packages/run-engine/src/engine/tests/snapshotStoreReadGate.test.ts (1)

14-53: LGTM!

Also applies to: 55-304

internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts (2)

32-58: LGTM!

Also applies to: 106-163, 279-399


232-237: 🩺 Stability & Availability

No teardown guard is needed

RedisSnapshotStore.quit() already memoizes the shutdown promise and swallows rejected redis.quit() calls, including failed connections.

			> Likely an incorrect or invalid review comment.
knip.json (1)

40-42: LGTM!

Comment on lines +10 to +24
function recordingDelegate(): { store: RunStore; calls: { name: string; args: unknown[] }[] } {
const calls: { name: string; args: unknown[] }[] = [];
const store: Record<string, unknown> = {};

for (const name of RUN_STORE_METHOD_NAMES) {
store[name] = (...args: unknown[]) => {
calls.push({ name, args });
return `result:${name}`;
};
}
for (const name of RUN_STORE_PROPERTY_NAMES) {
store[name] = `property:${name}`;
}

return { store: store as unknown as RunStore, calls };

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Replace the manual RunStore fake.

recordingDelegate() mocks every RunStore method and property. Use a concrete, Testcontainers-backed RunStore and valid operations instead.

As per coding guidelines, “We use vitest exclusively. Never mock anything - use testcontainers instead.”

Source: Coding guidelines

Comment thread internal-packages/run-store/src/snapshotEntry.parity.test.ts
Comment thread internal-packages/run-store/src/snapshotFaultInjection.ts Outdated
Comment thread internal-packages/run-store/src/snapshotOrphanSweeper.ts
Comment thread internal-packages/run-store/src/snapshotOrphanSweeper.ts
@d-cs d-cs self-assigned this Aug 24, 2026
…arity real

The sweep discovered keyspaces by their cur key, which the append script writes
only when an entry is valid. A keyspace whose entries all carry an error has no
cur and no index, so neither sweep rule could ever see it and it leaked with no
expiry, which is the same unbounded leak the second rule exists to close. It now
scans on the entry hash, which every append writes, and the age probe falls back
to the newest instant in that hash when the index is empty.

Enumerating a run's cycle keys used KEYS. That command iterates the whole
database and blocks while it does, and a hash tag routes a key without scoping
the scan, so a sweep pass would have issued one full keyspace scan per run. It
now reads the dense cycle high-water counter the append script maintains, which
is the same source the store's own terminal-expiry loop uses, and pipelines the
existence checks into one round trip.

The timestamp parity assertion was still tautological. The previous commit added
a note saying the builders receive an independent instant and did not change the
builder calls, which kept reading the value off the row under test. Every case
now mints one instant, passes it to the store, and gives the builder the same
value, so a write site that stops forwarding the caller's instant fails here.

Also documents what an injected fault actually does at each write path, since
only the birth path rethrows, and scopes a run count in the chaos suite to the
environment under test.

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e5fbb6a-d5a7-4274-a849-fb630afffe9c

📥 Commits

Reviewing files that changed from the base of the PR and between ea0e17b and 1d4eb7b.

📒 Files selected for processing (5)
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotFaultInjection.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal-packages/run-store/src/snapshotFaultInjection.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: All PR Checks
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Prefer static imports over dynamic imports. Only use dynamic import() when:

  • Circular dependencies cannot be resolved otherwise
  • Code splitting is genuinely needed for performance
  • The module must be loaded conditionally at runtime

Files:

  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

**/*.{test,spec}.{ts,tsx}: We use vitest exclusively. Never mock anything - use testcontainers instead.
Test files go next to source files (e.g., MyService.ts -> MyService.test.ts).

Files:

  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Add crumbs as you write code — not just when debugging. Mark lines with
// @Crumbs or wrap blocks in `// `#region` `@crumbs. They stay on the
branch throughout development and are stripped by agentcrumbs strip
before merge.
Do not invent new namespaces — pick from this table or ask first.
Do not flag // @Crumbs markers, `#region `@crumbs blocks, or agentcrumbs
imports in reviews. These are temporary debug instrumentation stripped
before merge. Data logged in crumbs (IDs, names, values) never reaches
production.

Files:

  • internal-packages/run-store/src/snapshotEntry.parity.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts
  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
internal-packages/run-engine/src/engine/tests/**/*.test.ts

📄 CodeRabbit inference engine (internal-packages/run-engine/CLAUDE.md)

Implement tests for RunEngine in src/engine/tests/ using testcontainers for Redis and PostgreSQL containerization

Files:

  • internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts
🔇 Additional comments (1)
internal-packages/run-engine/src/engine/tests/snapshotStoreChaos.test.ts (1)

189-191: LGTM!

Comment on lines +219 to +253
/**
* Every key for one run: the four core keys plus each wait-cycle key.
*
* The cycle keys are enumerated from the `c` high-water field on the seq hash, which the append
* script mints densely with HINCRBY, so 1..high covers every wp key that was ever written. This
* is the same source the store's own terminal-expiry loop uses.
*
* It deliberately does NOT use `KEYS`. That command iterates the whole database and blocks while
* it does, and a hash tag routes a key without scoping the scan, so one sweep pass over a batch
* would issue a full keyspace scan per run.
*
* The trade-off: if the seq hash is evicted while a wp key survives, `high` reads 0 and that
* orphaned cycle key is left behind. That is the right way to be wrong here. Leaving one small
* key costs bytes, where scanning the keyspace to find it costs every hot-path client latency on
* every pass.
*/
async #allKeys(runId: string): Promise<string[]> {
const core = snapshotKeys(runId);

const high = Number((await this.#redis.hget(core.seq, "c")) ?? "0");
const cycles: string[] = [];
for (let n = 1; n <= high; n++) {
cycles.push(`${this.#prefix}{${runId}}:wp:${n}`);
}

const candidates = [core.e, core.idx, core.cur, core.seq, ...cycles];

// One round trip for the whole set, rather than one per candidate.
const pipeline = this.#redis.pipeline();
for (const key of candidates) {
pipeline.exists(key);
}
const replies = await pipeline.exec();

return candidates.filter((_key, index) => replies?.[index]?.[1] === 1);

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add crumb markers to the changed blocks. The selected new code has no // @Crumbs marker or `#region `@crumbs block.

  • internal-packages/run-store/src/snapshotOrphanSweeper.ts#L219-L253: mark the wait-cycle discovery and key-existence logic.
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts#L62-L77: mark the timestamp fixture and snapshot helper.
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts#L313-L352: mark the all-invalid-keyspace integration test.

As per coding guidelines, “Add crumbs as you write code — not just when debugging. Mark lines with // @Crumbs or wrap blocks in `// `#region` `@crumbs.”

📍 Affects 3 files
  • internal-packages/run-store/src/snapshotOrphanSweeper.ts#L219-L253 (this comment)
  • internal-packages/run-store/src/snapshotEntry.parity.test.ts#L62-L77
  • internal-packages/run-store/src/snapshotOrphanSweeper.test.ts#L313-L352

Source: Coding guidelines

Comment on lines +235 to +241
async #allKeys(runId: string): Promise<string[]> {
const core = snapshotKeys(runId);

const high = Number((await this.#redis.hget(core.seq, "c")) ?? "0");
const cycles: string[] = [];
for (let n = 1; n <= high; n++) {
cycles.push(`${this.#prefix}{${runId}}:wp:${n}`);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor keyPrefix when deriving core snapshot keys.

When callers set keyPrefix, Line 236 calls snapshotKeys(runId), which always uses snap:. The scan at Line 118 finds the custom-prefixed entry hash, but #allKeys() cannot find its core keys or sequence counter. Both cleanup rules then skip that keyspace. Build core keys from this.#prefix in both #allKeys() and #newestEntryAgeMs(). Add a non-default-prefix test.

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.

1 participant