Skip to content

fix(sltb): stop schedule-pickup stalling when the trader sends no response - #38

Open
Aravinda-HWK wants to merge 1 commit into
mainfrom
fix/sltb-schedule-pickup-gateway-stall
Open

fix(sltb): stop schedule-pickup stalling when the trader sends no response#38
Aravinda-HWK wants to merge 1 commit into
mainfrom
fix/sltb-schedule-pickup-gateway-stall

Conversation

@Aravinda-HWK

Copy link
Copy Markdown
Collaborator

The bug

Observed in nsw-staging on task sltb_3b_schedule_pickup:94650116-e7a4-40bb-9fdb-9c6ac2e56231 (consignment "SLTB Sampaling Date Change Option"). The trader's submit appeared to fail, and every retry returned:

level=ERROR msg="tasks: failed to complete task step"
  error="failed to resume task workflow: cannot find pending activity with
         ActivityID trader_suggest:42aba8c3-4544-4235-b963-5bead7a349ff"

That error is a symptom. The first submit actually succeeded and completed the activity — then the workflow stalled and never scheduled anything else:

 8 ActivityTaskCompleted   03:01:30   {"__command":"submit","suggested_date":"2026-08-28","suggested_time":"10.00 a.m"}
 9 WorkflowTaskScheduled   03:01:30
10 WorkflowTaskStarted     03:01:30
11 WorkflowTaskCompleted   03:01:30   <- no commands emitted

Status Running, Pending Activities 0, forever. All later submits hit the already-consumed activity, which is where the misleading error comes from.

Root cause

Note the payload has no response key.

On the first pass through trader_suggest the response control is hidden — the uiSchema only shows it once officer_outcome == 'propose_alternative', i.e. after an officer review. So a first-pass submit legitimately carries no response. Both response? and trader_notes? are optional output mappings, so when neither is present the suggest namespace is never created.

Both edges out of trader_response_gateway then referenced it:

e_trader_proposes   schedule.schedule_outcome != 'propose_alternative' || suggest.response != 'accept'
e_trader_accepts    schedule.schedule_outcome == 'propose_alternative' && suggest.response == 'accept'

EvaluateCondition compiles with expr.Compile(cond, expr.Env(WorkflowVariables), expr.AsBool()). expr rejects an unknown root name at compile time, before && / || can short-circuit:

unknown name suggest (1:55)
 | schedule.schedule_outcome != 'propose_alternative' || suggest.response != 'accept'
 | ......................................................^

handleGatewayNode does return err on that, so the gateway can neither route nor recover.

A missing key on a map that does exist is fine — it evaluates to nil. Only the absent root name is fatal. That is why the flow works whenever the form happens to send response (JSONForms materialising the "default": "propose") and breaks when it doesn't. Same artifact version, same deployment — purely payload-dependent, which is what made it look intermittent.

The fix

Restructure the gateway so no condition can reference a possibly-absent namespace, and so it always has an exit:

order edge condition target
1 e_trader_first_pass schedule.schedule_outcome != 'propose_alternative' officer_review
2 e_trader_accepts suggest.response == 'accept' end
3 e_trader_proposes (none — default passthrough) officer_review

Why this is safe:

  • schedule always exists at this gateway. The form requires suggested_date + suggested_time whenever response is absent or 'propose' (both map into schedule.*), and officer_review writes schedule_outcome via a non-optional mapping.
  • Edge 1 short-circuits the first pass using only schedule, so suggest is never compiled against a first-pass context. schedule_outcome being an absent key is fine — nil != 'propose_alternative' -> true.
  • Edge 2 is only reached on the second pass, where the form requires response (allOf -> if officer_outcome == 'propose_alternative' then required: [response]), so suggest exists.
  • Edge 3 is unconditional, so EXCLUSIVE_SPLIT can no longer hit no matching conditions found at exclusive gateway. Belt and braces: even a second-pass payload that somehow omits response now routes back to officer_review instead of hanging.

Behaviour for every valid path is unchanged — this only removes the stall.

version bumped 2 -> 3.

Verification

Driven by the patched workflow.json itself, evaluated through the real EvaluateCondition from go-temporal-workflow, across all five reachable states:

ok   first pass, response ABSENT (the bug)  -> e_trader_first_pass -> officer_review
ok   first pass, response=propose           -> e_trader_first_pass -> officer_review
ok   second pass, accept                    -> e_trader_accepts -> end
ok   second pass, propose                   -> e_trader_proposes -> officer_review
ok   second pass, response ABSENT           -> e_trader_proposes -> officer_review

Against the pre-fix edges, case 1 gives ERROR: unknown name suggest — reproducing the stall exactly.

Not fixed here — worth separate follow-ups

  1. schedule_gateway has the same shape. Its two edges (== 'approve', == 'propose_alternative') exactly cover the officer form's oneOf, and schedule_outcome is required with a non-optional mapping, so it is safe today — but it has no fallback edge, so adding a third outcome would stall the same way. Left alone to keep this PR to the actual defect; happy to harden it if you'd prefer.
  2. The engine should fail loudly. A gateway that can't route leaves the workflow Running with no pending activity and no error surfaced to the user. Worth an issue on go-temporal-workflow — it affects every flow, not just this one.
  3. Stranded tasks need manual recovery. They can't be resumed by re-submitting; the activity is gone. In nsw-staging: 94650116-..., plus 6f32b868-... (2026-07-07) and c979d3b8-... (2026-07-31).
  4. audit_logs is empty (0 rows) in nsw_staging and the app logs carry no actor field, so submits had to be attributed via consignment ownership. A real gap for triage.

🤖 Generated with Claude Code

…ponse

The `trader_response_gateway` edges both referenced `suggest.response`:

    e_trader_proposes  schedule.schedule_outcome != 'propose_alternative' || suggest.response != 'accept'
    e_trader_accepts   schedule.schedule_outcome == 'propose_alternative' && suggest.response == 'accept'

On the first pass through `trader_suggest` the `response` control is hidden
(it only shows once the officer has proposed an alternative), so the submit
payload can arrive without it. `response?` and `trader_notes?` are both
optional mappings, so when neither is present the `suggest` namespace is
never created at all.

The engine compiles each condition with `expr.Env(WorkflowVariables)`, which
rejects an unknown *root* name outright — `unknown name suggest` — before the
`&&`/`||` can short-circuit. `handleGatewayNode` returns that error, and the
workflow is left Running with no pending activity and no way forward. Every
later submit then fails with "cannot find pending activity", because the
activity was already consumed by the submit that stalled it.

Restructure the gateway so no condition can reference a namespace that may be
absent, and so it always has somewhere to go:

    e_trader_first_pass  schedule.schedule_outcome != 'propose_alternative'  -> officer_review
    e_trader_accepts     suggest.response == 'accept'                        -> end
    e_trader_proposes    (no condition — default passthrough)                -> officer_review

`schedule` always exists at this gateway (the form requires suggested_date /
suggested_time whenever response is absent or 'propose', and officer_review
writes schedule_outcome unconditionally), and a missing *key* on an existing
map evaluates to nil rather than erroring. `suggest.response` is now only
reached on the second pass, where the form requires it. The unconditional
third edge means an exclusive gateway can no longer stall here.

Verified against the real evaluator (go-temporal-workflow) across all five
reachable states: first pass with and without `response`, and second pass with
`accept`, `propose`, and `response` missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a1d2f57-e70a-4b4d-ac7a-453b037a709c


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Aravinda-HWK Aravinda-HWK self-assigned this Aug 27, 2026

@ginaxu1 ginaxu1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

@Aravinda-HWK

Copy link
Copy Markdown
Collaborator Author

Lgtm

Shall we keep this without merging? Because the previous logic was also right. Once we get a response to this issue https://github.com/LSFLK/lsf-govtech-tnsw/issues/36, we can either close or merge this PR.

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.

2 participants