Steer lane (mid-run input injection) — FLT-101¶
Status: phase 1 implemented (SteerService, queue + HTTP paths, CLI steer); phase 2
(persistent sessions) is a follow-on ticket. Companion: steer-lane-consult.md (raw Codex
consult, 2026-07-17) and cancel-lane.md (the sibling control lane this reuses).
Goal¶
Send NEW information to a running fleet agent job that the agent considers as soon as its harness allows — the Codex-harness steering experience — without killing the run when the harness supports better.
Marker (shared contract, all phases)¶
Path: <dispatchRoot>/<target>/steer/<jobId>/<sequence>.json (sequence = monotonically
increasing int; multiple steers per job allowed).
{
"jobId": "...", "target": "...", "sequence": 1,
"createdAt": "ISO-8601", "origin": "...",
"mode": "append | interrupt_then_append | replace",
"message": "<the new information / instruction>",
"sig": "<hex HMAC-SHA256>"
}
Signature canonical string: steer|{jobId}|{target}|{sequence}|{createdAt}|{origin}|{mode}|{message}
— same fleet key and posture as CancelService (size cap, shape-validated ids/targets,
consumed → archive/consumed/<jobId>.steer.<sequence>.json, 24h sweep).
Phases¶
Phase 1 — degraded steering for today's fire-and-forget runners (this branch).
No persistent sessions yet. A SteerService watcher (same poll pattern as
CancelService.ExecuteWithCancelAsync) sees a marker mid-run and performs
kill-and-resume: cancel the current attempt's token (existing tree-kill), then re-invoke
the same kind with an augmented payload. Implemented rules:
- phase 1 accepts only
mode:"append"; other modes are refused at signing AND at verification (forward-compat field for phase 2); - messages capped at 8 KiB (
MaxMessageBytes), markers at 32 KiB, at mostMaxRestarts = 3kill-and-resume cycles per job (marker-spam bound); - cancel always outranks steer; a steer that races a successful completion does NOT restart the job (finished work is never re-run);
- augmentation revalidates against the kind's enqueue rules (argv caps) — steering never turns a valid job into an invalid one; unappliable steers are consumed and logged;
DispatchResult.Steeredis an int COUNT of messages folded in (0 = never steered);ListenSummary.Steeredaggregates per drain pass;- markers found while nothing is running (including before the first attempt) fold in for free — no kill, no restart slot; applied sequences are tracked as a SET, so markers syncing out of order (3 before 2) are never dropped;
- once the restart budget is spent the watcher disarms: late markers can no longer kill work they cannot resume, and a steer racing a successful completion is consumed and discarded (the success result stands);
- run_agent resumes in its OWN worktree:
AgentWorktreeManager.AddAsyncreuses a path that already carries a worktree.gitfile (single-use job ids make that provably this job's earlier attempt); - payload for the resumed attempt —
Previous headless run was stopped for steering.
New user steering: <message>
Preserve useful completed work. First inspect the current state (for repo tasks: git
status/diff). Do not repeat already completed steps unless needed.
<original task>
- Prompt kinds (claude/codex/agy/grok/ollama, run_agent): augmentation as above; run_agent re-runs in the SAME worktree (host already does same-worktree retries, and the tree carries the partial work forward).
- Non-prompt kinds (pwsh, odysseus): not steerable — marker rejected at enqueue.
- Result records
"steered": [sequences]and a known-risk line "steering was kill-and-resume (degraded)".
Phase 2 — persistent-session runners (separate ticket once phase 1 lands).
Capability-mapped true steering per the consult:
- Codex → codex app-server JSON-RPC: turn/steer (same-turn append), turn/interrupt.
- Claude → claude -p --input-format stream-json --output-format stream-json, stdin held
open: steer = queued next turn; interrupt via kill + --resume <sessionId>.
- grok → grok agent stdio (ACP): session/cancel + session/prompt.
- agy → stays phase-1 fallback (no headless surface).
Capability flags: SameTurnSteer | QueueNextTurn | Interrupt | Resume; router picks the
strongest mechanism ≤ the marker's mode.
Sender surfaces¶
- CLI:
fleet-dispatch steer --target PC --job-id id --message "..." [--mode append] - FMC cockpit: POST /api/agent-runs/{jobId}/steer → writes marker (holds fleet key server-side, same as the cancel upgrade).