fix: start the agent again when a task is sent back a workflow step (#3852)
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
---
|
||||
created: 2026-09-21
|
||||
status: implemented
|
||||
requirements:
|
||||
- REQ-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001
|
||||
system_design:
|
||||
- ../../specs/tasks/system-design/deferred-move-onenter-dispatch-identity.md
|
||||
legacy_specs:
|
||||
- ../../specs/workflow-on-enter-action-dispatch/spec.md
|
||||
- ../../specs/tasks/system-design/workflow-task-step-transition-ledger.md
|
||||
---
|
||||
|
||||
# Implementation Plan: Deferred move on_enter dispatch identity
|
||||
|
||||
## Outcome
|
||||
|
||||
A deferred `move_task_kandev` (E2, applied at turn end via `applyPendingMove` ->
|
||||
`processStepExitAndEnterForDeferredMove`) currently commits the workflow-step
|
||||
transition and then has its own `on_enter` dispatch rejected as stale, so
|
||||
`auto_start_agent` (and any other on_enter action) never runs. This fix threads
|
||||
the transition ledger ID that the deferred-move commit already produces into
|
||||
`processOnEnter`, so the existing "may replace a stale route" escape hatch
|
||||
(`workflowEntryDispatchMayReplaceRoute` in `ceiling_entry.go`, added for #3755)
|
||||
recognises this callback as the current, legitimate entry instead of falling
|
||||
through to the route-mismatch rejection.
|
||||
|
||||
## Root cause
|
||||
|
||||
`processStepExitAndEnterForDeferredMove` (`apps/backend/internal/orchestrator/event_handlers_workflow.go:5086`)
|
||||
calls `s.processOnEnter(ctx, identity.TaskID, fresh, entryStep, taskDescription, 0, fromStep)`
|
||||
with a hardcoded `transitionID` of `0`. `processOnEnter`'s staleness guard
|
||||
(`workflowEntryDispatchIsCurrentForSession` -> `workflowEntryDispatchRouteIsCurrent`
|
||||
in `ceiling_entry.go:303`) checks `workflowEntryDispatchMayReplaceRoute` first,
|
||||
which requires `entryIDs[0] > 0` to match the callback against
|
||||
`task.WorkflowStepTransitionID` via `workflowEntryDispatchMatchesCurrentTaskEntry`
|
||||
(`ceiling_entry.go:367`). With `0`, that check short-circuits to `false`
|
||||
(`ceiling_entry.go:373`: `len(entryIDs) == 0 || entryIDs[0] <= 0`), so the guard
|
||||
falls through to comparing the task's stored `workflow_session_route` against
|
||||
the destination step. That route is only updated to name the destination
|
||||
*inside* `maybySwitchSessionForProfile`, which runs **after** the staleness
|
||||
guard (PR #3826 moved route preparation there). The route therefore still
|
||||
names the source step, the comparison fails, and `processOnEnter` returns
|
||||
before dispatching `on_enter`.
|
||||
|
||||
The transition ledger ID this callback needs already exists: the deferred-move
|
||||
commit path (`workflowStore.applyTransition`, called via
|
||||
`ApplyDeferredMoveTransition` or the unfenced `applyTransition` fallback) writes
|
||||
it via `updateTaskTx` (`apps/backend/internal/task/repository/sqlite/task.go:1258`,
|
||||
`task.WorkflowStepTransitionID = transitionID`) into the same `*models.Task`
|
||||
value `applyTransition` holds locally — but neither `ApplyTransition` nor
|
||||
`ApplyDeferredMoveTransition` return it, so it is discarded before reaching
|
||||
`applyPendingMove` and the goroutine it spawns.
|
||||
|
||||
The forward-move path (`applyEngineTransitionWithCommitMode`,
|
||||
`event_handlers_workflow.go:7666`) already solves this correctly for `E1`,
|
||||
using the existing `stepentry.AllocationResult` / `stepentry.WithResultHolder`
|
||||
context out-parameter to capture `stepEntry.TransitionID` and thread it into
|
||||
`launchProcessOnEnter`. That mechanism is deliberately **not** reused here: it
|
||||
also causes `transitionContext` to attach a `stepentry.PendingAllocation` for
|
||||
ledger-owned marker-bearing on_enter actions
|
||||
(`workflow_store.go:460`), which is unrelated behavior this fix must not
|
||||
introduce as a side effect on the deferred-move path. Instead, `applyTransition`
|
||||
is changed to also return the `int64` transition ID it already computes, and
|
||||
that value is threaded explicitly through the two existing call sites down to
|
||||
`processOnEnter`.
|
||||
|
||||
## Requirement and system-design mapping
|
||||
|
||||
- **AC-A1**, **AC-A2** (`docs/specs/workflow-on-enter-action-dispatch/spec.md`):
|
||||
every declared `on_enter` action must execute, in order, exactly once per
|
||||
step entry, on every entry path including `E2` (manual move, which includes
|
||||
both immediate and deferred `move_task_kandev`). A backward deferred move
|
||||
into a step declaring `auto_start_agent` currently executes none of its
|
||||
`on_enter` actions, violating both criteria for that entry path.
|
||||
- **Ledger identity contract** (`docs/specs/tasks/system-design/workflow-task-step-transition-ledger.md`):
|
||||
`task.WorkflowStepTransitionID` is the immutable per-write ledger identity
|
||||
the repository layer populates after every step-changing write, including
|
||||
the `mcp_deferred_move` trigger. This fix is purely a consumer-side gap: the
|
||||
identity is already produced correctly and durably; it was never threaded to
|
||||
the one caller that needs it to identify its own dispatch as current.
|
||||
|
||||
## Constraints
|
||||
|
||||
- `event_handlers_workflow.go` / `workflow_session_target.go` / `ceiling_entry.go`
|
||||
are under heavy concurrent upstream churn (8 commits by carlosflorencio,
|
||||
2026-09-10 to 2026-09-20). Keep the diff to call-site plumbing only.
|
||||
- Do not weaken, bypass, or delete `workflowEntryDispatchIsCurrentForSession` or
|
||||
`workflowEntryDispatchMayReplaceRoute`. They correctly reject a genuinely
|
||||
stale callback (AC-TASKS-QUEUED-SESSION-OWNERSHIP-002.6-style protection);
|
||||
the defect is that a legitimate entry had no way to identify itself.
|
||||
- `engine.TransitionStore.ApplyTransition` is a broad interface implemented by
|
||||
several test fakes across `internal/workflow/engine`. Its public signature
|
||||
(`error` only) is unchanged. Only `workflowStore.ApplyDeferredMoveTransition`
|
||||
— which has exactly one production call site and no direct test fakes — gains
|
||||
an `int64` return.
|
||||
|
||||
## Architecture
|
||||
|
||||
`workflowStore.applyTransition` (private, `workflow_store.go:344`) already
|
||||
leaves the transition ID on its local `task.WorkflowStepTransitionID` after a
|
||||
successful `updateDeferredTransitionTask`/`updateTransitionTask` call. Change
|
||||
its signature to `(int64, error)`, returning `task.WorkflowStepTransitionID` on
|
||||
success and `0` alongside every existing error return.
|
||||
|
||||
- `workflowStore.ApplyTransition` (public, part of `engine.TransitionStore`):
|
||||
keep its `error`-only signature; discard the new first return value.
|
||||
- `workflowStore.ApplyDeferredMoveTransition` (public, one call site): change
|
||||
to return `(int64, error)`.
|
||||
- `Service.applyPendingMove` (`event_handlers_workflow.go:4938`): capture the
|
||||
transition ID from both the atomic (`ApplyDeferredMoveTransition`) and
|
||||
unfenced-fallback (`applyTransition`) branches.
|
||||
- `Service.processStepExitAndEnterForDeferredMove` (`event_handlers_workflow.go:5050`):
|
||||
add a `transitionID int64` parameter, pass it into the `processOnEnter` call
|
||||
at line 5086 instead of the hardcoded `0`.
|
||||
- `applyPendingMove`'s call to `processStepExitAndEnterForDeferredMove`
|
||||
(`event_handlers_workflow.go:5005`): pass the captured transition ID through.
|
||||
|
||||
`processStepExitForDeferredMove` (the WIP-queued branch, which does not call
|
||||
`processOnEnter`) is unchanged — its later promotion re-enters through the
|
||||
ledger dispatcher (`DispatcherLedger`), which already has its own dispatch
|
||||
identity from the promotion write, not from this call.
|
||||
|
||||
## Verification strategy
|
||||
|
||||
A regression test at the `orchestrator` package level drives a deferred
|
||||
`move_task_kandev` backward transition (source step B -> destination step A,
|
||||
where A declares `on_enter: auto_start_agent`) end to end through
|
||||
`applyPendingMove`, and asserts the auto-start dispatch actually ran (not just
|
||||
that `tasks.workflow_step_id` changed) — matching the existing shape of
|
||||
`TestApplyTransitionRecordsEngineTransitionFromSessionID` and neighboring tests
|
||||
in `workflow_step_ledger_test.go`, but exercising the deferred-move goroutine
|
||||
path (`processStepExitAndEnterForDeferredMove`) specifically, since the direct
|
||||
`ApplyTransition`/`applyTransition` ledger tests do not reach `processOnEnter`
|
||||
at all.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [task-01-thread-deferred-move-transition-id.md](task-01-thread-deferred-move-transition-id.md)
|
||||
|
||||
## Risks
|
||||
|
||||
- Low blast radius: `ApplyDeferredMoveTransition` has one production caller and
|
||||
no test fakes to update. `applyTransition` is private with two callers
|
||||
besides the public wrappers, both in this same file.
|
||||
- The unfenced-fallback branch (`SupportsAtomicDeferredMoveTransition() ==
|
||||
false`) is exercised by the in-memory message queue backend used in tests;
|
||||
the regression test should cover the path actually reachable in the test
|
||||
environment and note which backend it exercises.
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
---
|
||||
id: "01-thread-deferred-move-transition-id"
|
||||
title: "Thread the deferred-move transition ID into processOnEnter"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001
|
||||
acceptance_criteria:
|
||||
- AC-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001.1
|
||||
- AC-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001.2
|
||||
system_design:
|
||||
- ../../specs/tasks/system-design/deferred-move-onenter-dispatch-identity.md
|
||||
---
|
||||
|
||||
# Work Order 01: Thread the deferred-move transition ID into processOnEnter
|
||||
|
||||
## Outcome
|
||||
|
||||
`processStepExitAndEnterForDeferredMove` stops passing a hardcoded `0` as the
|
||||
transition/entry ID to `processOnEnter`. It receives and forwards the real
|
||||
ledger transition ID that `applyPendingMove` already produces when it commits
|
||||
the deferred move, so the staleness guard's existing "may replace a stale
|
||||
route" escape hatch recognises the callback as current and `on_enter` actually
|
||||
dispatches `on_enter` actions (in particular `auto_start_agent`) for a
|
||||
backward (or any) deferred `move_task_kandev`.
|
||||
|
||||
## In scope
|
||||
|
||||
- `apps/backend/internal/orchestrator/workflow_store.go`:
|
||||
- `applyTransition` (private): change return type from `error` to
|
||||
`(int64, error)`. Return `0` alongside every existing error return; on
|
||||
success return `task.WorkflowStepTransitionID` (populated by
|
||||
`updateDeferredTransitionTask`/`updateTransitionTask` via `updateTaskTx`).
|
||||
- `ApplyTransition` (public, part of `engine.TransitionStore`): keep its
|
||||
`error`-only signature; discard the new first return value from
|
||||
`applyTransition`.
|
||||
- `ApplyDeferredMoveTransition` (public): change return type to
|
||||
`(int64, error)`, returning `applyTransition`'s two values directly.
|
||||
- `apps/backend/internal/orchestrator/event_handlers_workflow.go`:
|
||||
- `applyPendingMove`: capture the transition ID from both branches — the
|
||||
atomic `ApplyDeferredMoveTransition` call and the unfenced-fallback
|
||||
`applyTransition` call — into a local `transitionID int64`.
|
||||
- Thread `transitionID` through the existing call to
|
||||
`processStepExitAndEnterForDeferredMove` (replacing no existing argument;
|
||||
add it as a new trailing parameter).
|
||||
- `processStepExitAndEnterForDeferredMove`: add a `transitionID int64`
|
||||
parameter; pass it (instead of the current hardcoded `0`) as the
|
||||
`transitionID` argument to the `s.processOnEnter(...)` call at the end of
|
||||
the function.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `processStepExitForDeferredMove` (the WIP-queued branch) — it never calls
|
||||
`processOnEnter`; no change needed.
|
||||
- Any change to `workflowEntryDispatchIsCurrentForSession`,
|
||||
`workflowEntryDispatchMayReplaceRoute`, or any other staleness-guard logic
|
||||
in `ceiling_entry.go`.
|
||||
- Reusing the `stepentry.AllocationResult` / `WithResultHolder` context
|
||||
mechanism the forward-move (`E1`) path uses — it has the side effect of also
|
||||
attaching a `stepentry.PendingAllocation` for ledger-owned marker-bearing
|
||||
on_enter actions via `transitionContext`, which is unrelated behavior this
|
||||
fix must not introduce on the deferred-move path.
|
||||
- Any change to `engine.TransitionStore`'s interface signature or its test
|
||||
fakes in `internal/workflow/engine/*_test.go`.
|
||||
|
||||
## Applicable requirements
|
||||
|
||||
- **AC-A1**, **AC-A2** — `docs/specs/workflow-on-enter-action-dispatch/spec.md`
|
||||
- **Ledger identity contract** — `docs/specs/tasks/system-design/workflow-task-step-transition-ledger.md`
|
||||
|
||||
## Acceptance conditions
|
||||
|
||||
1. A deferred `move_task_kandev` into a step whose `on_enter` declares
|
||||
`auto_start_agent` actually dispatches that action (asserted via a real
|
||||
launch/start-agent call, not just `tasks.workflow_step_id`), for both a
|
||||
forward and a backward transition.
|
||||
2. `workflowEntryDispatchIsCurrentForSession` still rejects a callback for a
|
||||
transition that is not the task's current one (no regression to the #3755
|
||||
staleness protection) — covered by the existing ledger/staleness tests in
|
||||
`workflow_step_ledger_test.go` and `ceiling_entry_test.go` (or equivalent),
|
||||
which must continue to pass unmodified.
|
||||
3. `golangci-lint` and the package build are clean for the changed files.
|
||||
|
||||
## Regression test
|
||||
|
||||
Add a test in `apps/backend/internal/orchestrator/` (new file or alongside
|
||||
`event_handlers_pending_move_test.go`) that reuses `buildPendingMoveScenario`'s
|
||||
shape: two steps each declaring `on_enter: auto_start_agent`, drive
|
||||
`svc.applyPendingMove` for a **backward** move (mirroring the production
|
||||
reproducer — Review bounced back to an earlier step), let the spawned
|
||||
`processStepExitAndEnterForDeferredMove` goroutine settle, and assert the
|
||||
destination step's agent was actually started — e.g. via `startAgentProcessFunc`
|
||||
/ `launchAgentFunc` invocation tracking on `mockAgentManager`, following the
|
||||
`wireBootReadySimulator` pattern already used by
|
||||
`TestPendingMove_ReviewToInProgress_OneTransitionOnly`. Assert the dispatch,
|
||||
not merely that `task.WorkflowStepID` changed — the step change already works
|
||||
today and is what makes the missing dispatch invisible without this assertion.
|
||||
|
||||
Confirm the test fails against the current code (hardcoded `0`) before the fix
|
||||
and passes after.
|
||||
|
||||
## Verification commands
|
||||
|
||||
```bash
|
||||
cd apps/backend
|
||||
go test ./internal/orchestrator/... -run 'PendingMove|DeferredMove' -v
|
||||
go test ./internal/orchestrator/... -run 'ApplyTransition' -v
|
||||
go build ./...
|
||||
golangci-lint run ./internal/orchestrator/... --new-from-rev=HEAD~1 --timeout=5m
|
||||
```
|
||||
|
||||
## Likely files
|
||||
|
||||
- `apps/backend/internal/orchestrator/workflow_store.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_workflow.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_pending_move_test.go` (or
|
||||
a new sibling test file)
|
||||
|
||||
## Dependencies
|
||||
|
||||
None — this is a self-contained call-site change with no upstream work-order
|
||||
dependency.
|
||||
|
||||
## Results
|
||||
|
||||
Implemented as designed:
|
||||
|
||||
- `workflow_store.go`: private `applyTransition` returns `(int64, error)`,
|
||||
returning `0` on every existing error path and `task.WorkflowStepTransitionID`
|
||||
on success. Public `ApplyTransition` discards the new first return value,
|
||||
keeping its `error`-only signature for `engine.TransitionStore`. Public
|
||||
`ApplyDeferredMoveTransition` now returns `(int64, error)`.
|
||||
- `event_handlers_workflow.go`: `applyPendingMove` captures `transitionID` from
|
||||
both the atomic (`ApplyDeferredMoveTransition`) and unfenced-fallback
|
||||
(`applyTransition`) branches, and threads it through the existing call to
|
||||
`processStepExitAndEnterForDeferredMove`. That function gained a
|
||||
`transitionID int64` parameter, passed into `processOnEnter` instead of the
|
||||
hardcoded `0`.
|
||||
- New regression test:
|
||||
`apps/backend/internal/orchestrator/event_handlers_pending_move_backward_test.go`
|
||||
(`TestPendingMove_BackwardMoveDispatchesOnEnterWithStaleRoute`), in a new
|
||||
file per the 800-line test-file convention. It seeds a
|
||||
`workflow_session_route` naming a step other than the move's destination
|
||||
(the state any task is in after its first workflow entry — which is exactly
|
||||
why the pre-existing `TestPendingMove_ReviewToInProgress_OneTransitionOnly`
|
||||
scenario, with no route ever established, never caught this bug), drives a
|
||||
backward `applyPendingMove`, and asserts `StartAgentProcess` was actually
|
||||
called — not just that `workflow_step_id` changed.
|
||||
- Confirmed RED against pre-fix code: failed with "StartAgentProcess was
|
||||
never called...".
|
||||
- Confirmed GREEN after the fix.
|
||||
|
||||
No changes to `ceiling_entry.go` or any staleness-guard logic — the existing
|
||||
`workflowEntryDispatchIsCurrentForSession` / `workflowEntryDispatchMayReplaceRoute`
|
||||
protections are exercised unmodified and still pass
|
||||
(`TestReplayCeilingDeferralRejectsStaleWorkflowEntryBeforeDispatch`,
|
||||
`TestWorkflowEntryDispatchRejectsStaleCommittedRoute`).
|
||||
|
||||
Commands run (from `apps/backend` unless noted):
|
||||
|
||||
```
|
||||
go build -tags fts5 ./...
|
||||
go test ./internal/orchestrator/ -run 'PendingMove|DeferredMove|ApplyTransition|Ceiling|WorkflowEntryDispatch' -v -timeout=180s -p 1
|
||||
golangci-lint run ./internal/orchestrator/... --new-from-rev=a7f7e01af8c04625e5378de6d69fd1756ef2937c --timeout=5m
|
||||
make fmt # (root) — reverted two unrelated gofmt-drift files by explicit path
|
||||
make typecheck # (root)
|
||||
make lint-format # (root)
|
||||
cd apps/web && pnpm run i18n:ratchet
|
||||
```
|
||||
|
||||
Note on `make test` (full-suite `go test ./...`): the whole-repo run is
|
||||
resource-contention-flaky in this sandbox — a bare `-timeout=10m` panic
|
||||
surfaced in unrelated SQLite-migration setup code
|
||||
(`migrateSubagentContextBackfillUnsafe`) inside completely unrelated tests,
|
||||
alongside failures in packages with no relation to this change (launcher,
|
||||
subproc, agentctl/server/process, k8s/docker/ssh-dependent tests). Verified
|
||||
non-diff-caused: the same targeted test set passes cleanly at
|
||||
`-p 1` (`go test ./internal/orchestrator/ -run '...' -p 1` → `ok`, all PASS
|
||||
including the new regression test), and re-running the identical targeted
|
||||
command plus a spot-check of an unrelated failing package
|
||||
(`internal/common/subproc`, `TestRunGitOutputAfterAcquireStartsTimeoutAfterAdmission`)
|
||||
against `git merge-base HEAD origin/main` (`a7f7e01af8`, a scratch worktree at
|
||||
`/tmp/kandev-scratch-verify/kandev`, removed after verification) also passed
|
||||
cleanly with default parallelism. The hang is load-dependent and reproduces
|
||||
independently of this diff.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
status: draft
|
||||
system: tasks
|
||||
created: 2026-09-21
|
||||
owners:
|
||||
- nova28
|
||||
---
|
||||
|
||||
# Deferred Move on_enter Dispatch Identity Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
A deferred `move_task_kandev` (a backward or otherwise non-immediate board
|
||||
move, applied at turn end) commits a workflow-step transition and then
|
||||
dispatches the destination step's `on_enter` actions itself. That dispatch
|
||||
must be able to identify itself as the current entry using the transition it
|
||||
just committed, so the destination step's declared `on_enter` actions
|
||||
(including `auto_start_agent`) actually run instead of being rejected by the
|
||||
staleness guard as if they belonged to an earlier, superseded entry.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001: Deferred move on_enter dispatch identity
|
||||
|
||||
**Intent:** A deferred move's own `on_enter` dispatch carries the identity of
|
||||
the transition it just committed, so the staleness guard recognizes it as
|
||||
current rather than rejecting it as stale.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001.1:** When a deferred
|
||||
`move_task_kandev` commits a workflow-step transition, the system shall pass
|
||||
that transition's ledger identity to the destination step's `on_enter`
|
||||
dispatch.
|
||||
- **AC-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001.2:** When the
|
||||
`on_enter` staleness guard evaluates a deferred-move dispatch that carries
|
||||
its own just-committed transition identity, the system shall recognize it
|
||||
as current and execute the destination step's declared `on_enter` actions,
|
||||
including `auto_start_agent`, in declared order. A dispatch that does not
|
||||
carry the current transition identity shall remain rejected.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The staleness guard's rejection logic itself
|
||||
(`workflowEntryDispatchMayReplaceRoute`, `workflowEntryDispatchIsCurrentForSession`),
|
||||
which correctly rejects a genuinely stale callback and is unchanged by this
|
||||
requirement.
|
||||
- The forward-move (`E1`, agent-turn auto-advance) `on_enter` dispatch path,
|
||||
which already threads its own transition identity through
|
||||
`stepentry.AllocationResult`.
|
||||
- The broader multi-path `on_enter` dispatch unification (covering
|
||||
`switch_workflow`, WIP-queue promotion, and the full acceptance-criteria set
|
||||
`AC-A1`-`AC-A13`) tracked informally in
|
||||
[`workflow-on-enter-action-dispatch/spec.md`](../../workflow-on-enter-action-dispatch/spec.md).
|
||||
This requirement formalizes only the narrow deferred-move identity gap that
|
||||
subset (`AC-A1`, `AC-A2` as applied to the `E2` deferred-move entry path)
|
||||
describes.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
status: draft
|
||||
system: tasks
|
||||
requirements:
|
||||
- REQ-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001
|
||||
---
|
||||
|
||||
# Deferred Move on_enter Dispatch Identity System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The task system owns the workflow-step transition ledger and the `on_enter`
|
||||
dispatch that follows a committed transition. This design covers only the
|
||||
identity a deferred move's own `on_enter` dispatch presents to the existing
|
||||
staleness guard. It does not change the guard's decision logic, the ledger's
|
||||
write path, or the forward-move dispatch path.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design sections |
|
||||
| --- | --- |
|
||||
| `REQ-TASKS-DEFERRED-MOVE-ONENTER-DISPATCH-IDENTITY-001` | [Transition identity threading](#transition-identity-threading) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
`workflowStore.applyTransition` (private, `orchestrator/workflow_store.go`)
|
||||
computes and durably records the ledger transition ID for every
|
||||
step-changing write, including a deferred move, via `updateTaskTx`. Its
|
||||
public wrappers `ApplyTransition` (part of `engine.TransitionStore`) and
|
||||
`ApplyDeferredMoveTransition` previously discarded that value.
|
||||
|
||||
`Service.applyPendingMove` and `Service.processStepExitAndEnterForDeferredMove`
|
||||
(`orchestrator/event_handlers_workflow.go`) drive the deferred-move commit and
|
||||
its own `on_enter` dispatch call.
|
||||
|
||||
`processOnEnter`'s staleness guard (`workflowEntryDispatchIsCurrentForSession`
|
||||
/ `workflowEntryDispatchMayReplaceRoute` in `ceiling_entry.go`) decides
|
||||
whether an `on_enter` dispatch is the current entry for a session, using the
|
||||
transition/entry identity the caller supplies.
|
||||
|
||||
## Transition identity threading
|
||||
|
||||
`workflowStore.applyTransition`, `ApplyDeferredMoveTransition`, and the
|
||||
private `applyTransition` return the `int64` ledger transition ID alongside
|
||||
their existing error. `ApplyTransition`'s public, broadly-implemented
|
||||
interface signature is unchanged; only `ApplyDeferredMoveTransition`, which
|
||||
has exactly one production call site, gains the new return value.
|
||||
|
||||
`applyPendingMove` captures the transition ID from both the atomic
|
||||
(`ApplyDeferredMoveTransition`) and unfenced-fallback (`applyTransition`)
|
||||
commit branches, and threads it through `processStepExitAndEnterForDeferredMove`
|
||||
into `processOnEnter`, replacing a hardcoded `0`. The staleness guard's
|
||||
existing `workflowEntryDispatchMayReplaceRoute` escape hatch then recognizes
|
||||
the dispatch as current by matching it against `task.WorkflowStepTransitionID`,
|
||||
the same identity the transition write already produced.
|
||||
|
||||
This changes only what identity a deferred move's own dispatch presents to
|
||||
the guard. It does not change the guard's rejection logic for a genuinely
|
||||
stale callback, and does not affect the forward-move (`E1`) path, which
|
||||
already threads its own transition identity through
|
||||
`stepentry.AllocationResult`.
|
||||
Reference in New Issue
Block a user