- stopLoop re-arms the poll ticker after removing one loop, so stopping one of
several no longer strands the rest with no tick scheduled (jatmn).
- a bare self-paced /command is rejected with a pointer to the interval form:
self-paced drives a free-form goal via the playbook + LOOP: control line, which
a custom command's expanded template never carries, so it would silently
degrade to adaptive polling (jatmn).
- /resume only tears loops down on an actual session-id change; /resume latest or
/resume <currentID> can resolve to the already-active session, whose loops are
its own, not a "previous" session's (CodeRabbit).
- tests: skipped-behind-modal loop stays scheduled, footer carries the next-wake
time, the confirmed /clear actually wipes the transcript, plus multi-loop stop
keeping the ticker alive and self-paced-/command rejection.
Response to the review. The self-paced path was the real gap: it advertised
model-chosen completion but nothing ever wrote the done/next-wake fields, so it
was just adaptive polling dressed up. Replaced that with an actual control
channel — each self-paced iteration runs a short maintenance playbook and ends
its reply with a control line (LOOP: DONE / LOOP: CONTINUE [interval]) that
advanceLoop parses to stop the loop or pick the next wake. The two dead model
fields are gone; done and next-wake now come from the model's output.
Rest of the review:
- loops stop on a session switch (/new, /resume) instead of firing the previous
conversation's prompt into the new one
- validateLoopTarget resolves custom /commands up front and rejects unknowns, so
a mistyped `/loop 5m /nope` isn't scheduled and then re-sent as literal text
- a one-shot confirm gates /clear and /quit while loops are active
- age-based auto-expire (24h) next to the iteration cap
- the two CodeRabbit test nits (HasPrefix footer check, bare `/loop stop` on a
single loop)
Hardening from a self-review pass over the above:
- normalize CRLF and allow a trailing note on the control line, so
`LOOP: DONE — all clean` and \r\n replies still stop the loop
- disarm the leave-confirm before the queue/exit early returns, so an interposed
prompt can't leave it falsely armed
- a loop no longer fires behind an open picker/modal; a run launched behind the
/resume picker would otherwise complete into whatever session you switch to
Addresses CodeRabbit review on #502:
- advanceLoop now counts consecutive failed iterations (loopMaxFailures=3) and
stops the loop, so a loop whose turns keep erroring no longer churns to the
iteration cap (the doom guard only sees identical non-empty answers).
- cancelRun clears activeLoopID and re-arms the interrupted loop for its next
cadence, so a cancelled iteration is not left running forever and the next
unrelated turn is not misattributed as that loop's completion.
Third slice of #501.
- Self-paced loops now use an adaptive cadence (2m early -> 30m heartbeat as
iterations accrue), with the loopNextWake channel kept as an override for a
future explicit model signal.
- Persistent footer segment "↻ N loops · next 3:05pm" while loops are active.
- /clear notes that loops keep running (it wipes the transcript, not the session)
so a loop never silently fires into a cleared screen.
- Tests for adaptive cadence + footer summary.
Second slice of #501. Adds the controller and wires it in:
- /loop command registered (commands.go) and dispatched to handleLoopCommand.
- Loop state on the model (loops, activeLoopID, loopSeq/loopTicking poll guards,
loopNextWake/loopDone self-pace channel).
- An idle poll tick (loopTickMsg, ~1s) fires the earliest due loop only when the
session is idle — mirrors the launchQueuedMessageIfReady guard so loops and a
user's queued prompt never collide; a self-perpetuating tea.Tick that stops when
no loops remain.
- The agentResponseMsg completion seam advances a loop iteration (schedule next
wake for fixed, model delay for self-paced) with doom-loop, iteration-cap, and
done stop conditions, restarting the ticker if other loops remain.
- /loop list|stop|stop all, footer summary + list rendering, and a guard that only
a prompt or custom /command may loop (not a built-in).
Controller tests cover start/advance/self-paced-delay/done/doom/cap/stop/busy.