# Bug Report: Ralph Loop Iteration Counter Not Incrementing

**Date**: 2026-01-22
**Repo**: dotfiles (using skills flake's ralph-wiggum extension)
**Extension**: `~/.pi/agent/extensions/ralph-wiggum/index.ts`

## Summary

The Ralph loop iteration counter stays stuck at 1 even when the agent completes work and calls `ralph_done`. The iteration prompt shows "Iteration 1/50" throughout the entire session, never advancing.

## Observed Behavior

1. Started ralph loop with `ralph_start` tool
2. Completed 7 categories of review work (35 lens passes)
3. Called `ralph_done` multiple times after completing work
4. Each `ralph_done` call returned: `"Pending messages already queued. Skipping ralph_done."`
5. Iteration counter never incremented past 1
6. Work completed successfully but loop showed "Iteration 1/50" the entire time
7. Final completion banner showed "1 iterations" despite doing ~7 logical iterations of work

## Root Cause Analysis

In `ralph_done` tool execute function (line ~460):

```typescript
async execute(_toolCallId, _params, _onUpdate, ctx) {
  if (!currentLoop) {
    return { content: [{ type: "text", text: "No active Ralph loop." }], details: {} };
  }

  const state = loadState(ctx, currentLoop);
  if (!state || state.status !== "active") {
    return { content: [{ type: "text", text: "Ralph loop is not active." }], details: {} };
  }

  // THIS IS THE PROBLEM
  if (ctx.hasPendingMessages()) {
    return {
      content: [{ type: "text", text: "Pending messages already queued. Skipping ralph_done." }],
      details: {},
    };
  }

  // Iteration only increments AFTER the pending messages check
  state.iteration++;
  // ...
}
```

The `ctx.hasPendingMessages()` check returns `true` when:
- Other tool calls are batched with `ralph_done`
- Follow-up messages are queued from previous operations
- Any async operations have pending responses

**In practice**, this guard ALWAYS triggers during normal agent operation because:
1. Agent makes multiple tool calls (read files, run commands, file issues)
2. Agent then calls `ralph_done` 
3. Previous tool responses create "pending messages"
4. Guard triggers, iteration skipped

## Impact

- **User confusion**: Progress appears stuck at iteration 1
- **No reflection checkpoints**: `reflectEvery` never triggers since iteration never advances
- **Incorrect completion stats**: Final banner shows wrong iteration count
- **Work document diverges**: Agent's actual progress doesn't match Ralph's iteration state

## Reproduction Steps

1. Start a ralph loop:
```
/ralph start test-loop --items-per-iteration 5
```

2. Have the agent do ANY work involving multiple tool calls:
```
- Read a few files
- Run some bash commands
- Call ralph_done
```

3. Observe: `ralph_done` returns "Pending messages already queued"

4. Check state file:
```bash
cat .ralph/test-loop.state.json | jq .iteration
# Always returns 1
```

## Proposed Fixes

### Option A: Remove the guard entirely

The guard's purpose seems to be preventing duplicate iteration messages, but it's too aggressive:

```typescript
// Remove this block entirely
if (ctx.hasPendingMessages()) {
  return { ... };
}
```

**Risk**: Might cause duplicate prompts if agent calls ralph_done multiple times.

### Option B: Increment iteration regardless, only skip prompt delivery

```typescript
// Always increment
state.iteration++;
saveState(ctx, state);
updateUI(ctx);

// Only skip the PROMPT delivery if there are pending messages
if (ctx.hasPendingMessages()) {
  return {
    content: [{ type: "text", text: `Iteration ${state.iteration} recorded. Prompt deferred due to pending messages.` }],
    details: {},
  };
}

// Continue with prompt delivery...
```

**Benefit**: Counter stays accurate even if prompt is deferred.

### Option C: Check for pending USER messages only

If `hasPendingMessages()` can distinguish message types:

```typescript
if (ctx.hasPendingUserMessages?.()) {  // More specific check
  return { ... };
}
```

**Benefit**: Tool responses wouldn't block iteration.

### Option D: Use a flag to prevent re-entry

```typescript
// At module level
let ralph_done_in_progress = false;

// In execute
if (ralph_done_in_progress) {
  return { content: [{ type: "text", text: "ralph_done already in progress." }], details: {} };
}
ralph_done_in_progress = true;
try {
  // ... do the work
} finally {
  ralph_done_in_progress = false;
}
```

**Benefit**: Prevents actual re-entry without blocking on unrelated pending messages.

## Recommended Fix

**Option B** seems safest:
- Iteration counter always reflects actual progress
- UI stays accurate
- Prompt delivery can be deferred without losing state
- Backwards compatible

## Additional Context

### State file after "completion" (iteration stuck at 1):

```json
{
  "name": "nix-modules-review",
  "taskFile": ".ralph/nix-modules-review.md",
  "iteration": 1,
  "maxIterations": 50,
  "itemsPerIteration": 5,
  "reflectEvery": 0,
  "active": false,
  "status": "completed",
  "startedAt": "2026-01-22T22:49:53.055Z",
  "completedAt": "2026-01-22T22:55:10.628Z"
}
```

### Actual work completed:
- 7 module categories reviewed
- 5 lenses per category = 35 review passes
- 14 issues filed in beads
- Epic created and closed

The iteration should have been ~7-8, not 1.

## Questions for Investigation

1. What exactly does `ctx.hasPendingMessages()` check? Is it documented in pi's ExtensionAPI?
2. Is this guard necessary for correctness, or just a precaution?
3. Are there other extensions using similar patterns that work correctly?
4. Should `ralph_done` be designed to be called as the ONLY tool in a response (documented behavior)?

## Workaround (Current)

Agent can manually copy the completed work doc to `.ralph/` and output `<promise>COMPLETE</promise>` to trigger completion detection via the `agent_end` event handler, bypassing `ralph_done` entirely. This is what happened in the observed session.
