7171 from datetime import datetime, timezone, timedelta
7272
7373 programs_dir = ".github/autoloop/programs"
74- memory_dir = ".github/repo-memory/ autoloop"
74+ state_file = ".github/autoloop/state.json "
7575 template_file = os.path.join(programs_dir, "example.md")
7676
7777 # Bootstrap: create programs directory and template if missing
@@ -182,35 +182,36 @@ The metric is `REPLACE_WITH_METRIC_NAME`. **Lower/Higher is better.** (pick one)
182182 schedule_str = line.split(":", 1)[1].strip()
183183 schedule_delta = parse_schedule(schedule_str)
184184
185- # Check last_run from repo memory
186- mem_file = os.path.join(memory_dir, f"{name}.json")
187- last_run = None
188- if os.path.isfile(mem_file ) :
185+ # Read lightweight state file (committed to repo, not repo- memory)
186+ # state.json tracks: last_run timestamps, pause flags, recent statuses
187+ state = {}
188+ if os.path.isfile(state_file ) :
189189 try :
190- with open(mem_file) as f :
191- mem = json.load(f)
192- lr = mem.get("last_run")
193- if lr :
194- last_run = datetime.fromisoformat(lr.replace("Z", "+00:00"))
190+ with open(state_file) as f :
191+ all_state = json.load(f)
192+ state = all_state.get(name, {})
195193 except (json.JSONDecodeError, ValueError) :
196194 pass
197195
198- # Check if paused (e.g., plateau or recurring errors)
199- if os.path.isfile(mem_file) :
196+ last_run = None
197+ lr = state.get("last_run")
198+ if lr :
200199 try :
201- with open(mem_file) as f :
202- mem = json.load(f)
203- if mem.get("paused") :
204- skipped.append({"name" : name, "reason": f"paused: {mem.get('pause_reason', 'unknown')}"})
205- continue
206- # Auto-pause on plateau: 5+ consecutive rejections
207- recent = mem.get("history", [])[-5:]
208- if len(recent) >= 5 and all(h.get("status") == "rejected" for h in recent) :
209- skipped.append({"name" : name, "reason": "plateau: 5 consecutive rejections"})
210- continue
211- except (json.JSONDecodeError, ValueError) :
200+ last_run = datetime.fromisoformat(lr.replace("Z", "+00:00"))
201+ except ValueError :
212202 pass
213203
204+ # Check if paused (e.g., plateau or recurring errors)
205+ if state.get("paused") :
206+ skipped.append({"name" : name, "reason": f"paused: {state.get('pause_reason', 'unknown')}"})
207+ continue
208+
209+ # Auto-pause on plateau: 5+ consecutive rejections
210+ recent = state.get("recent_statuses", [])[-5:]
211+ if len(recent) >= 5 and all(s == "rejected" for s in recent) :
212+ skipped.append({"name" : name, "reason": "plateau: 5 consecutive rejections"})
213+ continue
214+
214215 # Check if due based on per-program schedule
215216 if schedule_delta and last_run :
216217 if now - last_run < schedule_delta :
@@ -344,12 +345,11 @@ Each run executes **one iteration per configured program**. For each program:
344345### Step 1: Read State
345346
3463471 . Read the program file to understand the goal, targets, and evaluation method.
347- 2 . Read repo memory (keyed by program name) to get:
348- - ` best_metric ` : The current best metric value (null if first run).
349- - ` iteration_count ` : How many iterations have been completed.
348+ 2 . Read ` .github/autoloop/state.json ` for this program's ` best_metric ` and ` iteration_count ` .
349+ 3 . Read repo memory (keyed by program name) for detailed history:
350350 - ` history ` : Summary of recent iterations (last 20).
351- - ` current_branch ` : Any in-progress branch from a previous run.
352351 - ` rejected_approaches ` : Approaches that were tried and failed (to avoid repeating).
352+ - ` consecutive_errors ` : Count of consecutive evaluation failures.
353353
354354### Step 2: Analyze and Propose
355355
@@ -377,25 +377,26 @@ Each run executes **one iteration per configured program**. For each program:
377377### Step 5: Accept or Reject
378378
379379** If the metric improved** (or this is the first run establishing a baseline):
380- 1 . Record the new ` best_metric ` in repo memory.
381- 2 . Create a draft PR with:
380+ 1 . Create a draft PR with:
382381 - Title: ` [Autoloop: {program-name}] Iteration <N>: <short description of change> `
383382 - Body includes: what was changed, why, the old metric, the new metric, and the improvement delta.
384383 - AI disclosure: ` 🤖 *This change was proposed and validated by Autoloop.* `
385- 3 . Add an entry to the experiment log issue.
386- 4 . Update memory: add to ` history ` , increment ` iteration_count ` , clear ` current_branch ` .
384+ 2 . Add an entry to the experiment log issue.
385+ 3 . Update repo memory: add to ` history ` , reset ` consecutive_errors ` to 0.
386+ 4 . Update ` state.json ` : set ` best_metric ` , increment ` iteration_count ` , set ` last_run ` , append ` "accepted" ` to ` recent_statuses ` . ** Commit and push.**
387387
388388** If the metric did not improve** (or evaluation failed):
3893891 . Do NOT create a PR.
390- 2 . Record the attempt in ` rejected_approaches ` in memory with: what was tried, the resulting metric, and why it likely didn't work.
390+ 2 . Update repo memory: add to ` rejected_approaches ` with what was tried, the resulting metric, and why it likely didn't work.
3913913 . Add a "rejected" entry to the experiment log issue.
392- 4 . Update memory : increment ` iteration_count ` , clear ` current_branch ` .
392+ 4 . Update ` state.json ` : increment ` iteration_count ` , set ` last_run ` , append ` "rejected" ` to ` recent_statuses ` . ** Commit and push. **
393393
394394** If evaluation could not run** (build failure, missing dependencies, etc.):
3953951 . Do NOT create a PR.
396- 2 . Record the error in memory .
396+ 2 . Update repo memory: increment ` consecutive_errors ` .
3973973 . Add an "error" entry to the experiment log issue.
398- 4 . If this is a recurring error (3+ times), create an issue describing the problem and pause further iterations until resolved.
398+ 4 . If ` consecutive_errors ` reaches 3+, set ` paused: true ` and ` pause_reason ` in ` state.json ` , and create an issue describing the problem.
399+ 5 . Update ` state.json ` : increment ` iteration_count ` , set ` last_run ` , append ` "error" ` to ` recent_statuses ` . ** Commit and push.**
399400
400401## Experiment Log Issue
401402
@@ -436,19 +437,50 @@ Maintain a single open issue **per program** titled `[Autoloop: {program-name}]
436437- Close the previous month's issue and create a new one at month boundaries.
437438- Maximum 50 iterations per issue; create a continuation issue if exceeded.
438439
439- ## Memory Schema
440+ ## State and Memory
441+
442+ Autoloop uses ** two persistence layers** :
443+
444+ ### 1. State file (` .github/autoloop/state.json ` ) — lightweight, committed to repo
445+
446+ This file is read by the ** pre-step** (before the agent starts) to decide which programs are due. The agent ** must update this file and commit it** at the end of every iteration. This is the only way the pre-step can check schedules, plateaus, and pause flags on future runs.
447+
448+ ``` json
449+ {
450+ "training" : {
451+ "last_run" : " 2025-01-15T12:00:00Z" ,
452+ "best_metric" : 0.0234 ,
453+ "iteration_count" : 17 ,
454+ "paused" : false ,
455+ "pause_reason" : null ,
456+ "recent_statuses" : [" accepted" , " rejected" , " rejected" , " accepted" , " accepted" ]
457+ },
458+ "coverage" : {
459+ "last_run" : " 2025-01-15T06:00:00Z" ,
460+ "best_metric" : 78.4 ,
461+ "iteration_count" : 5 ,
462+ "paused" : false ,
463+ "pause_reason" : null ,
464+ "recent_statuses" : [" accepted" , " accepted" , " rejected" , " accepted" , " accepted" ]
465+ }
466+ }
467+ ```
468+
469+ ** After every iteration** (accepted, rejected, or error), update this program's entry in ` state.json ` :
470+ - Set ` last_run ` to the current UTC timestamp.
471+ - Update ` best_metric ` if the iteration was accepted.
472+ - Increment ` iteration_count ` .
473+ - Append the status (` "accepted" ` , ` "rejected" ` , or ` "error" ` ) to ` recent_statuses ` (keep last 10).
474+ - Set ` paused ` /` pause_reason ` if needed.
475+ - ** Commit and push** the updated ` state.json ` to the default branch.
476+
477+ ### 2. Repo memory — full history for the agent
440478
441- Store state in repo memory ** keyed by program name** . Each program gets its own memory namespace ( e.g., ` autoloop/training ` , ` autoloop/coverage ` ) :
479+ Use repo- memory ( keyed by program name, e.g., ` autoloop/training ` ) for detailed state the agent needs but the pre-step doesn't :
442480
443481``` json
444482{
445483 "program_name" : " training" ,
446- "best_metric" : 0.0234 ,
447- "metric_name" : " validation_loss" ,
448- "metric_direction" : " lower" ,
449- "iteration_count" : 17 ,
450- "current_branch" : null ,
451- "last_run" : " 2025-01-15T12:00:00Z" ,
452484 "history" : [
453485 {
454486 "iteration" : 17 ,
@@ -467,9 +499,7 @@ Store state in repo memory **keyed by program name**. Each program gets its own
467499 "reason" : " SGD converges slower within the 5-minute budget"
468500 }
469501 ],
470- "consecutive_errors" : 0 ,
471- "paused" : false ,
472- "pause_reason" : null
502+ "consecutive_errors" : 0
473503}
474504```
475505
0 commit comments