Problem
v2 f.agent-spawned subprocesses have no broker seat, so agent-relay node agent attach can't reach them. Live inspection/steering — a real v1 UX (drive/view/passthrough modes, buffer preservation, SIGWINCH, session recording) — regresses.
Options considered + rejected
- Broker-back the worker. Violates journal-truth covenant (RFC-0001 covenant 1). Couples exactly-once execution semantics to broker delivery (at-least-once with dedup). Makes a broker crash equivalent to an agent death. Rejected.
- Native
flows attach reimplementing pty. ~year of pty work in ../relay (mode negotiation, screen buffer preservation, SIGWINCH forwarding, keystroke injection, session recording) would need reinventing. Rejected.
Chosen design (sidechannel)
The worker (packages/sdk/src/worker.ts) opens a UNIX socket at <data-dir>/runs/<run-id>/steps/<step-id>/pty.sock when it spawns an agent CLI subprocess. Byte-forwarding proxy:
- Everything the child writes to its pty goes to socket subscribers.
- Anything a drive-mode subscriber writes goes to the child's stdin.
Sidechannel is entirely optional for flow correctness. Socket broken/absent/unattended → flow still runs to its journaled outcome. Attach just fails.
Companion relay-side work (separate follow-up, out of this PR's scope)
Small feature in ../relay: agent-relay node agent attach --external-pty <socket-path> mode. Reuses its existing pty client stack (drive/view/passthrough, buffer preservation, SIGWINCH, session recording) — just pointed at the flows socket instead of a broker-managed agent. Filed as sibling issue in AgentWorkforce/relay after this lands.
Journal semantics ("off-journal by construction")
The sidechannel is a byte-forwarding proxy with no journal hook. That is a structural fact, not a policy.
- View mode is entirely passive → safe. Nothing about the run's outcome changes.
- Drive mode injections influence the child's stdout → journal captures the influenced output, but not the operator input.
Consequence: drive-influenced runs are silently non-replayable unless we mark them. Therefore:
- Worker marks
step.completed.human_intervention: true on any drive-mode connect.
- Replay/resume refuses to progress past such a step with a new refusal
human_influenced_run unless the operator passes --allow-human-influenced on flows resume/flows replay.
- View mode never sets the flag.
Acceptance
- Running a flow with an agent step:
agent-relay node agent attach --external-pty <data-dir>/runs/<run-id>/steps/<step-id>/pty.sock in another terminal shows the agent's live pty.
- View mode: text appears,
step.completed.human_intervention is absent (or false).
- Drive mode: keystrokes reach the child's stdin;
step.completed.human_intervention == true on that run.
- Flow with agent step still runs to completion if nobody attaches — sidechannel is optional.
- Socket path is deterministic + printed by
flows run after the worker spawns each agent step.
- Replay/resume across an intervention step refuses with
human_influenced_run unless --allow-human-influenced is passed.
Files to touch (flows side)
packages/sdk/src/worker.ts — open + serve the pty socket alongside the child spawn
packages/sdk/src/authored-worker-step.ts — thread run-id + step-id into the worker so it knows the socket path
kernel/relayflowd-core/src/machine.rs — human_intervention field on step.completed
kernel/relayflowd-core/src/* — new refusal kind human_influenced_run for resume/replay
packages/sdk/src/journal-client.ts — protocol type for the new field + refusal
packages/sdk/src/cli/run.ts + resume.ts + replay.ts — --allow-human-influenced flag plumb-through
docs/SURFACE.md — new subsection (under §5 or a new §7) documenting the sidechannel invariants + human_intervention semantics
- Tests: sidechannel view/drive smoke, replay refusal + override, run-to-completion without attach
Not in scope
- Relay-side
--external-pty client (sibling issue after this lands).
- Cross-cell tunneling (existing
agent-relay --node remote attach covers it).
- Journaled agent-to-agent chatter (that's slice R, filed separately).
Problem
v2
f.agent-spawned subprocesses have no broker seat, soagent-relay node agent attachcan't reach them. Live inspection/steering — a real v1 UX (drive/view/passthrough modes, buffer preservation, SIGWINCH, session recording) — regresses.Options considered + rejected
flows attachreimplementing pty. ~year of pty work in../relay(mode negotiation, screen buffer preservation, SIGWINCH forwarding, keystroke injection, session recording) would need reinventing. Rejected.Chosen design (sidechannel)
The worker (
packages/sdk/src/worker.ts) opens a UNIX socket at<data-dir>/runs/<run-id>/steps/<step-id>/pty.sockwhen it spawns an agent CLI subprocess. Byte-forwarding proxy:Sidechannel is entirely optional for flow correctness. Socket broken/absent/unattended → flow still runs to its journaled outcome. Attach just fails.
Companion relay-side work (separate follow-up, out of this PR's scope)
Small feature in
../relay:agent-relay node agent attach --external-pty <socket-path>mode. Reuses its existing pty client stack (drive/view/passthrough, buffer preservation, SIGWINCH, session recording) — just pointed at the flows socket instead of a broker-managed agent. Filed as sibling issue inAgentWorkforce/relayafter this lands.Journal semantics ("off-journal by construction")
The sidechannel is a byte-forwarding proxy with no journal hook. That is a structural fact, not a policy.
Consequence: drive-influenced runs are silently non-replayable unless we mark them. Therefore:
step.completed.human_intervention: trueon any drive-mode connect.human_influenced_rununless the operator passes--allow-human-influencedonflows resume/flows replay.Acceptance
agent-relay node agent attach --external-pty <data-dir>/runs/<run-id>/steps/<step-id>/pty.sockin another terminal shows the agent's live pty.step.completed.human_interventionis absent (orfalse).step.completed.human_intervention == trueon that run.flows runafter the worker spawns each agent step.human_influenced_rununless--allow-human-influencedis passed.Files to touch (flows side)
packages/sdk/src/worker.ts— open + serve the pty socket alongside the child spawnpackages/sdk/src/authored-worker-step.ts— threadrun-id+step-idinto the worker so it knows the socket pathkernel/relayflowd-core/src/machine.rs—human_interventionfield onstep.completedkernel/relayflowd-core/src/*— new refusal kindhuman_influenced_runfor resume/replaypackages/sdk/src/journal-client.ts— protocol type for the new field + refusalpackages/sdk/src/cli/run.ts+resume.ts+replay.ts—--allow-human-influencedflag plumb-throughdocs/SURFACE.md— new subsection (under §5 or a new §7) documenting the sidechannel invariants +human_interventionsemanticsNot in scope
--external-ptyclient (sibling issue after this lands).agent-relay --noderemote attach covers it).