Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

## Unreleased

- **Durable session-stack recovery (P0.1).** While a child session runs, the
parent's `state.json` records `active_child_session_id`; on `--resume` the
coordinator walks the pointer chain to the deepest live session instead of
silently reopening the parent and orphaning the running child. Terminal
children found at startup are finalized (children.json completed, pointer
cleared) and their parent resumed. Every interrupted-dispatch crash window
reconciles deterministically: dangling pointers are cleared, a fully
created child whose parent commit never landed is adopted, and leftover
request files never dispatch twice (children.json records the originating
`request_file`). Invalid child requests are terminally rejected
(`*.json.rejected`) instead of being re-read forever.
- **Attempt ids.** Every dispatched task carries a unique `attempt_id`
(also on the wire in `TaskResponse`, echoed in `FinishedRequest`); a late
`/finished` from a superseded attempt of the same coordinates is treated as
stale rather than recorded as the current result.
- Iteration artifacts (`result.json`, `result_text.txt`, `prompt.txt`,
`harness_run_id.txt`, `pending_finished_request.json`), `children.json`,
and `salvage.json` are all written atomically (unique temp + rename) — a
crash can never leave a truncated recovery artifact.
- Internal: the three duplicated dispatch blocks in the coordinator collapsed
into one `_advance()` step (stop checks → child dispatch → next workflow →
stamped task), so they can no longer drift apart. `_advance()` also enforces
the suspended-parent invariant: a parent with a live child can never acquire
its own task (a duplicate `/finished` retry gets the child's live task
instead), and a coordinator-level transition lock serializes cross-store
handoffs.
- Review hardening (adversarial Codex review of the above): request-file
tombstones apply only to RUNNING child records (a completed child's
filename is reusable for new work); the children.json record lands BEFORE
the child state so an interrupted dispatch is always discoverable
(`failed_dispatch` + exactly-once redispatch); startup reconciles every
running-projected record (terminal children finalize even without a
pointer); the first child task carries an attempt id; attempt checks are
strict whenever the live task has one (including `result.json` provenance —
a stale artifact can no longer complete a new attempt); semantically
unusable child requests (unknown workflow set, no eligible workflow) are
terminally rejected instead of wedging every completion; packaged prompts
instruct atomic control/goal_check publication; the crash model (process
crash, no fsync) is documented.
- **The `pm_planner_dispatcher` template is executable from a clean init
(P0.4).** `loopy init --template pm_planner_dispatcher` now also ships the
`inner_outer_eval` child workflow set its dispatcher spawns — previously a
Expand Down
16 changes: 15 additions & 1 deletion docs/http-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Run response:
"workflow_id": "planner",
"session_id": "20260419_143022_71393ee22450_ab12cd34",
"iteration": 3,
"attempt_id": "a1b2c3d4e5f6",
"config_snapshot": {
"goal": "Ship a minimal working landing page",
"goal_hash": "71393ee22450",
Expand Down Expand Up @@ -115,6 +116,13 @@ Rules:
escape hatch is to kill that process and register again — the 409 message
names its pid.
- If the loop is in a terminal state, `/register` immediately returns `action=stop`.
- On `--resume`, the coordinator walks the durable parent→child session
pointers to the deepest live session: a running child continues where it
was (previously a restart silently reopened the parent and orphaned the
child), a child found terminal is finalized and its parent resumed, and
each interrupted-dispatch crash window reconciles deterministically
(dangling pointers cleared, fully-created-but-unpointed children adopted,
leftover request files never dispatched twice).

## POST /finished

Expand All @@ -128,6 +136,7 @@ Request:
"success": true,
"text": "done",
"error": null,
"attempt_id": "a1b2c3d4e5f6",
"worker": {
"hostname": "buildbox",
"pid": 4242,
Expand All @@ -143,7 +152,12 @@ Response: same shape as `/register` response (`action` is either `"run"` or `"st

Rules:

- If `session_id` + `workflow_id` + `iteration` does not match `current_task`,
- Every dispatched task carries a unique `attempt_id`; the worker echoes it on
`/finished`. A call whose attempt id differs from the live task's — even
with matching session/workflow/iteration — is stale: it belongs to a
superseded attempt whose work was already recovered or abandoned.
- If `session_id` + `workflow_id` + `iteration` (+ `attempt_id`, when both
sides carry one) does not match `current_task`,
the call is treated as stale: state is not mutated and `current_task` is not
changed. The current task's run response is returned only when the caller's
identity matches the task's recorded owner (or either identity is unknown —
Expand Down
20 changes: 20 additions & 0 deletions docs/session-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ eval-banana run \
- If the file is missing but `result.json` exists for the active task, the
coordinator can reconstruct the finished request from `result.json`

`children.json` (parent sessions)

- Index of child sessions dispatched by this session; each record carries the
child `session_id`, `workflow_set`, `status`, timestamps, `stop_reason`, and
the originating `request_file` name (which makes the dispatch scan
idempotent: a request whose filename already appears here is never
dispatched twice, even if a crash left the file behind)
- The parent's `state.json` additionally records `active_child_session_id`
while a child runs — the durable session-stack pointer a restarted
coordinator follows to resume the child instead of orphaning it

Atomicity and crash model: the coordinator- and worker-owned artifacts above
are written atomically (same-directory temp file + rename), so a **process
crash** never leaves them truncated — readers see either the previous or the
new complete file. This does not extend to power loss / kernel crashes (no
fsync), and it cannot be enforced for **workflow-written** signals
(`control.json`, `goal_check.json`, child requests): the packaged prompts
instruct agents to publish those via temp-file + rename, but a torn write by a
non-compliant agent is read as invalid output.

`salvage.json`

- Written into the interrupted iteration's directory during crash recovery,
Expand Down
Loading
Loading