docs/craft/background-interactive-turns-plan.md
Interactive Craft turns were request-bound: POST /sessions/{id}/send-message
owned the generator that drove opencode, persisted packets, and released the
prompt slot. A browser refresh closed that stream, causing GeneratorExit to
abort the sandbox turn and drop the live UI.
The target shape is to split starting work from watching work:
POST /sessions/{id}/send-message creates one interactive turn and returns
turn metadata.GET /sessions/{id}/turns/{turn_id}/events attaches to the live sandbox
event stream without owning the turn.CacheBackend with TTLs, request idempotency, runner ownership, and
heartbeat/stale-claim recovery./event stream as a viewer.BuildMessage rows.send_message remains
the opencode transport primitive and subagent follow-ups may still stream
directly.QUEUED, RUNNING, terminal statuses, active-turn lookup by session, and
idempotency by client_request_id.POST /send-message to validate ownership, persist the user message,
create the turn, start/retry a background runner, and return JSON turn
metadata instead of an SSE response.prompt_slot,
yield_sandbox_events, merge_events_with_announces,
persist_sandbox_event, finalize_persist, interrupt fence checks, and
heartbeat updates.GET /turns/active for refresh/rejoin discovery.GET /turns/{turn_id}/events for live SSE viewing through
subscribe_to_existing_session_events.activeTurnId and local ownership, call
createTurn, attach with streamTurnEvents, and reattach from
loadSession when the session has a running active turn.send-message, active-turn lookup, attach-only stream
behavior, terminal errors, and stale/not-running turns./event reconnects and
coalesced live text bursts.createTurn + attach, refresh rejoin,
attach failures, in-band errors, and stale terminal turn handling.localhost:3000 to confirm
refresh/rejoin does not resend the prompt and the UI displays streamed
output from the still-running sandbox session.