DEV Community

Lisandro Reinoso
Lisandro Reinoso

Posted on Originally published at first-class-blog.vercel.app

"Day three: the loop leaves a trace"

Day one I built the agent loop. Day two, the session record. Day three I ran ./run-ticket.sh US-04 and nothing happened: no error, no output, no ticket executed. The loop had been running for two days, and that early morning, for the first time, there was no way to see it.

A script that said nothing

The cause was small and a little embarrassing. run-ticket.sh looks up the ticket by file name, but the sprint's twenty user stories lived in 01-Stories/20260815Sprint/ as NN_slug.md, without the US- prefix. grep -i "US-04" found nothing, and with set -euo pipefail active the find | grep | head pipeline failed and killed the script before it could print the error message I had written myself. The bug wasn't just US-04's: it affected all twenty stories in the sprint equally.

The fix added a second content-matching attempt — look for the # US-04 — ... heading inside the file when the name isn't enough — and neutralized pipefail with || true on both pipelines, so the existing error block would actually run when it should. Verification turned up a second bug inside the first one: without anchoring the search to the heading, US-01 matched TECH-011_smtp_resend.md because that ticket mentioned "Discovered during US-01" in its body. It was fixed by anchoring the grep to ^# US-01. I closed that session at 03:06 without having run US-04 end to end — fixing the matching wasn't the same as invoking the whole loop, and that was left for later.

One file per run

The next session, that same early morning, tackled something different: the loop ran, but its output only lived in the terminal. If it got cut off, if I wanted to compare two runs of the same ticket, or simply know how much a run had cost, there was nowhere to look. The decision was to save each run in its own timestamped file — history/<TICKET_ID>_<timestamp>.json — instead of a fixed file per ticket that each new run would overwrite. That night I was already generating some of those files by hand, without a timestamp, running real tickets in parallel; I was asked whether I'd rather align the convention to that, and I chose to keep the timestamp anyway, so as not to lose the history of repeated runs of the same ticket.

The implementation was a tee at the end of the pipeline: claude ... | tee "$HISTORY_FILE", verified with bash -n and an isolated simulation of the pipe, without invoking claude for real so as not to spend money on a test. I closed that session at 03:46. The first run under the new convention, US-06_20260817-034659.json, started that very minute.

What the record saw that same night

From then on, every run left a trace, and that same night the new record captured something that used to get lost. Between that early morning and the following night, 29 valid runs were left in history/ — close to 500 turns in total, around USD 55 — 20 finished DONE and 5 BLOCKED.

US-11 was the one that showed the most. It ran four times: two early BLOCKEDs without even reaching the coder, a third one cut off at 49 turns by "Credit balance is too low" — there, the coder had touched database.types.ts, a file generated by Supabase, and along the way had broken a flow from another already-finished user story; the coordinator caught it and returned the ticket before the credit ran out — and a fourth, with a narrow fix this time, that finished DONE. The ticket was still marked blocked in the record anyway: a false positive from run-ticket.sh itself, which looked for the word "BLOCKED" anywhere in the text instead of in the status line, and which was fixed by hand after documenting it. The system that finally let me see the loop was born with its own reading bug.

What stayed

Today, a month later, the three decisions from that early morning are still intact in the template I copy into every new project: pipefail active, the heading fallback, tee writing to history/. That's more than 500 files spread across eight projects. Every run of the loop — including the one that wrote this post — leaves a record in some history/, and that's where, among other things, the cost of each one comes from.

Day three didn't add a new feature to the product. It added the ability to look back and know what had happened. What do you keep from every agent run, and who reads it afterward?

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

"The system that finally let me see the loop was born with its own reading bug" is the most honest sentence I have read about agent observability this month. A substring match for BLOCKED instead of the status line is exactly the class of defect a written record makes visible and a terminal scrollback never does — you only find it because you went looking in the files.

What I took from it: the record paid for itself twice in one night. It gave you the 49-turns credit cut-off and it gave you the generated-file collision, which is the one I keep getting bitten by — a run edits something produced by another tool, and nothing in the transcript says "this path is derived, do not touch it". Did you end up encoding that in the record, e.g. a guard that fails the run when it wrote to a path in the generated set, or is it still a human reading the history afterward?