Skip to content

Commit ed9421b

Browse files
Copilotmrjf
andauthored
Fix autoloop pre-step starvation: clone repo-memory before scheduler + deterministic tie-break
Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/a98d4d22-8daa-4733-ab25-6cd7025a0ddc Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
1 parent 80c9193 commit ed9421b

1 file changed

Lines changed: 38 additions & 3 deletions

File tree

.github/workflows/autoloop.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ imports:
8484
- shared/reporting.md
8585

8686
steps:
87+
- name: Clone repo-memory for scheduler
88+
# The "Check which programs are due" step below reads program state from
89+
# /tmp/gh-aw/repo-memory/autoloop/, but gh-aw's built-in repo-memory clone
90+
# runs *after* this pre-step (and clones to a different directory). Without
91+
# this step, every program looks like a "first run" and the tiebreaker
92+
# starves programs that lose the original-insertion-order tiebreak.
93+
# See issue: "Autoloop pre-step can't read state files".
94+
env:
95+
GITHUB_TOKEN: ${{ github.token }}
96+
GITHUB_REPOSITORY: ${{ github.repository }}
97+
run: |
98+
mkdir -p /tmp/gh-aw/repo-memory
99+
if [ -d /tmp/gh-aw/repo-memory/autoloop/.git ]; then
100+
echo "repo-memory/autoloop already cloned; skipping"
101+
else
102+
git clone --depth=1 --branch memory/autoloop \
103+
"https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
104+
/tmp/gh-aw/repo-memory/autoloop \
105+
|| {
106+
echo "memory/autoloop branch not found (first run); creating empty dir"
107+
mkdir -p /tmp/gh-aw/repo-memory/autoloop
108+
}
109+
fi
110+
87111
- name: Check which programs are due
88112
env:
89113
GITHUB_TOKEN: ${{ github.token }}
@@ -377,7 +401,8 @@ steps:
377401
"next_due": (last_run + schedule_delta).isoformat()})
378402
continue
379403
380-
due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric})
404+
due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric,
405+
"schedule_seconds": schedule_delta.total_seconds() if schedule_delta else None})
381406
382407
# Pick the program to run
383408
selected = None
@@ -422,8 +447,18 @@ steps:
422447
pass
423448
print(f"FORCED: running program '{forced_program}' (manual dispatch)")
424449
elif due:
425-
# Normal scheduling: pick the single most-overdue program
426-
due.sort(key=lambda p: p["last_run"] or "") # None/empty sorts first (never run)
450+
# Normal scheduling: pick the single most-overdue program.
451+
# Tiebreaker rationale: programs that have never run (no last_run) take
452+
# priority over ever-run programs; among never-run programs, prefer the
453+
# shortest schedule (so "every 30m" beats "every 6h"), then alphabetical
454+
# by name. This avoids permanent starvation when state is missing — see
455+
# issue: "Autoloop pre-step can't read state files".
456+
def _due_sort_key(p):
457+
if p["last_run"]:
458+
return (1, p["last_run"], p["name"])
459+
sched = p.get("schedule_seconds")
460+
return (0, sched if sched is not None else float("inf"), p["name"])
461+
due.sort(key=_due_sort_key)
427462
selected = due[0]["name"]
428463
selected_file = due[0]["file"]
429464
selected_target_metric = due[0].get("target_metric")

0 commit comments

Comments
 (0)