Skip to main content
Every step of a run is appended to an event log the moment it happens, so a run that stops carries on from the file, not from a model’s memory of what it did. Use recordTo when a plain run() must outlive its process. Use the supervised runner when the run should restart on its own after a crash. A handoff you ask the model to write can drop the rules. This file is appended as the steps happen.

Motivation

When a chat dies halfway through, the handoff note a model writes is a summary, and the discarded attempt is the part it forgets. The record is written by the runtime, one event per step, and no model summarises it. That makes it durable: a fresh process reads it and knows what finished, what failed, and which question is still waiting for a person. Nothing that finished runs twice, so a crash costs the step in flight, not the run.

Parts

  • Events. One line per event, appended as it happens: a step started, an engine call returned with its usage, a review failed with its findings, a gate paused for an answer. A line that has landed is never rewritten, so a later step can’t change what an earlier one recorded. This is the single source of truth for run state.
  • Artifacts. In the runner’s store, anything too large for an event sits in a directory beside the log. The event carries a small reference to it with its digest, so the log stays small and the artifact can be checked against it.
  • The store. A plain run() appends to the file at recordTo. A run under the supervised runner keeps its own store: the run’s definition, its events and its artifact directory. Both are plain files, with no database and no server.

Lifecycle

A run appends run:start when it begins and run:end only if it finishes, with the same outcome and token usage that run() returns. A record still being written has a start and no end, and so does one whose process was killed. A resumed record keeps the earlier start too. Between them, every step, review, pause and engine call is a line. examples/offline-review.ts writes its record to .obversa/records/offline-review.jsonl. In it, a writer drafts a config, the review fails it for a missing timeout, the writer runs again with that finding, and the second review passes:
Example record

Recovery

The runner package’s supervised run restarts a killed run. It starts the run in a bounded worker. When the worker dies, it starts another against the run’s own store, and the new worker reads that store and carries on. Steps that finished are never repeated. A step that was mid-flight when the worker died runs again only if its binding declares it safe to retry; otherwise the run pauses and asks a person to reconcile it before it continues, so uncertain work is never repeated silently. A plain run() opens a fresh record each time it starts. With resume: true, it reads the record at recordTo first. For a job built with workflow(), what happens to each stage depends on where it stopped: resume: true is how a workflow carries on when you start it yourself or a schedule starts it for you. Callback gates covers how a run waits for the answer.

Limits

  • The record leaves out the model’s streamed text. It holds every decision and outcome, and each engine call with its usage.
  • A resume applies to the same workflow. Same name, workspace, brief and stage list, and the same seat behind each role. Change the brief, the model behind a role, or the question a person role asks, and the run starts again.
  • Only workflow() skips finished stages. Any other job given resume: true appends to the record and runs again.

Next steps