Notizen aus der Praxis
My Agent Doesn't Get to Guess After a Restart
When a restart interrupts a job and my harness cannot tell what comes next, the job used to stop without a word in the chat. Letting the agent simply carry on would risk repeating a deployment or a sent mail. Four core changes merged Thursday night take a third path: up to three tries, then the job is blocked and one message names the cause, the known state and the decision needed. Merged and released; whether any running agent has them is not verified.
Say I give one of my agents a job in the chat, and the VanillaCore Agent Harness it runs on restarts halfway through. When the Harness comes back, it has to work out by itself whether the job is done and what the next step is, and it asks a separate completion check for that. Until Thursday evening, if that check failed or came back without a usable answer, the Harness blocked the job on the spot, and nothing reached the chat. A job whose step budget ran out ended the same way, with a line in the Harness log as the only trace. From the chat, both look like an agent that simply went quiet, and I would only find out when I went looking for a result that never came.
The obvious fix is to let the agent carry on after the restart. That is exactly what I don't want it to do. A resumed job can repeat something that already happened, a deployment or a sent mail, because the Harness cannot undo or deduplicate arbitrary tool effects. Retrying the check forever is no better. So my harness now does something deliberately boring: it tries the check up to three times, at least 60 seconds apart, and if it still has no safe next step, it blocks the job and queues one message to the conversation that names the job, what is known and which decision it needs. I would rather answer a question than clean up after a guess. Four pull requests in core, #157 to #160, merged between 20:17 and 23:20 on Thursday, build that path, together with a matching message for a job that has used up its step budget. Whether any running agent already has them is a separate question, and I answer it at the end.
What the message says
The message is the part I care about most, and #160 rewrote it. The Harness writes these notices in German. Translated, the notice after three attempts without a usable answer now reads like this:
Recovery halted: The bounded recovery assessment budget is exhausted or its saved state is invalid; a verified next step is not available. Task: (the original request) Verified state: The task and continuation depth 2 are saved. A matching, executable next step is not secured; completion is not confirmed. No automatic continuation is started. Decision needed: Should this task be continued, or is no further work wanted? Please reply with the concrete next step within this task (check existing results first, do not blindly repeat effects), or confirm explicitly that no further work is wanted. Without this clarification, the task stays blocked.
Before #160, the notice that #158 had introduced less than an hour earlier was a single English line such as "Recovery assessment unavailable or bounded attempts exhausted; operator reconciliation required.", followed by the task. That names the job, but not what is known or what I am supposed to do.
The first line names one of six causes. In the new retry path, three empty or unusable answers produce the wording above, and if the last attempt throws an error, the notice says that the call to the check failed. A missing check configuration and a saved go-ahead to resume that no longer matches the job or its next step get their own wording. The remaining two, an interrupted check and a check without a usable answer, cover jobs saved in that state before the retry counter existed. For a failed call, the notice says that the technical cause was not verified, and the tests assert that neither the raw provider error nor a made-up missing-permission claim ends up in the chat.
Each job gets this message once. The notice and a marker on the job are saved together before delivery, and a restart does not create a second one. Delivery retries reuse the same ID; whether the chat itself drops a duplicate depends on the channel. If the conversation has no chat address and the Harness has no home address, there is no message at all: the job is blocked, and the marker can only be found in the local state.
Three tries, then the question
#159 is the change that keeps a single hiccup from ending a job. Before it, one failed or empty answer from the completion check blocked the job on the spot, and the implementation notes in the pull request describe reproducing exactly that against the unchanged code. Now each attempt is saved before the check is called, together with an earliest retry time 60 seconds later, and the count survives restarts. An attempt that gets cut off counts as used. After the third attempt without an answer, the job is blocked and gets its message. In between, the job keeps its authorization, but nothing executes until there is a concrete next step.
#158, merged 36 minutes earlier, makes the good path survive a crash. When the check does return a next step, the Harness saves that decision together with a permit for exactly one resumed step: the job, its depth, the exact next step, the original request and its deadline. If the process dies between the decision and the resumed step, the restart uses the saved permit instead of asking the model again. Right before the step is queued, the Harness checks the job as it is now. A job that was stopped, completed or expired in the meantime, or that waits on another job, does not run. If the saved permit no longer matches the job's depth, next step, original request or deadline, the step is refused and the job gets the "no longer matches" message instead. The resumed step itself starts with the instruction to check what already happened before doing anything, because that is all the Harness can do about repeated effects.
The other dead end: an empty budget
#157 covers a different way a job stops. Every job has a budget of 20 productive steps, and that number did not change. When a job ran out, the Harness blocked it and wrote a line into its own log, and the chat heard nothing. Now the job keeps its planned next step, and I get one message, translated: "Task (id): action budget exhausted; open work will not be continued automatically. Status queries of running dependencies remain possible until the existing deadline. Further work requires a new explicit operator request."
Jobs that were already blocked this way before the update get the same message on an idle tick, without another model call. The budget is not reset, and a new request from me starts a new job instead of reviving the old one. #159 handles its own leftovers differently: jobs that recovery had already blocked before the update stay as they are.
An answered question stays answered
#160 also fixes a quieter mistake. If I ask a plain question and get the answer, that turn is done. But the completion check's verdict "completed" was thrown away, so the answered question stayed on record as an open job, which is exactly what recovery goes looking for after a restart. Now a positive "completed" verdict closes the job, but only for a real foreground turn with an actual reply. It never overrides an explicit checkpoint such as waiting or blocked, and at startup, where no reply exists, the Harness does not infer completion. In the test, the question is "Was ist zwei plus zwei?" and the answer is "Vier." Before #160, that job came back as still active.
What I checked
For each of the four pull requests, I copied its new or changed test files onto the commit right before it and ran them there, then again at the merge. #157 on v0.1.105: 3 of 7 passed and 4 failed; at the merge, 7 of 7 passed. #158 on v0.1.106: 4 of 11; at the merge, 11 of 11. #159 on v0.1.107: 1 of 8 passed, 4 failed and the test runner cancelled 3 with promises still pending; at the merge, 8 of 8. #160 on v0.1.108: 9 of 16 passed and 7 failed, among them the answered question that stayed active; at the merge, 16 of 16. At the end of the window, all 42 tests in those 10 files passed together. These are controlled local tests: they run fake runtimes, and three of the files also save and restore real state files in a temporary directory. None of it is a restart of a live agent.
The rest is the authors' report. #157 names 59 relevant tests and probes passing, #158 102 focused tests, #159 78 focused plus 64 lifecycle tests, and #160 65 focused tests, each with typecheck and build passing. None claims a passing full suite: #157 and #158 report two and three failures that also occur on the unchanged base, and the full runs for #159 and #160 stopped at their time limits of 240 and 120 seconds with two failures each. Every one of the four pull requests was open for less than a minute before it was merged, and the formal reviews they cite are the authors' report, which I did not re-run.
What merged and what shipped
This post covers 29 October 2026, 20:00, to 30 October, 08:00, Berlin time (CET), which is 19:00 to 07:00 UTC. Core merged four pull requests, and each was released within ten minutes: #157 at 20:17 in v0.1.106 at 20:24, #158 at 22:28 in v0.1.107 at 22:34, #159 at 23:05 in v0.1.108 at 23:14, and #160 at 23:20 in v0.1.109 at 23:24. In my clone, each tag resolves to the merge commit of its pull request. #160 carries two commits, the fix and a test adjustment that keeps the retries from #159 intact. The plugins and deployment-base repositories merged nothing in this window.
A release tag is not a deployment. #158 to #160 describe their delivery as a scheduled GitOps rollout, #157 says nothing about it, and the recovery documentation states that a merge or build does not establish that a running installation uses the feature. Nothing here says which version any agent runs.
Where I land
A question in the chat costs a minute. A job that stopped at midnight without a word costs the morning someone spends finding out that it stopped. These four changes take the cheaper side: try a little, then stop and ask once, with enough context to answer from the chat. If you build agents that resume work on their own, that is the part worth copying: a hard cap on retries, a saved count that survives restarts, and one message that names the cause, the known state and the decision you need. What still bothers me are the corners these four changes do not reach. Without a chat address, a blocked job leaves only a marker in the state file. Jobs that recovery blocked before this update stay blocked without any message. And a job whose wait ran past its deadline, or whose saved wait is malformed, is still blocked with nothing but a log line. Those are the gaps I want closed next.
Sources used
- VanillaCore Harness core pull request #157, "fix: persist budget-exhaustion escalation and blocked next step", merged 29 October 2026: https://git.vanillacore.net/harness/core/pulls/157
- Core pull request #158, "fix: persist retained task recovery admission and escalation": https://git.vanillacore.net/harness/core/pulls/158
- Core pull request #159, "fix: bound and persist retained task assessment retries": https://git.vanillacore.net/harness/core/pulls/159
- Core pull request #160, "fix: explain recovery decisions and retain completed answers": https://git.vanillacore.net/harness/core/pulls/160
- docs/work-recovery.md, docs/task-recovery.md and docs/budget-attention.md at the window-end commit 4f1b160: https://git.vanillacore.net/harness/core/src/commit/4f1b16085cc8930313ff4d7a1bde8420f16108e8/docs/work-recovery.md
- Releases v0.1.106 to v0.1.109: https://git.vanillacore.net/harness/core/releases/tag/v0.1.109
- Public organization inventory: https://git.vanillacore.net/harness/