Found during an adversarial review of the #1732 bounded-execution stack (#1749). Not introduced by that stack — it is inherited from the reply-before-continue structure already on main — but it is a hole in the stack's stated promise that a caller of send_prompt hears {:error, :execution_fenced}.
The scenario
A conversation has both an unresolved bounded execution and a configuration reapply committed since its server last read configuration_revision.
handle_call({:send_prompt, ...}) preflights pass; kick_turn/4 calls TurnMachine.open/5, which returns :configuration_changed — the stale revision is checked before the fence.
kick_turn/4 returns {:noreply, %{state | handle: nil}, {:continue, {:reapply_prompt, ...}}}.
replying_ok/1 turns that into {:reply, :ok, state, continuation} — the caller is told :ok here, before the continuation runs.
- The continuation reaches
handle_continue(:provision, state), which calls ExecutionGuard._unsafe_interrupt(conv.id), gets {:ok, {:bounded, _}}, and returns {:stop, :normal, state}. The server terminates.
What the caller sees
200 {"status": "queued"} from the conversations controller. No turn is ever opened, no :execution_fenced is returned, and the server stops. The prompt is silently lost.
If provisioning had succeeded rather than stopping, the same prompt would then reach kick_turn/4 inside handle_cast({:initial_prompt, ...}), where a fence refusal has no caller to answer and becomes a Logger.info — same outcome by a different route.
Why it happens
The reply is sent with the continuation, so any refusal discovered after that point has nobody to tell. Anything that can fail in handle_continue after a {:reply, :ok, _, {:continue, _}} has the same shape of problem; the fence is just the case that makes it visible.
Repro sketch
Register a bounded execution that owes a remote stop (claim the spawn, bind an identity, then fail the turn — the journal lands on ready), bump the conversation's configuration_revision so the server's copy is stale, then POST /api/conversations/:id/prompts. Expect 409/422 naming the fence; observe 200 queued.
Notes
🤖 Generated with Claude Code
https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Found during an adversarial review of the #1732 bounded-execution stack (#1749). Not introduced by that stack — it is inherited from the reply-before-continue structure already on main — but it is a hole in the stack's stated promise that a caller of
send_prompthears{:error, :execution_fenced}.The scenario
A conversation has both an unresolved bounded execution and a configuration reapply committed since its server last read
configuration_revision.handle_call({:send_prompt, ...})preflights pass;kick_turn/4callsTurnMachine.open/5, which returns:configuration_changed— the stale revision is checked before the fence.kick_turn/4returns{:noreply, %{state | handle: nil}, {:continue, {:reapply_prompt, ...}}}.replying_ok/1turns that into{:reply, :ok, state, continuation}— the caller is told:okhere, before the continuation runs.handle_continue(:provision, state), which callsExecutionGuard._unsafe_interrupt(conv.id), gets{:ok, {:bounded, _}}, and returns{:stop, :normal, state}. The server terminates.What the caller sees
200 {"status": "queued"}from the conversations controller. No turn is ever opened, no:execution_fencedis returned, and the server stops. The prompt is silently lost.If provisioning had succeeded rather than stopping, the same prompt would then reach
kick_turn/4insidehandle_cast({:initial_prompt, ...}), where a fence refusal has no caller to answer and becomes aLogger.info— same outcome by a different route.Why it happens
The reply is sent with the continuation, so any refusal discovered after that point has nobody to tell. Anything that can fail in
handle_continueafter a{:reply, :ok, _, {:continue, _}}has the same shape of problem; the fence is just the case that makes it visible.Repro sketch
Register a bounded execution that owes a remote stop (claim the spawn, bind an identity, then fail the turn — the journal lands on
ready), bump the conversation'sconfiguration_revisionso the server's copy is stale, thenPOST /api/conversations/:id/prompts. Expect409/422naming the fence; observe200 queued.Notes
:execution_fencedreached callers as a bare 422 with no message is fixed separately in A reset refused by a bounded execution answers 409, not a bare 422 #2008.🤖 Generated with Claude Code
https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie