# pi-loop reference

## Authority boundaries

pi-loop separates controller, standalone-task, worker-execution, and monitor authorities:

| Domain | Authority | Storage |
| --- | --- | --- |
| Loop controllers, workflow state, and orchestration intent, evidence, and local capacity | `LoopStore` | `.pi/loops/*.json` |
| Standalone native tasks | `TaskStore` or external `pi-tasks` | `.pi/tasks/*.json` or provider-owned |
| Orchestration worker execution and global concurrency | protocol-v2 `pi-subagents` | provider-owned |
| Running monitors | `MonitorManager` | process memory |

Workflow state work is embedded in `LoopStore` as `WorkflowExecutionRecord`. It never appears in TaskStore, `/tasks`, task RPC, `TaskClaim`, or `TaskUpdate`. Standalone tasks never control workflow transitions. Finite orchestration intent, dispatch reservations, local capacity, and evidence are embedded in LoopStore; they never discover or project tasks or workflow executions. `pi-subagents` owns worker execution and its global queue.

Notification buffers and monitor process handles are memory-only. Orchestration wake intent is durable until delivery acknowledgement, but delivery remains at-least-once across a crash. Persisted controllers recover when Pi resumes; they do not execute while Pi is absent.

## Runtime map

- `src/index.ts`: extension registration and top-level wiring
- `src/types.ts`: loop, workflow, orchestration, and monitor contracts
- `src/store.ts`: loop/workflow/orchestration persistence and atomic mutations
- `src/task-store.ts`: standalone native task persistence
- `src/*-reducer.ts`: pure state transitions
- `src/runtime/`: session, notification, backlog, task-provider, orchestration, and monitor-completion wiring
- `src/tools/`: model-facing tool registrations
- `src/rpc/`: vendored cross-extension RPC contract
- `src/ui/`: status widget and tool rendering

File-backed stores use PID locks, unique temporary snapshots, fsync, atomic snapshot rename, and previous-snapshot recovery. Lock publication never replaces a destination: an initialized PID/UUID owner file is hard-linked into directory scaffolding, and a writer enters only when its claim is the sole directory entry. Scaffolding alone grants no authority. Contenders withdraw only their own claims; recovery reclaims individually dead owner claims while live or unknown entries remain fences. Legacy file/directory owners stay supported. This protocol requires local-filesystem hard links and coherent directory enumeration; arbitrary network filesystems are not guaranteed. Corrupt state fails visibly when no valid snapshot exists.

## Scope

`PI_LOOP_SCOPE` selects storage ownership:

- `memory`: process-local and discarded;
- `session` (default): isolated by Pi session ID;
- `project`: shared in the working directory.

Project scope shares durable state but does not yet elect one scheduler owner across concurrent Pi runtimes. Workflow execution leases prevent duplicate phase work; they are separate from the planned project scheduler fence. Subagent orchestration rejects memory, project, disabled, and custom-path stores; it supports only default file-backed session scope.

## Loop model

`LoopEntry.status` is `active` or `paused`. New pauses persist optional provenance as `pause:{kind,at,reason?}`. Kinds distinguish `administrative`, `controller_limit`, `semantic_terminal`, and `orchestration_settlement`; older paused snapshots may remain unattributed. Resume clears the record. Triggers are:

- cron: `{type:"cron", schedule}`
- event: `{type:"event", source, filter?}`
- hybrid: `{type:"hybrid", cron, event, debounceMs}`
- dynamic: `{type:"dynamic"}`

`LoopCreate` creates ordinary controllers. Use dynamic loops for one evolving goal without named phase/outcome routing; use workflows for ordered phases, conditional outcomes, rework, or durable handoff; use standalone tasks for independently completable backlog items. Free-text `/loop <goal>` controllers have no implicit fire-count cap; finite budgets require an explicit `LoopCreate maxFires`. `LoopUpdate` is only for dynamic controllers that are neither workflow nor orchestration owned and must persist `continue` after empty or unchanged iterations while work remains. `LoopDelete` pauses or removes ordinary/workflow controllers and cancellation-fences orchestration before stopping its workers. New loops expire after seven days by default; `PI_LOOP_EXPIRES_IN` changes that default and each creation tool accepts an `expiresIn` override. Durations are positive integer `s`, `m`, `h`, or `d` values and may exceed seven days. `LoopList` exposes the immutable ISO `expiresAt` boundary. Workflow presentation derives `claimed` (live lease, not observed execution), `waiting` (monitor attached), `paused`, `idle`, or ephemeral `stopped` activity from authoritative workflow fields and reports activity duration, wall-clock age, and state age without persisting duplicate UI state. Expiry and stale event/hybrid retirement during session recovery emit `loops:expired` plus a hidden notification with `deleted` or `paused` disposition. Fire limits bound repeated execution.

Wake delivery is idle-driven. A due timer or event mutates loop state, emits `loop:fire`, buffers a generation-tagged notification, and sends a hidden Pi message when delivery is safe. Immediately before a buffered workflow wake is sent, the runtime re-reads `LoopStore` and requires the controller status, definition revision, state, transition sequence, and execution ID to match the queued snapshot; deleted, transitioned, paused, or reissued work is dropped even within the same session generation. Retirement follows the same generation-fenced notification path after the store mutation and emits a typed `loops:expired` payload whose reason distinguishes `expires_at` from `resume_event_stale`. Pending fire and retirement notifications are memory-only; they are not a durable event ledger across process death. Stale extension contexts are probed before fire mutation.

## Subagent orchestration model

`OrchestrationCreate` persists an explicit finite batch of independent work directly in one dynamic `LoopEntry`. It requires the default file-backed session scope and a protocol-v2 `pi-subagents` responder before writing state. It does not discover TaskStore records, execute workflow phases, or use the generic scheduler.

The parent `agent_end`, existing 30-second heartbeat, session recovery, and direct `subagents:started|completed|failed` events drive reconciliation. Before each spawn, LoopStore records a generated dispatch ID and consumes local capacity. Spawn forces background execution, disables inherited extension tools, and leaves global queue/concurrency authority with `pi-subagents`. `spawning`, `queued`, and `running` all count against the local limit.

Lifecycle callbacks CAS loop revision, owner runtime/generation, work ID, dispatch ID, attempt, and agent ID. Normal terminal evidence is bounded and persisted with `provider_owned` completion status; pi-loop does not call `subagents:rpc:consume`, so the provider retains its native completion surface. Protocol-v2 stop replies and `stopped`/`aborted` lifecycle statuses acknowledge cancellation, not quiescence. They become non-retryable, non-consumable `uncertain` dispatches before stop RPCs run. A late spawn reply may attach its exact cleanup identity without restoring execution. Proved failures retry only within `maxAttempts`; a spawn timeout is ambiguous without upstream status/list or idempotent dispatch keys, so it becomes `uncertain` and is never retried automatically.

Completion bursts refill capacity without a pi-loop parent wake. When every terminal dispatch is provider-owned, pi-loop pauses the controller and acknowledges its aggregate wake internally so only the provider's native completion path runs. Uncertain dispatches and failures without a provider-owned terminal result retain the durable hidden wake until `pi.sendMessage` succeeds; a crash before acknowledgement may duplicate that delivery. Completed/attention controllers pause for `OrchestrationGet` inspection and explicit deletion. Presentation labels controller status as `active`, `needs attention`, `complete`, or `cancelled` while preserving reducer vocabulary in persisted state. Progress separates local `reserved` capacity from pending work and last-reported dispatch observations (`starting`, `queued`, `reported running`); none proves current worker liveness. Paused terminal controllers remain visible in the status line until deletion. Provider-native `SubagentWorkflow` UI and execution remain a separate provider-owned surface.

Session shutdown/switch invalidates lifecycle callbacks, persists unresolved work as uncertain, and best-effort requests cancellation before store rebinding. Any unresolved dispatch history prevents controller deletion under the Store lock; cancelled batches stay paused and inspectable. Late completion events do not clear this safety latch. Protocol v2 provides no safe automatic clearance or deletion route. Legacy `stopped`/`interrupted` records normalize to non-consumable uncertainty; previously launched retries or consumed evidence cannot be undone, and missing historical stop provenance cannot be reconstructed. A new runtime cannot adopt an unexpired foreign owner lease; after expiry it marks active dispatches uncertain rather than risking duplicates. Project scope, dynamic work addition, dependency graphs, cross-session election, and exact-once dispatch are not implemented.

## Workflow model

`WorkflowDefinition.version` is 1. Every state in a newly created definition must be reachable from `initialState`; reachable cycles are valid, and terminal reachability remains a separate policy. A state contains:

- `prompt`
- optional embedded `task:{subject,description}`
- optional `on:{outcome:targetState}`
- optional `maxAttempts` (required when an outcome targets the same state)
- optional state-local cron `loop`
- optional terminal status `completed` or `paused`

A run persists current state, transition sequence, attempts, state fire counts, active execution, execution history, monitor wait, last transition, definition revision, and immutable definition history.

### Ownership

The initial task execution is leased to the creating runtime. Every destination execution, including self-loop retries, starts unowned. `WorkflowClaim` claims unowned work, renews the same owner, or takes over an expired lease. Live foreign ownership fails closed.

`WorkflowTransition` validates the current lease, declared available outcome, attempt limit, active execution, pause authority, and definition revision. Ordinary active transitions proceed directly. Administrative and unattributed pauses require explicit resume. A controller-wide limit permits only a terminal transition; a state-local cadence limit permits an evidenced transition out of the exhausted state and resumes that destination atomically. Self-transitions never bypass a pause. A transition into a `paused` terminal state additionally requires `claim:{class,provider,subject,fact,expected}`. A trusted provider observes the fact outside the LoopStore lock; admission rejects missing, unavailable, stale, expired, conflicting, contradicted, or cross-context observations without writing. The built-in `monitor` provider exposes only `status`, `exitCode`, and `stopReason`; monitor output is never evidence. Admission confirms the exact fact, not whether workflow policy should treat that fact as a blocker—the declared edge remains the workflow author's policy. No user-authority provider is built in, and machine providers cannot grant `user_authority`, so those claims fail closed. After confirmation, the existing state/revision/execution CAS protects the locked transition.

No pending claim or general evidence ledger is persisted. A confirmed transition stores only a bounded admission receipt on `lastTransition` (claim class/provider/subject/fact/expected value, provider versions, and decision time). After restart, callers inspect current state and explicitly resubmit; stale context is rejected. One locked transition write settles source work, records evidence and the receipt, advances state, and creates the destination execution. A missing or exhausted route is handled through `WorkflowRevise`, not a fabricated transition. Completed terminals delete the controller; paused terminals preserve it with `semantic_terminal` pause provenance and represent a declared blocker—not a progress notification.

### Adaptive revision

`WorkflowRevise` accepts exact `expectedRevision`, `expectedState`, and `expectedTransitionSeq` plus a reason and 1–64 typed changes:

- `add_state`
- `revise_state` for non-current state content
- `reissue_state` for explicit replacement of current instructions
- `add_transition`
- `redirect_transition` with `expectedTo`

Graph revision is additive: no remove, rename, or full-definition replacement. Added states must be reachable and rejoin prior or terminal work. Redirected routes must retain a path to their prior target. `revise_state` still rejects the current state; callers must choose `reissue_state` explicitly rather than mutating instructions beneath an execution.

Acceptance appends the prior definition, reason, actor, timestamp, and patch to immutable history, then increments `definitionRevision`. Ordinary revisions preserve the current execution, lease, counters, and scheduler state. `reissue_state` is the bounded exception: it targets only the current nonterminal state. On an active controller it cancels any old execution into `executionHistory`, creates a fresh execution ID under the still-valid owner lease, resets state-entry/fire timing, keeps transition sequence and attempt count unchanged, then re-arms the active trigger and queues a fresh idle wake after persistence. Through an administrative pause it performs the same replacement atomically but leaves the controller paused with no trigger or wake, so `/loop` resume delivers only the fresh instruction. `controller_limit` and other non-administrative pauses reject reissue; semantic terminals reject revision entirely. Reissuing taskless work may create an unowned task execution that must be claimed. Reissued `maxAttempts` may equal but never undercut the current attempt; state-local fire limits apply to the reset fire count. The revision and fresh execution ID fence stale transition attempts. Revision never creates standalone workflow tasks. Maximum definition size is 65,536 UTF-8 bytes and maximum revision count is 32.

Revision and transition race through the same LoopStore lock. Revision-first makes a stale transition fail its definition CAS; transition-first makes a stale revision fail state/sequence CAS.

## Standalone tasks

Native task statuses are `pending`, `in_progress`, `completed`, and `closed`. `TaskClaim` starts or resumes unfinished work and returns a renewable bearer claim ID. `TaskHeartbeat` renews it. Subject/description edits, terminal updates, and deletion of live claimed work require that ID. Missing, wrong, or expired bearers reject before mutation; expired claims may be taken over.

Standalone tasks are independently completable backlog items. Related work that advances one evolving goal through ordered phases, conditional outcomes, rework, or durable handoff belongs in a workflow instead. Task prerequisites are description conventions, not first-class graph fields. Backlog workers use `TaskGet` to follow them, and unfinished handoffs persist material progress and next action through `TaskUpdate`.

pi-loop probes external `pi-tasks` through protocol-v2 RPC. When unavailable, the native provider and tools are registered. `autoTask` creates one standalone task per ordinary loop fire. `taskBacklog` adopts existing unfinished tasks and must use a recurring `tasks:created` trigger.

## Graph convergence warnings

Workflow summaries and `/loop` inspection show up to three warning witnesses, with an explicit omitted count. `diagnoseWorkflowGraph(run)` from `@trevonistrevon/pi-loop/api` returns full structured witnesses for closed nonterminal components, nonterminal dead ends, and a current-state terminal route cut off by already-exhausted destinations. It reuses outcome availability rules and does not mutate or reject workflows.

These are warning-only structural and point-in-time checks, not a liveness proof or a prerequisite engine. A reachable exit is not mandatory; an intentional ongoing cycle may still be valid. Diagnostics do not simulate future attempt increments, leases, evidence admission, fire/expiry limits, or future revisions. Revise only when finite completion is intended, preserving the existing revision/transition authority checks.

## Monitors

`MonitorCreate` spawns a detached process group, buffers bounded output, emits rate-limited progress, and records terminal status in memory. Its `timeout` is a renewable inactivity threshold: stdout/stderr bytes, JSONL progress, and `MonitorUpdate` renew the deadline; total runtime alone never stops an active monitor. `MonitorStop` sends TERM then KILL fallback. `onDone` creates a one-shot completion wake; `workflowId` pauses a workflow state's cadence until terminal monitor outcome.

Monitor events:

- `monitor:started`
- `monitor:output`
- `monitor:finished`
- `monitor:done`
- `monitor:error`

Monitor recovery across Pi process death is not implemented.

## Mutation guarantees

- Store mutations hold one file lock and persist one reducer snapshot.
- Rejected dynamic, workflow, and orchestration CAS operations are state-preserving.
- Workflow writes never span LoopStore and TaskStore.
- Server-held workflow leases expose no bearer token.
- Standalone task claims intentionally use bearer IDs because task RPC crosses extension boundaries.
- Session generation, owner leases, and stale-context checks prevent delayed callbacks from mutating replacement state.

## Cross-extension RPC

`src/rpc/` is canonical here and vendored verbatim into pi-orca. Any edit requires copying both files and bumping `VENDOR_REV` in both repositories.

RPC uses request/reply events with `requestId` and `<channel>:reply:<requestId>` envelopes. `PROTOCOL_VERSION` gates capability. The native task RPC server registers at extension initialization and disables itself when an external provider is active.

External consumers import only `@trevonistrevon/pi-loop/api`. Deep `src/` imports are unsupported.

## Limits and recovery gaps

- 25 active loops
- 25 running monitors
- 32 orchestration work items, 8 local workers, and 3 attempts per item
- 8,192 result characters and 2,048 error characters per orchestration dispatch
- seven-day default loop lifetime (explicit configuration may be longer)
- five-minute default interval for self-paced mode
- 32 workflow definition revisions
- 65,536 bytes per workflow definition

Remaining durability work is tracked separately in tasks: durable wake outbox, project scheduler fencing, and recoverable monitor execution. Current recovery is resume-time reconciliation, not unattended continuity or exactly-once delivery.

## Security boundary

Report vulnerabilities through GitHub private vulnerability reporting, never a public issue containing exploit details or secrets. CI and releases audit all dependencies. Do not use `npm audit fix --force`; update Pi and Pi TUI pins together and validate the resulting API contract.
