Skip to content

skipped masks crashes and total source failures — a broken run is indistinguishable from an idle one #60

Description

@rssrn

Summary

skipped is the harness's catch-all for anything that is not a completed run. A crash, a total failure of every task source, and a genuinely idle poll all produce the same signal — outcome=skipped in Prometheus, and in the crash case no runs row at all. Nothing on the dashboard or in Grafana distinguishes "healthy, nothing to do" from "broken".

This is not hypothetical. It has masked two separate incidents, one of them ten days long.

Three defects are covered here. None of them is fixed by the clean-checkout change in #61 — a fresh clone is still a network operation that can fail, and two of the three have nothing to do with repo prep at all.

Incident 1 — the crash that hid for 10 days

06:00:03Z  run start: repo=rssrn/labro
06:00:07Z  issue #59 created by proactive_improvement.fetch()
06:00:09Z  ERROR labro.repo - Command failed (exit 1): git -C /data/repos/labro pull
             Your configuration specifies to merge with the ref 'refs/heads/feat/publish-gate'
             from the remote, but no such ref was fetched.
           Traceback → cli.py:404 prepare_repo → repo.py:130 _run → CalledProcessError
           (process exits; nothing after this line runs)

No comment on the issue, no runs row, no label, and a Grafana metric reading skipped. Discovered ten days later only because someone asked why #59 had no agent comment.

Defect 1 — non-atomic issue creation

task_sources/proactive_improvement.py:214 creates the GitHub issue inside fetch(), during picking, before prepare_repo runs:

item_number, item_url = _create_issue(project.repo, title, body)

Any failure between issue creation and the agent invocation orphans a real issue on a public repo. #59 had zero comments and no ai-failed label, so it was indistinguishable from a queued item. Worse, it counted against the open-suggestion cap: every labro-proactive run for the following ten days logged

proactive-improvement: skipping rssrn/labro — 1 open suggestion(s) >= cap 1

so one orphaned issue dead-locked the whole task source. That dead-lock, not the crash, was the actual ten-day outage. gh-dependabot-alert creates issues the same way and has the same exposure.

Options. (a) Reorder so repo prep happens before issue creation — cleanest, but prepare_repo runs in the run loop and issue creation in the task source, so this moves a boundary. (b) Keep the ordering and add compensating cleanup: on any failure before the agent runs, close the issue or apply ai-failed so it stops blocking the cap. Option (b) is smaller and also covers agent-invocation failures, not just repo-prep ones.

Defect 2 — crashes are unrecorded and mis-reported

logger.write_run() is the only INSERT INTO runs, and it is never reached when prepare_repo raises — so a crashed run leaves no database row at all. The dashboard cannot show what it has no row for.

Separately, cli.py:404 is unguarded, so the exception escapes to main(). The finally block at cli.py:626-637 still runs and pushes metrics:

metrics_mod.push_run(
    project=project_name,
    outcome=_run_outcome,   # still "skipped" — the default from line 288
    ...
)

_run_outcome is initialised to "skipped" (line 288) and only reassigned at lines 315, 354, 374 and 600 — all after the prepare_repo call. A hard crash is therefore reported to Grafana as skipped, identical to a healthy idle run.

Incident 2 — every task source failed, reported as a normal idle run

Same masking, different code path, and this one recurs. On 2026-08-28 at 03:15 all five sources failed and the run was recorded as an ordinary skip:

03:15:03  run start: repo=rssrn/labro
03:15:04  WARNING gh-dependabot-alert: failed to fetch alerts for rssrn/labro — unexpected end of JSON input
03:15:04  WARNING picker: skipped: source error — gh-author
03:15:05  WARNING picker: skipped: source error — gh-label
03:15:05  WARNING picker: skipped: source error — gh-label
03:15:06  WARNING picker: skipped: source error — gh-author
03:15:06  picker: no task found across 5 source(s)
03:15:06  run skipped: no eligible task found for project 'labro'

The four source error lines are gh api ... returned non-zero exit status 1 tracebacks. An earlier occurrence on 2026-07-20 was a GitHub HTTP 503 that hit all three projects at once. Six crashes of this class appear in labro.log.

Defect 3 — total source failure is indistinguishable from an idle poll

picker catches per-source exceptions, logs a WARNING, and continues. That degradation behaviour is correct — one broken source should not sink a run. But when every source fails, the picker still reports no task found across 5 source(s) and the run reports skipped, which is the same output as a healthy poll with nothing to do.

Suggested behaviour: if all sources errored (or if any did, with a distinct outcome), record a non-skipped outcome and write a runs row. A run that examined zero sources successfully has not established that there is no work.

Why this went unnoticed for so long

Repo prep runs on 49 of 1,690 run starts (2.9%) — most runs skip at the picker and never touch the working copy. The broken working copy was therefore a landmine rather than a continuous failure: it crashed once, on 2026-08-17, and would have crashed again only on the next run that actually picked a task. Low-frequency code paths plus a catch-all skipped outcome is what makes these incidents invisible for weeks.

Suggested fix

  • Wrap the prepare_repo call (and ideally the whole run body) so an unexpected exception produces _run_outcome = "failure" and a write_run row with the traceback in the summary.
  • Introduce outcome values that separate harness faults from agent failures and from genuine idleness — e.g. crashed / source_error vs skipped.
  • Ensure a runs row is written for every run that starts, whatever happens after.
  • Once crashes and source failures land in the DB, alerts become possible: "no successful run for project X in N days", and "all sources errored".

Acceptance

  • A forced prepare_repo failure produces: a runs row with a non-success outcome, a metric that is not skipped, and either no orphaned issue or an orphaned issue clearly marked as failed.
  • A run in which every task source raises does not report skipped, and leaves a runs row.
  • The proactive cap cannot be dead-locked by a run that never reached the agent.

Context

Diagnosed while investigating why #59 never received an agent comment. The root cause of the git pull failure itself was stale branch.main.merge = refs/heads/feat/publish-gate config left in the reused working copy by an agent — the working copy has since been repaired by re-cloning, and #61 removes the reuse that allowed the residue to accumulate.


Related: #61 (clean checkout per run), #62 (remove WIP preservation).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions