Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneeszack

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

  1. Let a wait_for_signal step declare a key held by one open wait at a time (this issue)
  2. swamp-club#3210: Signal a wait by workflow and key
  3. swamp-club#3211: Deliver a signal by key through swamp serve
  4. swamp-club#3212: Hold a signal that arrives before its wait opens
  5. swamp-club#3213: Accept early signals through swamp serve
  6. 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] }
  • key is an optional field of the wait_for_signal task (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 under forEach, 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 putIfAbsent and 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 (outcomeAt in src/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), so allowFailure and failed dependents 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 waits listing 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

  1. 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.
  2. 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.
  3. 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 failed dependent 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 --json shows 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 decideSignal is for a payload.
  • Integration tests in integration/workflow_signal_wait_test.ts with 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.

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 18 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/9/2026, 4:59:59 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/9/2026, 3:12:44 PM

Sign in to post a ripple.