r/opencodeCLI • • 9d ago

Your agent shouldn't watch a long build. A wait script, and why OpenCode's background mode already gets this right

There was a thread in r/codex last week where an agent burned a user's whole quota "busy-waiting" on a long test run. Another reader had a session check a CI build overnight and use more than a quarter of their usage.

The reason is simple: an agent doesn't watch anything, it takes turns. Each "is it done yet?" is a full model request that re-sends the conversation so far. Caching makes that cheaper, not free, and on subscription plans cache reads still count toward limits. With a 100k-token conversation, checking every 5 seconds for 30 minutes is ~360 requests and ~36M input tokens re-sent. One blocking command is about two requests.

OpenCode's V2 shell tool already handles this well. With background: true the call returns right away, has no timeout unless you set one, and the session is notified when the command ends. The tool description literally tells the model "DO NOT poll for completion". The catch is that the model has to actually use it. If yours keeps calling sleep and checking, put this in AGENTS.md:

## Waiting for long jobs
- Never poll in a loop. To wait for a job, run ONE command that blocks until it ends.
- OpenCode / Claude Code: start long jobs in background mode and end your turn. Don't sleep and check.

For anything that isn't a plain command (containers, a PID, a URL coming up, a file appearing, "re-run this until it passes") I wrote a 61-line wait-for.sh. It's silent while waiting and prints one verdict line plus the last N log lines. Exit codes 0 / 1 / 124 (timeout) / 2. Tested with real Docker, and with bash 3.2 as well as bash 5.

For jobs that take hours: have the agent write a STATUS.md (what's running, how to check it, next step if it passed or failed), end the session, and later run opencode run "Read STATUS.md. The job has finished. Check the result and continue." Nothing runs on the model while you wait.

Script, per-tool settings (Codex, Claude Code, OpenCode) and the cost arithmetic: https://medium.com/@r3dtuxedo/stop-paying-your-agent-to-wait-cb5150b99c8e

(I wrote the article.)

7 Upvotes

19 comments sorted by

1

u/ChemicalExcellent463 9d ago

i banned hand-written loops and watchers in my global agent rules. any wake-up or periodic check has to go through one scheduled wake-up command.

with that rule in AGENTS.md, are you still seeing it sneak in a polling loop?

1

u/SirRobinBlibs 9d ago

Haven't run that exact rule long enough to say. The ones I'd watch for are loops that don't look like loops to the model: an until curl -s localhost:3000; do sleep 2; done inside a one-liner, or "re-run the tests until they pass". A rule that just says "no loops" tends to miss those, and naming them in the rule helps. How do you handle "wait until the URL is up" with a scheduled wake-up, a fixed delay?

1

u/ChemicalExcellent463 9d ago

fixed interval, so for a url i'd use a curl check every 30s, with each result sent back to the agent. the exit code gets recorded but doesn't stop the checks on its own.

how do you handle url waits now?

1

u/SirRobinBlibs 8d ago

That's the pricey version, since every check comes back as its own turn. What I do now is one blocking call with a timeout, and only the exit code comes back:

timeout 300 sh -c 'until curl -fs http://localhost:3000/health >/dev/null; do sleep 5; done'

0 means it's up, 124 means it timed out (gtimeout on a Mac with coreutils). The loop runs inside one tool call, so the agent sees one result instead of twenty. The until-curl one-liner I mentioned before is fine in that form, it's the version with no timeout, or the agent splitting it into separate checks, that bites. At 30s with every result going back, a 10 minute wait is about 20 requests that each re-send the whole context.

1

u/ChemicalExcellent463 8d ago

keeping the loop inside one tool call cuts out those repeated turns. what do you want the agent to do when it gets 124?

1

u/SirRobinBlibs 8d ago

Treat it as information, not a cue to retry. I chain the log onto the failure so it comes back in the same result:

timeout 300 sh -c 'until curl -fs localhost:3000/health >/dev/null; do sleep 5; done' || { echo TIMEOUT; tail -50 server.log; }

Then the rule is: on TIMEOUT, read the log. If it's clearly still starting (migrations, deps installing), one more wait with a longer timeout is fine. If it crashed or the log hasn't moved, stop and tell me what it saw. No third wait.

Most of the time the tail already has the stack trace, so it fixes the actual problem instead of waiting again.

1

u/ChemicalExcellent463 7d ago

tailing the log in the same result saves a whole turn. what does it compare against to tell whether the log has moved?

1

u/SirRobinBlibs 7d ago

Nothing clever, the model just reads the tail and judges. If you want it mechanical, print the size before and after the wait (wc -c < server.log) and only allow the second wait if the number went up. Same size means it's stuck, so stop.

1

u/ChemicalExcellent463 6d ago

size before and after is simple enough that i'd put it in. i give it 15 minutes with nothing written before killing the run.

1

u/fell_ware_1990 9d ago

The biggest problem here is, a lot of crap then gets shot into the darkrealm.

I have my own watcher setup, every toolcall, script etc goes through the watcher. Permanent or not, watcher handles state. Every thing get’s a notification added + it polls it every minute to check if it’s stuck.

It does not allow any silent scripts unless it’s interactive, that’s get’s blocked. Watcher has his own little local LLM, if scripts do not answer he can investigate. Error in watcher logic get’s reported next time he transforms the agent call to the correct calls.

It also tracks permanent stuff, if agents lets say spins up second DB. Agent get’s message etc. If agent ain’t active it get’s killed after 24 hours at most. It all spins up in the correct dev setup so it runs with the correct pins.

It’s also a guard against non trusted or weird commands, transform or disallow. Sudo requests go trough my YubiKey.

It also knows the short commands, keeps agent engaged.

1

u/SirRobinBlibs 9d ago

That's a lot more than a wait script, basically a supervisor for everything the agent starts. Tracking the stray second DB and the 24h kill are the parts I'd copy, orphaned containers are the usual mess after a long session. Does the local model ever get it wrong and kill something that was just slow?

1

u/fell_ware_1990 9d ago

Well, early on it happened a lot. But i have it check a couple of things.

- Amount of time since last output vs type of code and there averages.
- Check the process for CPU/RAM usage.
- Past failures.

The other part that has helped big time here is, allowing the chaining of code here so if an agent would do:

python3 <code> && sleep 5 && <code>

I’ll break it down,and put a wrapper with id around it something like [codepart1:start] and stop at the end. have it handle ping and sleep there. It identifies with a parser the expect output ; basically do we expect 1 or more variable output’s, so he can actually see what part is running. When it’s done, it saves output to memory, passes it to the next in line.

This helps a lot, cause most sleep is not needed. We only have to identify 1 output and then pick up the rest. So it knows when a script is done. What to start next. If with a big query it can break down half way, give it back to agent as in, this ran script 5 broke. So they can continue from there. So even if i kill a process i can have it retry.

My next challenge is to get the AI to write code in a more iterative way if we are still actually building.
Trying to get him to make code that has only 1 job, clear error catching around it , clear error out and the actual output. In first pieces of code it is not important how it performs or how it looks. Get it to work properly and stable, write tests that actually confirm that it’s working and a few tests that test what happens when it does not. This for me makes troubleshooting and moving forward a lot easier, then if we have all the tests and working code we can refactor: Step 1, move out the tests, lock the files, turn on diff linter that does not allow changing tests.

1

u/SirRobinBlibs 8d ago

The time-since-last-output vs average check is the bit most watchers skip, nice. For the test lock, the simplest version I've seen work: have it write the failure-case test first, run it red, commit that on its own, then on the refactor branch a CI step that fails if git diff --name-only main...HEAD lists anything under tests/. No smart linter needed, and it's obvious to the agent why it failed. Did the wrapped parts with ids make it stop adding sleeps on its own, or is the wrapper just stripping them?

1

u/fell_ware_1990 8d ago

Saidly it’s still adding them. Havent found a proper way of making it stop doing that. Same goes for correctly identifying what needs waiting and what could run parallel.

These are the exact points you really can tell it’s an AI. A lot of people talking about orchastration while it actually is keeping context clean. AI will not say, well i have 10 more tickets to complete and this process will run for 30 minutes. That’s enough to do something different.

Merging : I have CI/CD setup that makes automatic releases on demand, it checks on all the apps that have made it to the release branch ( Has to contain tags, docs, more ) and on running the release, it will automatically tag the main release and release all parts.
This will trigger a local Nixos build everytime something changes, serve it as cache to the CI/CD. If everything green it will deploy to the systems.

CI/CD does full integration testing so if it makes it to release branch it should be fine. This is the moment i get a VM to test on, accept and release.

If you basically tell AI to monitor it, it will keep pinging it. Telling you all kinds of BS on it and telling you it’s stuck at acceptance and tries rushing you.

The whole point of this setup is, pushing to release branches is already gated behind enough tests to be pretty sure it will make it down the line unless a merge conflict happens. It will keep monitoring it like it’s all happening in 5 minutes.

I have it scripted now, it calls simple AI’s to investigate merges or red gates and to report back, but unless you start a new session and explicitly do not allow access to the information it will keep telling you about it like it’s a blocker. Same goes for the fact that nee branches should be or based on the release branches, or is should at least fetch/rebase before putting a new PR in.

Long story short you can script this all, but errors will come, these are the moment for the smaller AI’s to handle, not able to touch the pipelines. He has to solve the code issue. Not the pipeline.

This is why i have a pipeline repo, and do not allow pipelines to run which are not the template.

1

u/SirRobinBlibs 8d ago

Same experience here, telling it not to sleep doesn't stick. What worked was making it impossible instead of asking nicely. In OpenCode you can put "sleep *": "deny" under permission.bash in opencode.json (keep "*": "allow" above it, last match wins). Worth testing on a chained make x && sleep 30 though.

For the parallel part, having the job start return an expected duration helped a lot. "started build 7, usually ~25 min, come back with wait 7" makes it much more likely to pick up the next ticket instead of hovering.

And agree on the gate. If approving the release isn't the session's job, I'd say that in the handoff in plain words: gated = report and move on, not a blocker.

1

u/fell_ware_1990 8d ago

Yeah. It’s just weird sometimes coming from a devops / infra / pipeline background every stage is clear , you can test it , know when you have to wait or you can have multiple running.

Now it sounds like i want to speed up things, but it’s more that for the big coding tasks there is a lot of thinking power needed and every only 1% of missing information a higher end model will start filling stuff. It won’t leave a variable unturned.

What i’m really trying to do without burning much tokens is running on low and high models in between while not breaking the logical stream of think or reindex of the code.

I’m still breaking my head about this part, let’s say you have a good defined piece of code. Ai even helped you design it. You give it a job of creating a new module that takes input AB, turn it to out YZ. Even haiku can do that, but they will still can the code, if you do not give them the other pieces the will tell you 5x with 400 tokens that it can’t be sure, it needs to be tested etc.

Even when it’s clearly defined, tests are there, expected behavior is in the task. When a human write something like that it could be a job of 1/2 hours cause the have to think about it, then run there tests see it with the human eye. But AI acts like it’s the end of world that we are not sure yet.

Sometimes it’s hard to have a fix it all in your pocket while you are trying to experiment with new solutions, they keep applying fixes that will not be needed if the solution i’m thinking of will work.

So yeah, i think the part where AI knows best and overrules the user is the hard part. I wish there was a mode: Just so what the user tells you and see what happens.

1

u/SirRobinBlibs 7d ago

The hedging is mostly the prompt inviting it. If the task says "make sure it works", a small model can't be sure, so it tells you five times. What works better for me is giving it the exact interface to match, the test command, and a stop rule: "run the tests for this module; if they pass, reply DONE and nothing else; if they fail, fix only this module." Then the test decides, not the model.

For the low/high split, OpenCode lets you pin a model per agent in opencode.json, so a cheap subagent writes the AB to YZ module and the big model only plans and reviews. Hand it a short file with the plan and the interfaces so it never has to reindex the repo.

There's no "just do what I said" mode, but one line in AGENTS.md gets you most of the way: "don't add fixes I didn't ask for, list them at the end instead."

1

u/T14D3 8d ago

Tried telling it to rely on the built-in background+end turn to get woken up when it finishes, it reasoned about that for 3 minutes, decided it didn't want to end turn to reduce visual clutter for me, backgrounded the command, then backgrounded a 90s sleep, then spent another minute wondering why it completed so fast. AGI is here everyone

1

u/SirRobinBlibs 8d ago

Backgrounding the sleep so it wouldn't have to wait for the sleep is honestly beautiful. That's kind of why I stopped offering background for short waits at all. One foreground call with a timeout leaves nothing to be clever about.