10. Session, Plan, and Goal State Machine
0. Durable operating mode versus live planning state
Each session persists exactly one operating mode: agent | plan | goal. There is one pi Agent. Live planning state and execution status are host/runtime projections. Plan and Goal are contract modes: both negotiate a proposal before executing and share one projection, one approval row, and one hard deny (D198). kind (plan | goal) is what distinguishes them, and the diagram below reads the same with Goal/SubmitGoal substituted for Plan/SubmitPlan:
Agent / inactive
-- user selects Plan while idle OR Agent calls EnterPlanMode --> Plan / planning
-- user selects Goal while idle OR Agent calls EnterGoalMode --> Goal / planning
Plan | Goal / planning
-- SubmitPlan | SubmitGoal (title, markdown, question) --> awaiting_approval
Plan | Goal / awaiting_approval
-- approve(permission mode) --> Agent / queued, same Agent continues
-- reject | expiry | abort | crash | persistence failure
--> same contract mode / planning
Agent / queued
-- dispatcher starts --> Agent / running
Agent / running
-- complete | fail | abort --> Agent / inactiveBoth contract modes retain the permission-mode selector. Their Bash policy is ask or accept-edits = confirmation and auto = no confirmation, so a contract mode expresses negotiating intent but is not a strict read-only security profile. Write/Edit and plugin tools remain denied by host policy in every Plan or Goal permission mode.
The kinds differ only in what the contract says and what the queued execution instruction asks for: Plan proposes ordered steps to carry out, while Goal proposes a goal statement, acceptance criteria, and boundaries, and its execution keeps working — choosing its own approach — until every acceptance criterion is verified or a boundary blocks it.
Mode and configuration changes through the UI/session API are allowed only while idle. Approval is not a generic tool permission: it is a separate host-owned state transition. A host restart interrupts every pending approval and queued/running execution field without replay; an already-approved interruption keeps the durable session in Agent.
1. Session status
idle <-> running <-> waiting_permission
\/ aborted
\/ error| status | meaning |
|---|---|
idle | no active turn |
running | model/tool turn active |
waiting_permission | blocked on user permission decision |
aborted | terminal for current turn (then returns idle) |
error | terminal for current turn (then returns idle) |
2. Turn lifecycle
accept_prompt
-> turn_start
-> streaming
-> (optional tool_loop)
-> permission_maybe
-> tool_exec
-> turn_end3. Transition rules
- Only one active turn per session
- New prompt rejected with
AGENT_BUSYwhile running/waiting_permission - Abort from running or waiting_permission is allowed. Renderer smart Stop removes an unanswered root user row and restores its session/turn-scoped pre-serialization composer snapshot; once assistant text, thinking, or any tool row begins, abort preserves the partial transcript and restores no draft.
- Permission timeout moves to tool denied, then agent may continue or end based on runtime handling
- Session status returns to idle after terminal turn states are persisted
- Changing the renderer's active project/session does not transition or abort any background session
- A tool transition retains the originating session's persisted project root; it never adopts the newly active project's root
session.endTurnmoves only arunningturn to terminal. In that same transaction, unseencompletedinsertstask.completed, unseenerrorinsertstask.failed, and a result already visible in the focused current chat or anyabortedturn inserts no notification (D117). Repeated terminal calls are no-ops.- Fork is allowed only while the source is idle. The child begins idle with no turn or waiting-permission state. Electron returns
AGENT_BUSYfor its active runtime guard and normalizes the host's persisted running-turnCONFLICTfallback to the same IPC error. Neither path produces a partial child. - Supplying
throughMessageIdchanges only the snapshot boundary. Assistant Fork/Edit still creates a new idle session id with no shared turn, permission wait, runtime, or provider-cache state (D134). EnterPlanMode,EnterGoalMode,SubmitPlan, andSubmitGoalmust be the only tool call in their assistant batch. A submit tool preserves exact Markdown bytes in a new host-owned.pi/<kind>/*.mdartifact and creates one pendingplan_approvalsrow with itskindplus structured title/question and artifact fields. A submit tool called against the other kind's mode fails withPLAN_KIND_MISMATCHand writes nothing.- Only a matching
plans.resolvecan settle a pending proposal. Approval atomically changes the durable mode to Agent, stores the selected explicit permission mode, assigns an execution ID, and changes the row'sexecution_statetoqueued. - Approve and reject are the only resolution actions. Rejection and expiry close the pending row, then return the live state to the editable planning state of the same contract mode and grant no execution tools. A pending interruption does the same; a queued/running interruption after approval stays Agent.
- A second prompt, Plan or Goal submission, configuration change, or execution is rejected while the session has an active turn, pending approval, or queued/running execution. Configuration is accepted only while idle.
- A later turn in the same contract mode may revise a rejected/expired/interrupted checkpoint and must create a new immutable artifact rather than overwrite the earlier snapshot.
4. Persistence points
Message persistence is two-step per 04-data-storage §5 (D119): fsync'd transcript-file line first, index transaction second.
- user message: on accept
- turn run row: on start + terminal
session.endTurnupdate - notification row: same transaction as an unseen completed/error terminal update; never for a visible-current result or abort
- assistant/tool messages: on message_end/tool_end
- unanswered smart Stop: mark the turn aborted through the existing lifecycle, then atomically rewrite the transcript to the prefix before its root user row; the structured composer snapshot remains renderer-memory-only
- mode/project fields: on change
- Plan/Goal submission: write exact Markdown bytes to a new unique
.pi/<kind>/*.md, record path/hash/size plus the kind and structured title/question, and insert apendingplan_approvalsrow before the approval event - Plan/Goal approval: approval outcome, mode transition, permission mode, execution ID, and
queuedstate in one transaction; reject/expiry/interruption retain the contract mode and return live planning to editable state - startup recovery: transactionally interrupt pending approvals and queued/running execution states before serving RPC; abort associated running turns and never replay work
- fork snapshot: new transcript file plus one child session/index transaction; source persistence remains untouched; a message-scoped snapshot ends inclusively at the selected message
5. Acceptance
- Busy session cannot start second concurrent turn
- Abort is idempotent
- waiting_permission is visible in UI status
- sessions in two retained project tabs may run independently without transcript-event or workspace-root crossover
- each unseen completed/failed turn produces exactly one notification record while a visible-current result or aborted turn produces none
- an idle fork starts as an independent idle session; a busy source cannot produce a child
- a message-scoped fork excludes later rows and begins with no source runtime or provider-cache state
- Plan, Goal, and Agent use one pi Agent; the Composer-left mode chip, UI entry, and
EnterPlanMode/EnterGoalModeconverge on the same planning state, and approval resumes that Agent in Agent mode - Contract-mode policy permits Bash only through the selected permission mode and denies Write/Edit/plugins regardless of
autoor session grants, in Goal exactly as in Plan - SubmitPlan/SubmitGoal writes an exact unique
.pi/<kind>/*.mdartifact with hash/size, keeps title/question structured, and only approve/reject can resolve itsplan_approvalsrow - Expiry uses
PLAN_APPROVAL_TIMEOUT; startup interruption, shell failure, and process recovery are fail closed, and restart does not replay pending, queued, or running work - A Goal execution reports each acceptance criterion's outcome before ending the turn, and a scheduled/unattended Goal run is rejected with
PLAN_REQUIRES_INTERACTIVE_SESSIONexactly like Plan