Skip to content

Bugfix spec: remove the process runner — a stale session record silently downgrades tmux delivery to a dead process resume ​

Phase 1 of 3 for a bug (bugfix → design → tasks). Human approval for this tier-3 change happens at the PR (autonomy.tiers."3": human-approves-pr).

Summary ​

As reported, routing.runner: tmux only controls the runner chosen at first spawn. Once a session record exists under the registry, every later dispatch reads the runner from the record itself ("the session's recorded runner wins", decision-021). A record that ends up with runner: "process" — the dataclass default when the field is omitted — silently redirects every subsequent event for that work item to a headless adapter.resume(...) subprocess, using whatever cwd/harnessSessionId the record happens to carry, instead of the tmux session the operator is watching. No reconciliation compares the record against the live config, and nothing logs the disagreement: the poller reports a successful dispatch while the operator's tmux pane stays silent.

The owner's decision on the ticket (comment) resolves the bug at its root instead of patching the reconciliation gap: remove the in-process (headless subprocess) runner entirely. tmux is the only runner. With a single runner there is no per-record runner choice left to go stale, no config/record disagreement to reconcile, and no silent path for a delivery to take: every dispatch either lands in the work item's loop-<slug> tmux session or respawns one, loudly, through the existing issue-80/89/146 machinery.

Steps to reproduce ​

  1. Run the daemon with routing.runner: tmux in cli-config.yaml.
  2. Get a work item's registry JSON into a state where runner is "process" (manual edit, a copy/pasted session file, an out-of-band the-loop sessions register — which never sets runner, so the dataclass default applies — or a race between two near-simultaneous events).
  3. Trigger a new comment/event for that work item.
  4. The poller logs "routing X -> session Y" and records a successful dispatch, but nothing appears in the tmux session — delivery went to a one-shot subprocess resume, with no warning anywhere.

Expected vs actual ​

  • Expected: with the operator running a tmux fleet, every event for a work item is delivered into (or respawns) its attachable loop-<slug> tmux session; any inability to do so is loud.
  • Actual: events are silently consumed by headless claude -p --resume … subprocesses driven by a stale record; if the record's cwd points at another work item's checkout, the resumed process runs against the wrong working directory entirely, with no error surfaced.

Root cause (confirmed by reading the code) ​

Dispatcher._dispatch_one branches on session.runner == "tmux"; anything else falls to adapter.resume(...). Session.runner defaults to "process" (sessions/registry.py), so any record written without the field — including every record the-loop sessions register creates — takes the headless branch forever. RoutingConfig.runner is consulted only in _spawn_for, i.e. at first spawn. The defect is structural: two runners, a per-record selector with a quiet default, and no invariant tying the selector to the config. The fix the owner chose removes the second runner, and with it the selector.

Requirements ​

The wording of the owner's decision: "remove the whole in-process based claude/cursor run. ONLY run using tmux. remove all code and documentation related to process based runner."

Requirement 1 — tmux is the only runner ​

User story: As an operator, I want every daemon-driven session hosted in an attachable tmux session, so that nothing the-loop does is invisible to me.

Acceptance criteria (EARS) ​

  • AC1.1 WHEN the dispatcher spawns a session for a work item THEN the system SHALL host it in a named tmux session (loop-<slug>), with no configuration able to select a headless subprocess instead.
  • AC1.2 WHEN the dispatcher delivers an event to an existing session THEN the system SHALL deliver into the recorded tmux session, or take the existing respawn path when it is gone — never a headless adapter.resume subprocess.
  • AC1.3 WHEN the daemon (gh-webhook start / poll start) starts THEN the system SHALL always require the tmux binary in its dependency check.

Requirement 2 — the process runner's code and vocabulary are removed ​

User story: As a maintainer, I want the process-runner code paths gone, so that no stale record or config can route a delivery somewhere silent.

Acceptance criteria (EARS) ​

  • AC2.1 WHEN reading the harness adapter contract THEN the system SHALL expose no headless per-dispatch spawn/resume subprocess path; the one-shot invocation surface used by critic reviews (oneshot_argv) SHALL remain.
  • AC2.2 WHEN a session record is written THEN the system SHALL no longer write a runner field, and WHEN an old record carrying one (any value) is read THEN the system SHALL ignore it rather than branch on it.
  • AC2.3 WHEN routing.runner appears in an operator's config THEN the system SHALL ignore it and log a warning naming the removal (tmux-only), and the config schema SHALL no longer declare the key.
  • AC2.4 WHEN a session record has no live tmux session behind it — including a legacy record written by the process runner or by sessions register, which has no tmuxTarget — THEN the next dispatched event SHALL take the respawn path: resume the recorded conversation in a fresh loop-<slug> tmux session when possible, else start a fresh one, per the existing issue-89/146 rules.

Requirement 3 — documentation matches the single-runner reality ​

User story: As an operator reading the docs, I want no instruction that configures or describes the process runner, so that I cannot configure a mode that no longer exists.

Acceptance criteria (EARS) ​

  • AC3.1 WHEN reading the CLI config reference, the config templates, the skill reference docs, or the capability docs THEN the system SHALL describe tmux as the only way daemon sessions run; routing.runner and "process runner" SHALL appear only in historical records (decisions, changelog, past specs), which are not rewritten.
  • AC3.2 WHEN the affected capability docs are read THEN their behaviour tables SHALL reflect tmux-only dispatch, with a history row tracing to this spec.

Requirement 4 — the removal is proved, and stays proved ​

User story: As a maintainer, I want the test suite to pin the tmux-only behaviour, so that a future change cannot quietly reintroduce a silent path.

Acceptance criteria (EARS) ​

  • AC4.1 WHEN the test suite runs THEN tests SHALL cover: delivery to an existing tmux session, respawn of a legacy record without a tmuxTarget, the routing.runner ignore-with-warning, and the always-on tmux dependency check.
  • AC4.2 WHEN the full gate runs (pytest, ruff, pyright, markdownlint, config validation) THEN it SHALL pass.

Security considerations ​

  • Untrusted actors / trust boundary: the trust boundary between GitHub payloads and harness invocations is unchanged: nothing payload-derived reaches an argv, a path, or a tmux target. Removing the headless resume shrinks attack surface — the path where a corrupted registry file's cwd/harnessSessionId was handed to a subprocess (adapter.resume) is deleted; the remaining tmux paths keep their existing guards (_SESSION_ID_RE on resumed ids, _LOOP_TARGET_RE on signalled targets).
  • Abuse case: a hand-edited session record pointing cwd at another work item's checkout could previously make a silent headless resume run there. Post-change the same record routes through the tmux respawn path, which spawns in that cwd but visibly — named loop-<slug> session, announce comment on first spawn, session.respawned events. The registry stays local operator-writable state; its validation rules are unchanged.
  • Fail-closed: when tmux is unavailable the daemon refuses to start (dependency check) and a dispatch fails loudly; nothing degrades to a headless run.
  • No new attack surface: no new inputs, no new privileged operations; this change only deletes an execution path.

Out of scope ​

  • The reconciliation/sessions doctor alternatives suggested in the issue body — superseded by the owner's decision to remove the process runner.
  • Interactive support for the cursor harness in tmux (cursor-agent still raises UnsupportedRunnerError for interactive hosting, unchanged; it remains usable as a critic via oneshot_argv).
  • Rewriting historical records: decisions (e.g. decision-021), CHANGELOG entries, and past specs keep their text.
  • A registry migration tool: legacy records heal lazily on their next event via the respawn path (AC2.4); no eager rewrite of files on disk.

Open questions ​

  • None. The owner's comment on the ticket is the decision of record.

Released under the MIT License.