You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add an opt-in withStepQueues(flow) deployment mode that gives every step of one concrete flow its own private PGMQ queue and worker pool.
This is the first useful queue-routing stage. It preserves one typed DAG and one run while preventing a busy step from starving ready work for another step.
It deliberately excludes custom queue names, queue sharing, aliases, and multi-flow worker registries.
Derive stepSlug from keyof ExtractFlowSteps<TFlow> and require it only in the step-worker overload.
Do not add queue parameters to .step(), .array(), or .map(). Do not add a flow-slug generic or conditional string types for generated-name validation. Flow.slug is widened to string, and valid long step names may use the index fallback, so complete generated-name checks belong in synchronous runtime validation plus authoritative SQL.
Deployment metadata
Queue mode is deployment metadata, not DAG behavior:
ensure_flow_compiled() must receive the complete shape, queue mode, and ordered (step_slug, queue_name) route map under the same concrete-slug lock. For an existing concrete slug:
matching shape, mode, and route map verify;
a mode or route mismatch fails in production with a dedicated routing error;
a fenced local destructive recompilation deletes old runtime data and private queues before compiling the new mode and route map.
Startup must compare the persisted route map even when shape and mode match. This detects resolver changes, migration defects, and manual database edits before a worker polls the wrong queue.
Changing queue mode or the resolved route map in production requires a new concrete flow slug.
Canonical queue-name resolution
Startup SQL compilation is authoritative. TypeScript mirrors the same algorithm for immediate feedback.
readable length <= 47
-> readable
readable too long and fallback length <= 47
-> fallback
both too long
-> reject the complete flow
Do not truncate or hash names.
Examples:
Input
Result
communityThreadsV1.classify, index 0
communitythreadsv1__classify
short flow plus a very long step, index 3
<flow>__3
44-character flow, short step, index 10
use readable if it fits; otherwise reject
45-character flow, index 0
reject because even <flow>__0 exceeds 47
The compiler validates every actual index in the complete ordered shape. Do not impose a separate step-count limit.
TypeScript validation
withStepQueues(flow) must synchronously:
reject a flow with zero steps;
resolve every readable and fallback name using flow.stepOrder;
enforce the 47-character limit;
detect duplicate normalized names;
freeze or otherwise protect the checked route snapshot from stale mutation;
return branded checked deployment metadata;
throw a typed error before any worker or database call.
Use structured errors:
FlowQueueNameError
the flow slug cannot fit even the shortest actual index suffix
StepQueueNameError
one step's readable and actual index fallback names both exceed 47
Errors include flowSlug, stepSlug, stepIndex, both candidate names, their lengths, the maximum, and a concrete shortening hint.
SQL preflight and errors
Add one canonical SQL resolver over the complete ordered shape. Before any mutation it must:
resolve every step queue using ordinality as zero-based step_index;
enforce lowercase and the 47-character compatibility limit;
call the installed pgmq.validate_queue_name() for every distinct name;
detect duplicate generated names;
verify each name is absent or privately owned by the same concrete flow and step route;
fail before creating the flow, queues, or steps.
Use PostgreSQL MESSAGE, DETAIL, and HINT fields.
Flow-slug failure example:
MESSAGE: Flow "<slug>" cannot use per-step queues.
DETAIL: The shortest required queue "<slug>__0" is 48 characters; PGMQ allows at most 47.
HINT: Shorten the concrete flow slug or use the default single queue.
Step-specific failure example:
MESSAGE: Cannot derive a queue for step "deliverSlack" at index 10 in flow "<slug>".
DETAIL: The readable name is 58 characters and the index fallback is 48; PGMQ allows at most 47.
HINT: Shorten the concrete flow slug, shorten the step slug enough for the readable name, or use the default single queue.
Worker claiming and safety
After compilation, a step worker resolves its persisted queue by exact (flow_slug, step_slug) and registers against that queue.
Claiming must classify the complete batch against the exact subscription before any task mutation:
exact task + queued
-> eligible to claim
exact task + started
-> benign duplicate visibility; consume no attempt and hide until the next recovery boundary
exact task + terminal
-> archive idempotently; do not execute
no task, wrong queue, or unsupported flow-step route
-> unsupported work
If any message is unsupported:
claim none of the batch;
reset visibility for the complete read batch;
consume no task attempt;
emit one fatal error with queue, message IDs, flow, and step, but no message bodies;
persistently disable or pause the HTTP worker function before shutdown so ensure_workers() cannot create a restart loop;
request worker shutdown.
A mixed supported batch may atomically claim queued tasks, defer still-started tasks, and archive terminal tasks. Do not treat a visible message for a still-started task as corruption.
Worker coverage and rollout
Compilation creates every private step queue, but it does not wait for every step worker to register.
If a worker is absent, its tasks wait durably. Monitoring and startup logs must make missing queue coverage visible. Do not add an activation or cross-worker readiness protocol in this issue.
The visibility extension at claim and stalled recovery must use the same effective step timeout and tested buffer. A visible still-started message must never consume attempts or stop a healthy worker.
Production docs must use #654's fence around the complete affected worker-function set.
Versioning
Generated queues use the concrete slug, never a future alias:
Summary
Add an opt-in
withStepQueues(flow)deployment mode that gives every step of one concrete flow its own private PGMQ queue and worker pool.This is the first useful queue-routing stage. It preserves one typed DAG and one run while preventing a busy step from starving ready work for another step.
It deliberately excludes custom queue names, queue sharing, aliases, and multi-flow worker registries.
Dependencies
start_tasks()visibility extension reliable before this issue adds stricter claim classification.Public API
Start one worker per selected step:
Plain flows remain unchanged:
Rules:
Flowdoes not requirestepSlug;stepSlug;stepSlugautocompletes from the wrapped flow and rejects unknown values;Type contract
withStepQueues()returns a lightweightStepQueuedFlow<TFlow>wrapper with:It preserves:
Derive
stepSlugfromkeyof ExtractFlowSteps<TFlow>and require it only in the step-worker overload.Do not add queue parameters to
.step(),.array(), or.map(). Do not add a flow-slug generic or conditional string types for generated-name validation.Flow.slugis widened tostring, and valid long step names may use the index fallback, so complete generated-name checks belong in synchronous runtime validation plus authoritative SQL.Deployment metadata
Queue mode is deployment metadata, not DAG behavior:
Persist queue mode separately from
FlowShape.ensure_flow_compiled()must receive the complete shape, queue mode, and ordered(step_slug, queue_name)route map under the same concrete-slug lock. For an existing concrete slug:Startup must compare the persisted route map even when shape and mode match. This detects resolver changes, migration defects, and manual database edits before a worker polls the wrong queue.
Changing queue mode or the resolved route map in production requires a new concrete flow slug.
Canonical queue-name resolution
Startup SQL compilation is authoritative. TypeScript mirrors the same algorithm for immediate feedback.
Use the fixed compatibility limit:
Generated names are lowercase.
For each zero-based step index:
Resolution:
Do not truncate or hash names.
Examples:
communityThreadsV1.classify, index0communitythreadsv1__classify3<flow>__3100<flow>__0exceeds 47The compiler validates every actual index in the complete ordered shape. Do not impose a separate step-count limit.
TypeScript validation
withStepQueues(flow)must synchronously:flow.stepOrder;Use structured errors:
Errors include
flowSlug,stepSlug,stepIndex, both candidate names, their lengths, the maximum, and a concrete shortening hint.SQL preflight and errors
Add one canonical SQL resolver over the complete ordered shape. Before any mutation it must:
step_index;pgmq.validate_queue_name()for every distinct name;Use PostgreSQL
MESSAGE,DETAIL, andHINTfields.Flow-slug failure example:
Step-specific failure example:
Worker claiming and safety
After compilation, a step worker resolves its persisted queue by exact
(flow_slug, step_slug)and registers against that queue.Claiming must classify the complete batch against the exact subscription before any task mutation:
Classify each read message:
If any message is unsupported:
ensure_workers()cannot create a restart loop;A mixed supported batch may atomically claim queued tasks, defer still-started tasks, and archive terminal tasks. Do not treat a visible message for a still-started task as corruption.
Worker coverage and rollout
Compilation creates every private step queue, but it does not wait for every step worker to register.
If a worker is absent, its tasks wait durably. Monitoring and startup logs must make missing queue coverage visible. Do not add an activation or cross-worker readiness protocol in this issue.
The visibility extension at claim and stalled recovery must use the same effective step timeout and tested buffer. A visible still-started message must never consume attempts or stop a healthy worker.
Production docs must use #654's fence around the complete affected worker-function set.
Versioning
Generated queues use the concrete slug, never a future alias:
A new concrete version therefore cannot consume another version's tasks. Old workers remain until old runs drain.
Step order only affects an index fallback. Reordering steps changes
FlowShape, so production already requires a new concrete slug.Acceptance criteria
withStepQueues(flow)preserves the exact flow type and step-slug union through a protected checked-route wrapper.withStepQueues()rejects an empty flow synchronously.stepSlug.stepSlug.pgmq.validate_queue_name()and returns actionableMESSAGE,DETAIL, andHINTfields.Out of scope