plan() → [develop]RunState picks an instance that can run
FlowWorkflow calls the activities in order
1
prepare_step
Creates the directories on the volume, chown 1000. A new attempt (turn 1) clears out/; later turns delete only the old manifest and question. The session directory and logs/ are kept.
2
run_step
StepRunner: first deletes any leftover sandbox with the same name. profile → image, policy, provider; builds the mounts, assembles the prompt, openshell sandbox create, named <flow>-<step>-<hash>. Polls until it ends, with regular heartbeats. Afterwards saves the supervisor log, then deletes the sandbox.
3
collect_manifest
Reads manifest.json and question.json, and writes a fallback when one is missing. The workflow then turns results into questions according to escalate_when.
DoneBack to plan() for the next instances that can run. One sandbox per instance, never reused
needs_input → waiting_inputAfter you reply, turn+1: a new sandbox resumes the same session
FailedRetries with a new attempt up to limits.retries. After that, on_failed decides
A step with action: calls a single run_action (at most 2 tries). If run_step raises (worker crash, heartbeat timeout), cleanup_step makes sure the sandbox is deleted. Whenever the state has new events, the background publish_events writes them to events.db.