Skip to content

intent: a timed wait step - kind: wait, args: { after: PT10S, next: ... } - so an expected wait is a loop, not a retry (#8040) - #8045

Merged
iliyan-velichkov merged 1 commit into
masterfrom
issue-8040-timed-wait
Oct 22, 2026
Merged

iliyan-velichkov merged 1 commit into
masterfrom
issue-8040-timed-wait

Conversation

@iliyan-velichkov

Copy link
Copy Markdown
Contributor

Cause

A wait step could only park until an entity event (onCreate / onUpdate / onTransition); the DSL's only timers were a user task's boundary timeout: / expire:. A model waiting for a state it expects to change (a remote export, a released resource) therefore polled through retry:: the delegate threw "not yet" and retry: { count: 60, every: PT10S } paced the next check. Every expected "not yet" became a failed job - an ERROR with a stack trace, a failure count, an exhausted budget that reads as a technical failure - and a genuinely failing check had no retries of its own left.

Change

A wait may declare after: <ISO-8601 duration> instead of an entity event:

- { name: checkExport, kind: serviceTask, args: { delegate: custom.CheckExportTask, produces: [exportState], retry: { count: 3, every: PT30S }, onError: recordFailure, next: exportDone } }
- { name: exportDone,  kind: decision,    args: { if: "exportState == 'DONE'", then: publish, else: pause } }
- { name: pause,       kind: wait,        args: { after: PT10S, next: checkExport } }
  • Parser (IntentParser): after joins the wait's args. It must be a positive ISO-8601 duration (PT0S, PT-10S, 10s are refused). It excludes onCreate/onUpdate/onTransition, via and when - each refusal names the step. A timed wait needs no trigger entity. A wait with neither form gets the existing message, extended to name after: (the existing substring is kept, so no existing test changed).
  • BPMN (BpmnIntentGenerator): a timed wait renders as <intermediateCatchEvent><timerEventDefinition><timeDuration>PT10S</timeDuration></timerEventDefinition></intermediateCatchEvent>; the definitions-level <message> loop skips it.
  • Glue (ProcessWaitSupport.isTimed): waits() generates no wait listener for it; eventKind() reads it as not an event wait.
  • Event waits generate byte-identically (their code path is unchanged; TimedWaitBpmnTest pins the message definition and catch event of an event wait beside a timed one).
  • intent-assistant-guide.md: the timed form and the polling loop, and that retry: is for unexpected failures, not an expected wait. .claude/docs/intent-dsl-features.md: a construct paragraph.

Verification

  • engine-intent unit suite: 1775 tests, green, including:
    • WaitTimerIntentTest: aTimedWaitParses (the issue's YAML), invalid / zero / negative duration, a period (P1D), after + event, after + via / when, neither form, no trigger entity needed;
    • GlueWaitsTest.aTimedWaitGeneratesNoListener (and the event wait beside it unchanged);
    • TimedWaitBpmnTest: the timer catch event, no <message>, the loop exportDone -> pause -> checkExport wired, an event wait's message unchanged;
    • TimedWaitEngineTest: the generated BPMN on a live in-memory Flowable engine (async executor on) - the check answers RUNNING twice then DONE; the instance's active activity is pause while parked on one timer job; it completes through publish after 3 checks and 2 pauses, with no dead-letter job and no ERROR logged.
  • IntentEngineIT (83) and IntentEmissionCoverageIT green. The latter now generates, compiles and publishes a RenditionPoll process with a timed wait (after: PT1S) and a custom checker. It asserts the timer catch event and the absence of a wait listener, then that the record reaches DONE with the instance's history showing exactly 3 checks.
  • mvn -T 1C formatter:validate with the formatter cache wiped; release-profile javadoc on engine-intent.

Not verified: PostgreSQL leg locally (the change emits no SQL); the editor diagram renders a timed wait with the same wait shape (no change made there).

Out of scope per the issue: durations from config/fields, a loop-budget construct, boundary timers on service tasks, docs-site and specification text (proposal: IntentFile/intent-specification#136).

Fixes #8040

🤖 Generated with Claude Code

….. } - so an expected wait is a loop, not a retry (#8040)

A wait could only park until an entity event, so a model waiting for a
state it expects to change (a remote export, a released resource) polled
through retry: - the delegate threw "not yet", and every check became a
failed job with an ERROR and a stack trace, spending the retry budget a
real failure of the check should have had.

A wait may now declare `after: <ISO-8601 duration>` instead of an entity
event. It renders as a timer intermediate catch event with no <message>
and no wait listener, and needs no trigger entity: a check produces the
state, a decision routes on it, the timed wait loops back.

- IntentParser: `after` is a wait arg; it must be a positive ISO-8601
  duration and excludes onCreate/onUpdate/onTransition, via and when,
  each refusal naming the step. A wait with neither form names both.
- ProcessWaitSupport.isTimed: waits() generates no listener for it and
  eventKind() reads it as not an event wait.
- BpmnIntentGenerator: the timer catch event; the definitions-level
  message loop skips it. Event waits generate byte-identically.
- intent-assistant-guide: the timed form, the polling loop, and that
  retry: is for unexpected failures.

Verified: engine-intent unit suite (1775) green, incl. the new
WaitTimerIntentTest cases, GlueWaitsTest, TimedWaitBpmnTest and
TimedWaitEngineTest (the generated loop on a live Flowable engine:
parks at the wait, polls twice, completes, no dead letter, no ERROR);
IntentEngineIT and IntentEmissionCoverageIT green, the latter now
publishing a timed-wait poll and asserting it completes after three
checks; formatter:validate with the cache wiped; release javadoc on
engine-intent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iliyan-velichkov
iliyan-velichkov merged commit 2112b53 into master Oct 22, 2026
16 checks passed
@iliyan-velichkov
iliyan-velichkov deleted the issue-8040-timed-wait branch October 22, 2026 12:22
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.

intent: a timed wait step - kind: wait, args: { after: PT10S, next: … } - so an expected wait is a loop, not a retry

1 participant