Skip to content

feat(webapp): mollifier trigger-time decisions — mollify, claim, read fallback#3753

Draft
d-cs wants to merge 8 commits into
mollifier-phase-3-bufferfrom
mollifier-phase-3-trigger
Draft

feat(webapp): mollifier trigger-time decisions — mollify, claim, read fallback#3753
d-cs wants to merge 8 commits into
mollifier-phase-3-bufferfrom
mollifier-phase-3-trigger

Conversation

@d-cs
Copy link
Copy Markdown
Collaborator

@d-cs d-cs commented May 26, 2026

Summary

The trigger hot path's mollifier integration:

  • mollifyTrigger: when the gate trips, write the engine.trigger snapshot to the buffer and return a synthesised QUEUED response. Postgres write is deferred to drainer-replay (next PR in the stack).
  • Pre-gate idempotency-key claim: same-key triggers serialise through Redis so a burst lands in PG / buffer exactly once.
  • Read-fallback extensions: findRunByIdWithMollifierFallback for the trigger-time idempotency lookup that must see buffered runs.
  • Gate bypasses: debounce, oneTimeUseToken, parentTaskRunId/triggerAndWait skip the mollify path entirely.
  • triggerTask + IdempotencyKeyConcern wired to the above.

All behaviour gated by the master TRIGGER_MOLLIFIER_ENABLED switch; off-state hot path is unchanged (the gate is not even consulted).

Stacked on the buffer extensions PR.

Test plan

  • `pnpm run typecheck --filter webapp` passes
  • `pnpm run test --filter webapp test/mollifierMollify.test.ts` passes
  • `pnpm run test --filter webapp test/mollifierIdempotencyClaim.test.ts` passes
  • `pnpm run test --filter webapp test/mollifierReadFallback.test.ts` passes
  • `pnpm run test --filter webapp test/mollifierGate.test.ts` passes
  • `pnpm run test --filter webapp test/engine/triggerTask.test.ts` passes

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

⚠️ No Changeset found

Latest commit: 9de966d

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5eb4ce22-7a54-49c1-9f40-5a42ec7a4251

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mollifier-phase-3-trigger

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 and usage tips.

Comment thread apps/webapp/app/v3/mollifier/readFallback.server.ts
@d-cs d-cs self-assigned this May 26, 2026
@d-cs d-cs force-pushed the mollifier-phase-3-trigger branch 2 times, most recently from 5a7bc19 to baa6f17 Compare May 26, 2026 13:24
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread apps/webapp/app/runEngine/services/triggerTask.server.ts
Comment thread apps/webapp/app/runEngine/services/triggerTask.server.ts
@d-cs d-cs force-pushed the mollifier-phase-3-trigger branch from 01f3958 to 449a0bc Compare May 27, 2026 12:04
@d-cs d-cs force-pushed the mollifier-phase-3-buffer branch from e85e771 to 9298706 Compare May 27, 2026 12:15
@d-cs d-cs force-pushed the mollifier-phase-3-trigger branch from 449a0bc to ffe51b8 Compare May 27, 2026 12:15
@d-cs d-cs force-pushed the mollifier-phase-3-buffer branch from 9298706 to f4c5b21 Compare May 27, 2026 12:21
@d-cs d-cs force-pushed the mollifier-phase-3-trigger branch 5 times, most recently from e56b937 to cae33fa Compare May 27, 2026 15:33
d-cs and others added 7 commits May 27, 2026 17:37
… fallback

The trigger hot path's mollifier integration:
- `mollifyTrigger`: when the gate trips, write the engine.trigger
  snapshot to the buffer and return a synthesised QUEUED response.
- Pre-gate idempotency-key claim: same-key triggers serialise through
  Redis so a burst lands in PG / buffer exactly once.
- Read-fallback extensions: `findRunByIdWithMollifierFallback` for the
  trigger-time idempotency lookup that must see buffered runs.
- Gate bypasses: debounce, oneTimeUseToken, parentTaskRun
  (triggerAndWait) skip the mollify path entirely.
- triggerTask + IdempotencyKeyConcern wired to the above.

Stacked on buffer extensions PR. All behaviour gated by the master
`TRIGGER_MOLLIFIER_ENABLED` switch; off-state hot path is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`new Date("not-a-date")` returns a truthy Invalid Date object, which would
mis-classify the run as CANCELED in the read-fallback synthesised shape.
Add an `asDate` helper that rejects NaN-valued parses and use it for
`cancelledAt` and `delayUntil`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The buffer's claim API now requires a caller-supplied ownership token
so compare-and-act protects the slot against a stale predecessor.
Wires the token end-to-end:

- `claimOrAwait` generates the token (UUID) up front and reuses it
  across the retry path; returns it on the `claimed` outcome.
- `publishClaim` and `releaseClaim` wrappers accept and forward the
  token to the buffer.
- `ClaimedIdempotency` carries the token so the trigger pipeline can
  publish or release with the same token it claimed under.
- `triggerTask.server.ts` threads the token into the publish call.

Tests pin token round-trip: claimOrAwait → claimIdempotency, plus the
publish and release pass-through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t leak fix)

Devin review flagged a security + correctness bug: the mollify path
returned \`MollifySyntheticResult\` with run shape
\`{ friendlyId, spanId }\` and the call site cast it to
\`TriggerTaskServiceResult\` (which expects \`run: TaskRun\` with an
\`id: string\`). Downstream, the trigger route calls
\`saveRequestIdempotency(requestKey, "trigger", result.run.id)\` — so
the cache stored \`undefined\` as the entity id. On SDK retry the
request-idempotency flow then ran
\`prisma.taskRun.findFirst({ where: { id: undefined } })\`. Prisma
strips \`undefined\` from where clauses, so the query degenerates to an
unfiltered \`findFirst\` and returns an arbitrary TaskRun row —
potentially from another env / user.

Fix:
- Add \`id: string\` to \`MollifySyntheticResult.run\`.
- Compute it via \`RunId.fromFriendlyId(...)\` on both branches:
  the happy-accept path (id from \`args.runFriendlyId\`) and the
  \`duplicate_idempotency\` race-loser path (id from
  \`result.existingRunId\` so the response carries the WINNER's id).
- Add regression tests pinning both branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uard

- readFallback: read snapshot.taskVersion (the key buildEngineTriggerInput writes) instead of the nonexistent snapshot.lockToVersion, so buffered version-locked runs report their locked version; test now uses the real key as a regression guard.

- env: TRIGGER_MOLLIFIER_TRIP_THRESHOLD back to positive() (matching sibling mollifier numerics) to forbid threshold=0 silently mollifying every trigger.

- idempotencyKeys: document why the resolved-but-unfindable fall-through is safe (PG-unique + accept SETNX dedup + ~30s claim TTL self-heal); add regression test pinning the fall-through and the resolved-and-findable cached-hit path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove plan-tracking shorthand (Q#, C#, F#, Phase N, _plans/) from trigger-layer mollifier comments and test names; reword to plain English. Comment/test-name only; no behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@d-cs d-cs force-pushed the mollifier-phase-3-trigger branch from cae33fa to 16bfff0 Compare May 27, 2026 16:50
…f-state token alloc

triggerTask.server.ts: mollifier.buffered logs at debug, not info — it fires per mollified trigger during a burst, exactly when info-level would flood aggregation.

idempotencyClaim.server.ts: move the buffer-null fall-open check above token generation so the mollifier-OFF path skips the default randomUUID() for idempotency-keyed triggers (triggerTask is the highest-throughput path). Injected test generators still honoured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant