Record the in-flight ScriptTicket before the StartScript RPC
Summary
-
DispatchOrReattachAsyncrecorded the in-flight ScriptTicket afterStartScriptAsyncreturned (HalibutMachineExecutionStrategy.cs). A server crash — or a lost RPC response — in the window between the agent launching the script and the server committing the ticket left no durable pointer, so the resumed run dispatched a fresh ticket and ran the (non-idempotent) deployment script a second time. - Fix: record the ticket before firing StartScript (a one-statement reorder). A resume then always knows a ticket to re-probe —
TryReattachAsyncre-attaches if the agent still holds the script, or falls through to a clean fresh dispatch (via the agent'sComplete+UnknownResultunknown-ticket sentinel) if it never started.
This is the StartScript half of the SOTA-audit P1 (server-side idempotency, aligning with Octopus ScriptServiceV2). The RPC-retry + observe-loop pointer-preservation half (GetStatus/CompleteScript) follows in a separate PR.
Why it's non-breaking + generic
- Pure reorder of two adjacent calls that already exist; reuses the same
IInFlightScriptStoreand checkpoint column. No new state, no signature change. - It only ever avoids a duplicate — the agent is idempotent for a known ticket (
LocalScriptService/ScriptPodServicededupe by ticket), so a retried/known StartScript returns status, not a second launch. - Composes with the existing reattach (#430 stripe-locking), checkpoint-preserving timeout (#428), and circuit breaker — it makes the reattach path the recovery mechanism, not a victim. If
RecordDispatchedAsyncitself fails, the dispatch now aborts cleanly before any agent-side launch (strictly safer than the old order).
Test plan
-
Integration regression HalibutResumeReattachTests.FreshDispatch_RecordsInFlightTicket_BeforeStartScriptRpc— a hook captures, at the instant StartScript fires, what the store has durably committed (read through a fresh DI scope, as a resumed server would). Verified red→green: fails with "record-before-RPC violated" before the reorder, passes after. -
Checkpoints integration sweep: 12/12. Halibut/reattach unit: 158/158. dotnet build0 errors.