Skip to content

Commit e1324f3

Browse files
mrjfclaude
andcommitted
Use committed state.json instead of repo-memory for pre-step
repo-memory is only available inside the agent runtime, not during the bash pre-step. Split persistence into two layers: 1. .github/autoloop/state.json (committed to repo) — lightweight state the pre-step reads: last_run, best_metric, pause flags, recent_statuses for plateau detection 2. repo-memory (agent only) — full iteration history, rejected approaches, detailed notes The agent updates and commits state.json at the end of every iteration so the pre-step can make scheduling decisions cheaply. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ed4b671 commit e1324f3

1 file changed

Lines changed: 76 additions & 46 deletions

File tree

workflows/autoloop.md

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ steps:
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

346347
1. 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):
389389
1. 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.
391391
3. 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.):
395395
1. Do NOT create a PR.
396-
2. Record the error in memory.
396+
2. Update repo memory: increment `consecutive_errors`.
397397
3. 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

Comments
 (0)