Skip to content

[feat] Publish and resume complete workflow state across machines #1084

Closed

Description

@hsuchifeng

Parent: #818

Goal

Allow explicit handoff of a composed workflow run between machines without a server or concurrent multi-machine leasing.

Scope

  • Add teamai workflow publish and resume.
  • Publish a masked snapshot under runs/<id>/state.yaml on teamai-reports.
  • Serialize DAG progress, nested sub-workflow frames, expression-visible results, named ends, loop/fan-out state when present, workspace and branch lineage, cleanup state, and session-continuation lineage.
  • Record the names of branches the previous owner pushed, for manual recovery. resume does not merge or delete those branches.
  • Preserve machine/tool bindings needed to decide whether a session may resume, without publishing private session contents.
  • Exclude prompts, secrets, unsafe outputs, and absolute machine paths.
  • Record owner, state revision, team revision, and publication base.
  • Claim ownership when another machine resumes.
  • Reject publication from a machine that no longer owns the latest snapshot.
  • Return in-flight leases to pending with new attempts.
  • Resume non-portable sessions fresh while preserving their semantic continuation point.
  • Reuse reports-branch locking and push-conflict handling.

Out of scope

Run records, concurrent multi-machine leasing, automatic branch recovery, live synchronization, state merging, and event waits.

Dependencies

Acceptance criteria

  • A nested DAG run with workspaces and session continuation survives machine handoff.
  • Machine B can claim a published run; A’s next publish is rejected.
  • In-flight attempts receive new IDs.
  • Snapshots contain no secret, prompt, private session content, or absolute machine path.
  • The snapshot preserves pushed branch names for manual recovery, and resume neither merges nor deletes those branches.
  • Local-only runs never touch Git.

Verification

Focused serialization, migration, two-clone ownership, pushed-branch snapshot, masking, and attempt-renewal tests; build/typecheck/lint; one real-CLI publish/resume E2E covering a sub-workflow and workspace. Regenerate commands.md and update both guides.

Activity

  1. SaulMoro commented on Oct 13, 2026

    @SaulMoro
    Collaborator

    #818 says a branch the old driver pushed is noted in the run record for manual recovery, and never merged automatically. Automatic recovery is out of scope here, which matches, but noting the branch isn't in this ticket or in #1083. Maybe a line here?

  2. hsuchifeng commented on Oct 14, 2026

    @hsuchifeng
    CollaboratorAuthor

    The snapshot records the branch names the previous owner pushed so they are available for manual recovery. resume does not merge or delete those branches.

    Automatic recovery stays out of scope. Run records stay in #1083.

  3. SaulMoro commented on Oct 14, 2026

    @SaulMoro
    Collaborator

    Notes from #1078 (#1129) for publishing and resuming run state:

    • A run file is self-contained. It snapshots the parent and every child definition at start, with one sourceHash, so resuming on another machine does not need that machine's cache to match.
    • It also stores inputs, and leases carry rendered env values. Publishing a snapshot has to treat both as possibly sensitive (see [feat] Enforce workflow requirements and execution limits #1082's masking).
    • Child steps exist only once they have been reached. A snapshot taken mid-run holds the steps created so far, not the whole tree. A resume rebuilds the rest from the snapshotted definitions.
    • Definitions written before the graph rules fail closed. A stored definition with a step after the first and no needs makes next return run_unreadable instead of running that step as a root. A resumed snapshot from an older teamai hits the same check.

    The engine rules that span files are in docs/dev/workflow-engine.md, added by #1129.

  4. SaulMoro commented on Oct 15, 2026

    @SaulMoro
    Collaborator

    For "Record owner, state revision, team revision": the team revision follows #1163. It is the commit the run's cached definitions came from, and the run state already carries it, so the snapshot copies it rather than reading HEAD on the publishing machine. The snapshot already carries sourceHash as the exact identity of what ran.

    Acceptance criterion to add: a run started at commit A and published after the team repo moves to commit B has a snapshot with teamRevision: A.

  5. SaulMoro commented on Oct 16, 2026

    @SaulMoro
    Collaborator

    Two things from #1148 for the snapshot:

    • run.archive is part of the state. A loop that runs a sub-workflow or for_each again moves the old children to run.archive, and a new instance numbers its pass after them (firstPass). A snapshot without it reuses pass ids, which collide in [feat] Add workflow scorers and recorded quality labels #1085 labels and [feat] Add workflow workspaces, branches, and session continuation #1080 branch names, and turns reads of a previous pass into null. The archive is never trimmed. The run file of a 1,000-item for_each that a loop ran 10 times was 4.66 MB, against 0.51 MB for one pass.
    • An older CLI continues a newer run without an error. The run file still says schemaVersion: v1alpha1. A CLI without loops continues a run that uses them and runs the rest without them, and a CLI with loops drops step keys it does not know when it reads a run. On one machine that takes a downgrade. With snapshots it takes two members on different versions. A snapshot field that names the features the run uses, which a CLI refuses to resume when it does not know one, would make this fail closed.
  6. SaulMoro commented on Oct 20, 2026

    @SaulMoro
    Collaborator

    Things from #1175 that a resume on another machine has to handle:

    • Workspaces are bound to the exact driver. feat(workflow): add workspace branches #1175 adds run.copies: { [branch]: { owner: DriverIdentity, commit? } }. next holds back any new or continue step whose branch is owned by a different kind/machine/workspace (workspace.ts:260-263). The owner is recorded at lease time (engine.ts:261-265), so a step in flight at handoff is held back too, and on machine B the local driver stops with busy (local-driver.ts:850). Claiming a run should rewrite every copies[*].owner to the claiming driver, and continue keeps starting from copies[*].commit, the last commit a step reported, as proposed on [feat] Add workflow workspaces, branches, and session continuation #1080. owner.workspace, lease.driver and run.driver hold absolute paths (src/workflow/types.ts:185, :206-207; the local driver sets workspace: path.resolve(cwd), local-driver.ts:54), so the snapshot drops them; resume rewrites them anyway. Criterion: a run published while a continue step is in flight resumes on another clone, and that step starts from the last reported commit of its branch.
    • Commits a lease needs may exist only where they were made. The lease names base.commit, start.commit and merge[].commit, and the local driver never fetches. On machine B, merging a commit B lacks fails, and mergeBranch reports it as a conflict with no files (worktree.ts:123-125): a command fails merge_conflict (local-driver.ts:383), and a prompt is told to resolve a conflict that does not exist. I'd have the driver fetch from origin when a commit the lease needs is missing, and fail with its own code. resume runs the same check for run.base and every commit the run still needs, and refuses, naming the commit and its branch, when one cannot be fetched, for example a base the member never pushed. It does not block on an old branch whose commits nothing needs any more. No new report field is needed, since a failed push already fails the step with push_failed. [feat] Add a Paseo driver adapter #1087's adapter meets the same contract from its own clone, so the fetch rule belongs in the driver contract.
    • Session bindings need the driver of each pass. Your scope keeps "machine/tool bindings needed to decide whether a session may resume", but feat(workflow): add workspace branches #1175 stores none: the lease's session is { tool, id } (workspace.ts:33-36), and a settled pass keeps no driver. I'd record the reporting driver's kind and machine with each settled pass, leaving out workspace because it is an absolute path, and put them on the lease's session. Criterion: a session started on machine A and continued after a resume on machine B is leased with A's machine, and B's driver starts it fresh.
    • The snapshot also leaves out any scorer answer or evidence; [feat] Add workflow scorers and recorded quality labels #1085 should keep them out of the run file in the first place.
  7. self-assigned this
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions