Skip to content

Bugfix spec: failed events are never retried — dead tmux sessions, and the poller baselining failures as "processed" ​

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 ​

Two linked defects mean an event the-loop fails to process is silently dropped, never retried:

  1. Dead tmux session, no respawn (the issue title). With routing.runner: tmux, the dispatcher delivers a routed event by bracketed-pasting into a work item's live harness TUI (tmux session loop-<slug>). When that session has crashed or been killed, delivery fails; the dispatcher only releases the delivery for retry, and nothing recreates the session — so every retry hits the same missing session.

  2. The poller marks failed events as "processed" (the issue comments). The poller ends every cycle with state.update(ref, [all current comment ids], now) (cli/the_loop/poller/poller.py), which (a) baselines every comment as "seen" regardless of whether its dispatch succeeded, and (b) marks the work item "known", flipping first_sight false. So a comment (or spawn) whose dispatch failed is recorded as done and never retried on the next poll — exactly what the reporter observed: "even though the spawn of the tmux failed, it didn't pick that up in the next poll … probably because it was marked as processed in some log file."

There is no retry budget anywhere on the pull path, and no terminal "gave up" signal. Tracked as issue #80 and its comments.

Steps to reproduce ​

Dead session (webhook or poll): spawn a tmux-mode session for a work item, tmux kill-session -t loop-<slug>, then route any event for it — delivery fails forever into the missing session.

Poller baseline bug: run the-loop poll start with routing.runner: tmux against a labelled issue. If the tmux spawn fails on the first cycle, the item is recorded "known" and its comments baselined, so no later cycle retries the spawn or the comment — the item is stuck with no session.

Expected vs actual ​

  • Expected: an event the-loop fails to process is retried on subsequent polls, up to a configurable number of attempts (default 3); after the budget is exhausted the poller logs a terminal failure and ignores that event on later polls; a dead tmux session is respawned so a retry can actually land; and a new event (e.g. a fresh comment) on the same work item retriggers processing with a fresh budget.
  • Actual: a failed delivery into a dead tmux session loops forever; a failed spawn/forward on the poll path is baselined as "processed" and never retried; there is no attempt limit and no give-up log.

Root cause (confirmed) ​

  • TmuxRunner.deliver (runner.py) fails closed when has_session is false but gives no distinct signal, so Dispatcher._dispatch_one treats a terminal "session gone" identically to a transient fault — release for redelivery, never respawn. The dispatcher already knows how to spawn a tmux session (_spawn_tmux); it just never invokes it on the delivery path.
  • Poller._process_item calls state.update(...) unconditionally at the end of every cycle, before it can possibly know the dispatch outcome (dispatch is async — dispatcher.handle enqueues and returns). So success and failure are both recorded as "seen", and the spawn-retry guard (first_sight or new_comments) can never fire again once the item is "known".

Acceptance criteria (EARS) ​

Configurable retry budget (poll path) ​

  1. The system SHALL expose a configurable per-event retry budget (polling.maxRetries, default 3).
  2. WHEN the poller forwards a comment to a work item's session AND the dispatch does not succeed THEN the system SHALL re-forward that comment on later poll cycles until it succeeds or the budget is exhausted, rather than baselining it as processed after the first attempt.
  3. WHEN the poller spawns a session for a labelled item AND the spawn does not succeed (no session appears) THEN the system SHALL retry the spawn on later cycles until a session exists or the budget is exhausted — a failed spawn SHALL NOT suppress subsequent spawn attempts.
  4. WHEN a comment's or spawn's retry budget is exhausted THEN the system SHALL emit a terminal failure record in the event log (will_retry: false) and thereafter ignore that event on later polls (no infinite retry).
  5. WHEN a delivery is still in flight (enqueued/processing) THEN the system SHALL NOT count it as a failed attempt, so a long-running dispatch is never mistaken for a failure and prematurely given up.
  6. WHEN a new event (e.g. a new comment) arrives on a work item THEN it SHALL be processed with its own fresh retry budget, and SHALL re-arm a spawn that had previously been given up (a new comment retriggers the item).

Respawn a dead tmux session (delivery path) ​

  1. WHEN a routed event is delivered to a tmux-mode session AND that session's tmux session is not found (crashed or killed) THEN the system SHALL respawn the harness in a fresh tmux session for the same work item and deliver the pending event into it (as its boot prompt), so the event is not dropped and the next delivery/retry can land. (AC7)
  2. WHEN delivery fails for a reason other than a missing session (tmux unavailable, a paste sub-command errors while the session still exists) THEN the system SHALL keep the existing behaviour: fail the dispatch and release for retry, NOT respawn. (AC8)
  3. WHEN a respawn cannot proceed (harness CLI missing, tmux new-session fails) THEN the system SHALL fail the dispatch and release for retry, emitting an observable failure record rather than silently dropping the event. (AC9)

Observability ​

  1. Respawns and terminal give-ups SHALL be observable via dedicated event-log types distinct from a first-time spawn / a transient error. (AC10)

Out of scope ​

  • Bounding webhook (push) redelivery. GitHub owns webhook redelivery cadence and count; the retry budget governs the poll (pull) path, where the-loop itself drives retries. The respawn fix (AC7–9) still repairs the webhook dead-session loop by making the next redelivery land.
  • Resuming the crashed harness conversation. A respawn starts a fresh harness session; the prior in-memory TUI conversation is not recovered. the-loop resumes a work item from its checked-in artifacts (spec, execution-log.md, tasks) plus the delivered event. Interactive --resume of a dead session id is a separate enhancement (see design "Alternatives").
  • Proactive liveness monitoring. Recovery is reactive (driven by a delivery or a poll cycle), not an independent health poller.

Security considerations ​

No new attack surface. Retry accounting and respawn are parameterised entirely by already-recorded, previously-validated state — the durable poll state (comment ids the-loop itself observed) and the session registry (work item, harness, cwd/worktree, tmux target). No new external input decides what, where, or how often to spawn. The event that triggers a respawn is still rendered through the same untrusted-payload template (payload framed as data, never instructions), and the upstream authorized-actor guard is unchanged. Retries are bounded (default 3) and give up fail-closed with an audit record, so a crashing harness or a hostile flood cannot induce unbounded spawning: the poll path caps attempts per event, and each attempt is one-per-cycle on the serialized per-work-item worker.

Open questions ​

None. (Retry budget lives on the poll path; webhook redelivery stays GitHub-driven — see Out of scope. Confirmed against the reporter's comments: configurable, default 3; log-and-ignore after exhaustion; new comment retriggers.)

Released under the MIT License.