Relationships
#3209 Let a wait_for_signal step declare a key held by one open wait at a time
Opened by zack · 10/9/2026· Shipped 10/9/2026
Problem
A signal names a wait ID and nothing else (the Signalling paragraphs of the Wait for Signal section in design/primitives/workflows.md). The ID is a random UUID issued when the step starts waiting (SignalWait.open in src/domain/workflows/signal_wait.ts), so anything that wants to signal must first learn the ID of each run's wait: from the workflow run output, from workflow waits (which needs read), or from a relay. A device or service that is configured once with a fixed address cannot signal at all.
This is part 1 of 6 of keyed signal waits: a declared key as a second way to address a wait. This issue adds the key and the rule that keeps a key unambiguous. Nothing can be signalled by key until part 2.
Series
- Let a wait_for_signal step declare a key held by one open wait at a time (this issue)
- swamp-club#3210: Signal a wait by workflow and key
- swamp-club#3211: Deliver a signal by key through swamp serve
- swamp-club#3212: Hold a signal that arrives before its wait opens
- swamp-club#3213: Accept early signals through swamp serve
- swamp-club#3214: Show wait keys and pending signals to remote clients and the dashboard
Proposal
- name: identify
allowFailure: true
task:
type: wait_for_signal
key: kitchen-verdict
timeout: 30
schema:
type: object
required: [verdict]
properties:
verdict: { type: string, enum: [known, intruder] }keyis an optional field of thewait_for_signaltask (src/domain/workflows/step_task.ts): a literal string from a restricted character set with a bounded length. It takes no expressions.workflow validate(src/domain/workflows/validation_service.ts) refuses the same key on two steps of one workflow, and a key on a step underforEach, where every iteration would share it. The workflow definition is therefore the list of its valid keys.- One key, one open wait. A step claims its key before it registers its wait. Claims are numbered records in the control-plane store, created with
putIfAbsentand never deleted to be replaced, following the generation pattern of continuation claims (src/domain/workflows/continuation_claim.ts). The holder of a key is the wait of its highest claim for as long as that wait has no outcome. A claimant settles an overdue holder as timed out first, as every reader of a wait does (outcomeAtinsrc/domain/workflows/signal_wait_cleanup.ts). - A step that reaches its wait while another open wait holds the key fails with its own error (proposed:
wait_key_held), soallowFailureandfaileddependents apply. No wait is opened and nothing is registered for it. - The key identifies the wait currently open under that name, not one attempt. A step that is reset closes its wait as today, and its next attempt claims the key again as a new claim with a new wait ID.
- The registration records the key (
src/domain/workflows/signal_wait_records.ts).SignalWaitStore(src/domain/workflows/signal_wait_store.ts,src/infrastructure/persistence/control_plane_signal_wait_store.ts) gains the claim operations. The S3 and GCS datastore extensions need no change: claims use the store operations wait records already use. - Claim records are removed wherever wait records are: when the run ends or is deleted, by
run gc, and by the wait-record sweep. - The local
swamp workflow waitslisting carries each wait's key in log and JSON output, so a claim can be observed before part 2 exists. Remote clients and the dashboard are part 6. - New terms for the design doc: a wait's key, and the claim that says which wait holds it.
Open decisions
- A second wait on a held key: fail the step (proposed), or replace the holder so the newest run holds the key. Replacing suits "the newest event wins", but lets one run end another run's wait.
- Literal keys only (proposed), or keys that may read
inputs.*. Input-derived keys would let runs overlap under distinct keys, but then the definition no longer lists the valid keys, which parts 2 to 5 rely on. - How an older build is kept from opening a keyed wait without a claim. It ignores a field it does not know. Either an upgrade rule in the docs (proposed, as for the other wait features) or a guard that makes an older build refuse the workflow.
Acceptance criteria
- A workflow whose wait declares a key validates. A duplicate key in one workflow, or a key under
forEach, fails validation with a message naming the steps. - Of two runs of the workflow, the first holds the key. The second's step fails with the held-key error, its
faileddependent runs, and the first run's wait is unaffected. - Once the holder is settled, whether signalled, timed out or cancelled, the next run's wait claims the key.
- Two steps that claim a free key at the same moment: exactly one opens a wait.
- Deleting a run removes its claim records.
swamp workflow waits --jsonshows the key.- A wait without a key behaves exactly as today.
Tests
- Unit tests beside each changed file.
- A property test for the holder decision as a pure function over claims and outcomes, as
decideSignalis for a payload. - Integration tests in
integration/workflow_signal_wait_test.tswith a fake clock and ordered creates, never sleeps: a held key, release by each kind of outcome, racing claims, cleanup with the run.
Depends on
Nothing. First of the series.
References
swamp-club#3068, swamp-club#3093, swamp-club#3108 (the generation pattern for claims), the Wait for Signal section in design/primitives/workflows.md.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.