Skip to content

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 most MaxRestarts = 3 kill-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.Steered is an int COUNT of messages folded in (0 = never steered); ListenSummary.Steered aggregates 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.AddAsync reuses a path that already carries a worktree .git file (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).