Skip to content

Peers: tagged nodes across a call, and a resumed caller reattaches instead of relaunching - #128

Merged
Evanfeenstra merged 1 commit into
mainfrom
feature/peer-nodes-and-reattach
Oct 9, 2026
Merged

Evanfeenstra merged 1 commit into
mainfrom
feature/peer-nodes-and-reattach

Conversation

@Evanfeenstra

Copy link
Copy Markdown
Contributor

Why

Two gaps in strut/run-workflow (#123), both ruled on 2026-10-09 and folded into plans/federation.md §2.2:

  1. Provenance stopped at the call. The peer's stream carries each step's nodes, but the step tailed without reading them. The caller's log had the handle and the output and nothing about which graph nodes the peer's agent read.
  2. A caller that restarted launched twice. The handle only existed on step.end. A caller that crashed mid-call and auto-resumed re-executed the step and launched a second run on the peer, while the first kept running, orphaned and never cancelled. Separately, tailing a peer run that had died without a terminal event polled forever.

What

  • Tagged nodes. The step folds the peer run's step.end.nodes off the tail it already reads, dedupes by ref_id, tags each with the caller's peer id (AccessedNode.peer, never a URL: a URL is a second identity and goes stale on re-register), and marks its own output. A ref only means something against the graph that recorded it, so nothing resolves a tagged one locally: the projector skips it (no edge, not counted as unresolved), and the run flyout lists it under an @peer badge and opens nothing. Once read-through lands it opens through /peers/:id/graph/nodes/:ref_id.
  • Reattach on resume. Right after the launch, the handle is journaled as a synthetic step.end at <path>#launch. It has no stepType, so step counts, verify and the projector never see a step execution. A re-executed step finds it in ctx.journal and tails the same peer run from the start instead of launching another. The rule: in-process a child dies with its parent, so relaunch is right; across struts the child survives, so reattach is right.
  • A stale run ends its tail. The stream route passes TailOpts.live (controllers.has). A log with no terminal event and no producer now closes at EOF with done { status: "stale" }. tailPeerRun retries a stale done with backoff on its own counter, then fails peer_run_stale:. A summary-less done (the finalize race) reattaches at once.

Verified

1376 unit tests pass. New coverage:

  • the two-strut suite (real HTTP): tagged nodes, the #launch record, a hand-written stale caller log resumed through POST …/resume returning the same peer run's result with one run on the peer, and a stale peer log streamed to done stale
  • tailPeerRun: the stale retry/cap and the summary race
  • conformance: live on both tails
  • the projector and the web fold: the tag

Also run live on 2026-10-09: a local strut calling swarm38's explore over this path.

Not here

  • Hive's nodesOf still needs a one-line skip of tagged refs.
  • lab:peer + resolveScope (federation.md §3, step 4) is next. Until then a peer token is the peer's whole key.

…esumed caller reattaches instead of relaunching

strut/run-workflow (plans/federation.md §2.2, decided 2026-10-09):

- Provenance across the call. The step folds the peer run's step.end.nodes
  off the tail it already reads, dedupes by ref_id and tags each with the
  peer's id (AccessedNode.peer — the handle rule, never a URL), then marks
  its own output. A ref is meaningful only against the graph that recorded
  it, so no reader resolves a tagged ref locally: the projector skips it
  (no edge, not counted unresolved), the run flyout shows it under an
  @peer badge and opens nothing.

- Resume. The handle is journaled right after the launch as a synthetic
  step.end at <path>#launch (no stepType, so stepCounts/verify/projector
  see no step execution). A re-executed step — the caller restarted and
  resumed the run — finds it in ctx.journal and tails the same peer run
  from the start instead of launching a second one. In-process a child
  dies with its parent, so relaunch is right there; across struts the
  child survives, so reattach is right.

- A stale peer run no longer hangs the caller. TailOpts.live, passed by
  the stream route as controllers.has(key), closes a tail at EOF when the
  log has no terminal event and no producer, and the done frame says
  "stale". tailPeerRun retries a stale done with backoff (its own counter
  — the failed-open counter resets on every successful open) and then
  fails peer_run_stale:; a summary-less done (the finalize race)
  reattaches at once. Streaming an id with no log and no live run now
  ends at once with done stale rather than waiting for a log to appear.

Tests: the two-strut suite checks the tagged nodes and the #launch
record, resumes a hand-written stale caller log through POST …/resume
and sees the same peer run's result with one run on the peer, and
streams a stale peer log to done stale; tailPeerRun covers the stale
retry/cap and the summary race; conformance covers `live` on both tails;
the pure projector and the web fold cover the tag.

Docs: federation.md (§2.2 Provenance + Resume, Decided rows, validation,
step order, a dated status note that steps 1–2 are merged), API.md §4 +
§10, RUN_CONTROL_SPEC tail terminality, AGENTS.md.
Evanfeenstra added a commit to stakwork/hive that referenced this pull request Oct 9, 2026
…#5466)

A strut/run-workflow step folds the peer run's step.end.nodes onto its own
step.end, each tagged peer: <id> (stakwork/strut#128, plans/federation.md
§2.2). That ref_id names a node in the peer's graph, so resolving it against
this workspace's graph would show a node that is not there. nodesOf now
skips it, the same rule strut's projector and run flyout follow.
@Evanfeenstra
Evanfeenstra merged commit 6061c17 into main Oct 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant