Skip to content

A fenced conversation whose configuration was reapplied answers 200 queued, then does nothing #2009

Description

@jhgaylor

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.

  1. 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.
  2. kick_turn/4 returns {:noreply, %{state | handle: nil}, {:continue, {:reapply_prompt, ...}}}.
  3. replying_ok/1 turns that into {:reply, :ok, state, continuation}the caller is told :ok here, before the continuation runs.
  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:conversationsConversation lifecycle, turns, streaming, recovery and orchestration.type:bugExisting behavior is incorrect or broken.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions