Skip to content

Refuse and close decision requests after their run ends - #2140

Merged
ppXD merged 1 commit into
mainfrom
fix/refuse-decisions-after-run-end
Nov 7, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/refuse-decisions-after-run-end

Conversation

@ppXD

@ppXD ppXD commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • A new tool-call claim on a terminal agent run is refused with AgentRunEndedException. The run's status is read with its fence epoch under the same FOR SHARE and checked before the fence, so a run ended by completion, stop, abandon or recovery always answers "ended". An existing key still replays as Duplicate, and a run with no row keeps the epoch-0 rule. The park never parks a row whose run ended, and takes no run lock ahead of the row lock.
  • StoppedRunDecisions.ExpireAsync closes Pending as well as AwaitingApproval decisions.
  • The MCP handler answers a refused call with a final "run ended" result instead of "retry shortly", and a re-call of a still-Pending decision re-parks it, posting exactly one card.

Known gaps (follow-up)

  • A write-approval row parked in the window after a stop's flip waits for the deadline reaper; only decision.request rows are closed by the stop's own close.
  • ReplayOnEndedRunAsync returns InFlight for a Pending write row on an ended run, and ResumeOrTicketAsync then blocks until the lease reaper.
  • Waking a call blocked on a closed decision and closing its card is left to a later backstop change.

Test plan

  • Unit: full project (15157 passed, 1 skipped), including the run-ended text, Expired replay, Pending re-park posting one card, Pending to Expired edge
  • Integration: StoppedRunDecisionFlowTests (47), ToolCallLedgerServiceTests (42), McpDecisionFlowTests (23)
  • Integration over real UDS + Postgres: AgentMcpEndpointFlowTests (29 passed, 1 skipped), handler held at guard-to-claim and at claim-to-park
  • Mutations (drop status check, refusal ahead of replay, drop Pending from ExpireAsync, restore retry text, fence checked before run end) each fail the suites

@ppXD
ppXD force-pushed the fix/refuse-decisions-after-run-end branch from 6142c90 to 6235157 Compare November 7, 2026 18:11
A run's normal completion keeps its fence epoch, so a decision.request
already past the authority guard could still claim a ledger row and park
it on a finished run, leaving an open question that accepts answers into
nothing. A row claimed before the end but parked after it was likewise
skipped by the end's close, which only covered AwaitingApproval.

TryClaimAsync now reads the run's status together with its epoch under
the same FOR SHARE and refuses a new claim on a terminal run with
AgentRunEndedException before comparing the fence, so a run ended by a
stop, abandon or recovery (which bump the epoch) answers "ended" rather
than lost ownership. An existing key still replays and a run with no row
keeps the epoch-0 rule. The park is guarded against ended runs without
taking a run lock ahead of the row lock, which would deadlock against
the terminal. StoppedRunDecisions.ExpireAsync now closes Pending as well
as AwaitingApproval rows. The MCP handler answers a refused call as final
instead of "retry shortly", and a re-call of a still-Pending decision
re-parks it, posting exactly one card.
@ppXD
ppXD merged commit bb4fb06 into main Nov 7, 2026
6 checks passed
@ppXD
ppXD deleted the fix/refuse-decisions-after-run-end branch November 7, 2026 18:50
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.

1 participant