refactor(automations): run dispatch inline, drop the thread-gate hop#3919
Merged
Conversation
Automation fires went through two partitioned queues: the per-org `automations` queue (fan-out fairness) AND the `thread-gate` queue (per-thread serialization). But each fire creates a fresh thread, so the per-thread gate never did anything for automations — they routed through it only to reuse the dispatch-execution body, paying a second queue hop and an extra child workflow per fire. Extract that body as `runDispatchSteps` (track-started → dispatch → track-failed) and call it directly from `fireAutomationWorkflow`, which already holds the org's queue slot. `threadGateWorkflow` now just wraps the same body behind its per-thread slot for user messages. Result: the two queues have crisp, non-overlapping roles — `thread-gate` = user-message executor (serialized per thread); `automations` = org-capped automation executor. No shared hop, one fewer workflow per fire, both guarantees preserved (per-org fairness + per-thread ordering for users). The dispatch step keeps its exact config (`retriesAllowed: false`), so its recovery semantics are unchanged — it just lives in the fire workflow's journal now instead of a child. `awaitThreadRun` had no other callers and is removed.
decocms Bot
pushed a commit
that referenced
this pull request
Jun 15, 2026
PR: #3919 refactor(automations): run dispatch inline, drop the thread-gate hop Bump type: patch - decocms (apps/mesh/package.json): 3.18.11 -> 3.18.12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #3918 (now merged). Addresses the "why two dispatch queues, they feel like the same thing" smell.
Before, an automation fire passed through both partitioned queues:
But each fire creates a fresh thread (
taskIdfromcreateRunThreadStep), so the thread-gate's per-thread serialization is a no-op for automations — they routed through it only to reuse the dispatch body.This extracts that body as
runDispatchSteps(track-started → dispatch → track-failed) and calls it directly fromfireAutomationWorkflow, which already holds the org's queue slot.Result
The two queues now have crisp, non-overlapping roles:
thread-gate— user-message executor, serialized per thread (where ordering actually matters).automations— org-capped automation executor.No shared hop, one fewer workflow per fire, both guarantees preserved (per-org fairness + per-thread ordering for users).
Why not fully merge to one queue?
Considered it: you'd either lose per-org fairness, or merge interactive user traffic with background cron fan-out into one pool — letting a cron storm delay user chats. Two queues give that isolation; the only real redundancy was the hop, which this removes. (DBOS gives one partition key + one cap per queue, and these are two genuine axes: orgId fan-out vs threadId order.)
Safety
retriesAllowed: false); recovery semantics are unchanged — it just lives in the fire workflow's journal now instead of a separate child.awaitThreadRunhad no remaining callers and is removed;enqueueThreadRun(user POST) unchanged.source: "automation", so the shared body is correct for both callers.Testing
bun run --cwd apps/mesh check— clean.bun test apps/mesh/src/dispatch-queue apps/mesh/src/automations— 31 pass.bun run fmtapplied.Summary by cubic
Runs automation dispatch inline in
fireAutomationWorkflowand removes the extrathread-gatequeue hop. This drops one child workflow per fire while keeping per-org fairness and per-thread ordering for user messages.runDispatchSteps(track-started → dispatch → track-failed).fireAutomationWorkflownow callsrunDispatchStepsdirectly; automations skipthread-gate.threadGateWorkflowwraps the same body for user messages only.awaitThreadRun;enqueueThreadRunremains.retriesAllowed: false) and recovery semantics.Written for commit c32ab0b. Summary will update on new commits.