Notizen aus der Praxis
Hayate Agent No Longer Wakes Up Old Tasks on Its Own
Across several of my agents, tasks from days ago came back after restarts and rollouts. One installation held 197 unfinished old tasks, and 43 saved permissions for old parent tasks rested on one snapshot of unknown workers. Core #337 and #338 now hold unresolved history without proof of ongoing work at upgrade, keep it intact and let an authenticated admin wake exactly one task. Includes a correction to this morning's post, a three-commit test run, and what merged versus what shipped.
冬の宵
古き頼みは
眠らせてWinter evening.
As for the old requests,
letting them sleep.
Old tasks kept coming back. Across several of my agents, work from days ago reappeared after a rollout or a restart, as if somebody had just asked for it again. Core pull request #337 puts numbers on it. In a read-only snapshot taken at about 08:39 this morning, one installation called Peter held 197 unfinished tasks older than a day. 72 of them were marked active, and 46 carried a saved permission to continue on their own. 43 of those permissions were based on one identical snapshot of the same unknown workers, for parent tasks that were between 4.6 and 8.1 days old.
The evidence behind those numbers is persisted state, source reading and a synthetic reproduction, not a full trace of every incident. A task list that never forgets is a good thing. An agent that treats everything on that list as current work is not. Between 09:18 and 09:30, two merges gave Hayate Agent a boundary for exactly this: at upgrade, unresolved history without proof of ongoing work is held and kept, neither resumed nor deleted, and only an authenticated administrator can wake one exact task again.
All times are Europe/Berlin. The window is 11 November, 08:00 to 20:00.
How one unknown worker became 43 permissions
The evidence file that ships with #337, development-evidence/stale-task-reactivation-2026-11-11.md, walks through the chain step by step. Every incoming channel message creates or keeps a durable task, and the list keeps every task, not just the latest one. A later question does not close an earlier task. When the completion check cannot decide whether a task is done, it can keep the task active. Recovery, idle ticks and the boundaries between turns revisit those tasks, and nothing gives them a default expiry.
The weak point was a dispatcher worker without a task ID. The harness treated such a worker as a possible child of every task that had not recorded it as already existing. As a reason to wait, that is conservative. But the reconciliation step sent these workers of unknown ownership to the model, a "continue" verdict set the old parent task active again, and the host saved a permission for it to work on independently. The check for that permission accepted it because a hash of the worker list matched. So one set of workers nobody could assign could seed permissions for many old tasks at once. Peter had six unfinished executions without a task ID.
One more detail shows how old work could become runnable right after new work was done. A newer open topic only postponed some of these admissions. Once the newer topic closed, the older ones became runnable again, and tasks without a topic tag never waited at all. Finishing something new could expose a whole backlog.
The evidence file is clear that this was not an accident of last week's code. The independent admission of old parent tasks was introduced on purpose a week earlier (fd57e02), and existing tests required it.
A correction to this morning's post
The same file names a second cause, and it concerns a change this blog covered this morning. The morning post on core #331 said: "The source change does not revive old tasks: expiries that were already stored are not cleared, because where they came from has to be reconciled separately." That was wrong for one path. #331 made an elapsed polling window turn the task active again for reassessment when no child was running and the task had no expiry of its own, and that also applied to waits saved before the change. Old waits carry no marker that would tell their former meaning apart from new ones. A characterization test in #337 shows it without any inference: an eight-day-old task whose wait ran out seven days ago, with no expiry of its own, becomes active on the next tick.
The stored expiries were indeed not cleared. Those tasks just never had one.
What the boundary does
The rule is written down in docs/historical-recovery-boundary.md. It runs after the state is loaded and before any channel, scheduler or dispatcher can release restored input, and its result is saved first.
- New tasks are marked
currentfrom the start. - An old task without that marker stays current only if real ongoing work proves it: a genuine unfinished message from a channel or the API, or an accepted or running worker whose task ID and original mandate both match.
- Every other unresolved old task becomes
held-legacy. It is blocked, but its original state, checkpoint, deadline, budgets and evidence stay readable. Generated follow-up inputs for it are set aside underheldRecoveryInputsand never replayed. - Held tasks are skipped by startup assessment, ticks, queue reconstruction, model task selection and result-driven continuation. A model cannot enroll historical work.
- A polling window that runs out without a running child now holds the task instead of waking it.
- Workers of unknown ownership never justify an independent admission. A matching hash is not provenance.
Getting a held task back is a deliberate act. An authenticated channel administrator types /task-resume <exact-task-id> in the conversation that owns it, and that one task gets a fresh, independent assessment. The archived input is not replayed, the budget is not reset and no deadline is extended. A stop, an expired real deadline, an exhausted budget, an active session or a failed save still refuses the resume. Nothing is resumed in bulk, marked completed, cancelled or deleted, and the boundary is not an age limit. A task from last week that a running worker still serves stays current, and recovery for current tasks works as before.
I like that the fix does not guess. Age alone would have been the easy rule, and it would freeze real long-running work while waving through anything younger than the cutoff.
Nineteen minutes of opposite rules
The dispatcher side shows how easy it is to build the next resurrection by accident. Core #335 merged at 09:11. It gives internally paused workers (for example after a runtime failure) a persisted retry time, and the scheduler adopts diagnosed legacy pauses and resumes the same execution after a backoff, even across a host restart. That is a good feature for a worker that failed five minutes ago.
Nineteen minutes later, #338 follows up on #337 and on #335 in one PR body: it prevents the dispatcher "from resurrecting old paused/blocked internal failures just because a new runtime has automatic retry support". The host saves an enrollment for every worker before the dispatcher starts. Workers that are really running, and accepted ready workers that are not waiting for a retry, stay current. Historical paused, blocked and retrying entries are kept and fenced. A normal explicit resume enrolls one worker again, under the existing intent and deadline checks.
Tighter task identity also has to work for jobs nobody typed in. At 09:45, an hourly loop on Peter fired, and its dispatch submit failed because the model had left out the task ID and recurring turns had no host task. Core #339 (10:14) gives the first turn of each loop, cron, heartbeat, goal or API job a fresh mandate before it runs, without inheriting an earlier task, its expiry or held old work. Plugins #87 lets the dispatcher take the current task from the host instead of relying on the model to copy an ID. The PR does not say the fence caused the failed loop. It says the operator wants working recurring jobs, "not a weakened context fence".
What a scripted run shows
The AI agent that drafts these changelog posts copied the seven test files as they stand after #338 into three checkouts, installed dependencies fresh in each, and ran them on macOS with Node 22.22.3.
| Commit | Tests passing |
|---|---|
Before #337 (3f8d31e, lock refresh #336) |
11 of 31 |
#337 merge (6c41314, v0.1.233) |
30 of 31 |
#338 merge (52ac9d4, v0.1.234) |
31 of 31 |
Before #337, nine tests fail because Harness.holdLegacyRecovery does not exist yet, and the dispatcher upgrade test fails because ExecutionStore.holdLegacyRecovery only arrives with #338. The other ten get the old answers: new tasks carry no enrollment marker, workers of unknown ownership still trigger a model assessment or an admission, the expired eight-day-old wait becomes active instead of blocked, and status rechecks at depth 19 and 20 end up active. At #337 only the dispatcher upgrade test fails, because the execution store learns to hold historical workers in #338. These are controlled local tests with fake runtimes and temporary state files, not a run on a live agent. The authors report their full suites at 1,848 passed for #337 and 1,865 passed for #338, each with one optional skip and no failures.
Everything else in the window
- Slack and messaging. Core #341 (10:27) and plugins #88 (10:20) let the agent see its active gateways and send an operator-authorized message through the native
plugin_messagetool, with receipts that stop a retry from sending twice. Core #343 (12:12) and plugins #89 (12:13) add per-channel defaults for who may talk to the agent and where it replies, refuse configured policies on adapters that do not support them, and let the agent send one file of up to 10 MiB from its workspace or its own received files. Hidden, foreign and symlinked paths are refused, and an identical send does not upload again. - Core #344 (12:17) fixes a gap found in a final audit of #343: many container agents bind their workspace inside the private agent home, so those files could not be sent. They can now, while sibling stores, hidden folders, sessions and a workspace rooted at
.sshstay private. With #344's updated test file in the #343 tree, the outbound-file and conversation-policy tests pass 6 of 7, and the private-home workspace test fails. At #344, 7 of 7 pass. - Plugins #85 (08:23) makes the Slack gateway finish in-flight sends and uploads before it stops, so a native update can replace it safely.
- Core #334 (08:36) unblocks gateway reloads when adaptive skills are enabled.
- Plugins #86 (09:06) is the status side of #335: a timed automatic retry is reported as routine, not as a request for a human to resume.
- Core #342 (10:24) pins the merged plugin identity for the #339 release. Plugins #90 (15:31) lets the memory consolidation plugin take part in safe updates by fencing and draining its work.
- Core #333, #336, #345 and #346 are mechanical refreshes of the plugins lock.
- Still open, not merged: core #347 moves the completion validator into a provider plugin (last updated 19:54), and core #340 adds native self-updates with an external owner (last updated 10:28).
What merged and what shipped
Core has 13 merges in the window and ten release tags, v0.1.230 to v0.1.239. Plugins has six merges and no releases. WebUI and deployment-base have neither. I checked each tag locally: every one points at a merge from the window. v0.1.233 is the first tag that contains #337, and v0.1.234 the first with #338. v0.1.237 points at the #344 merge f9a0ca9 and is the first tag that contains #343 and #344, but its plugins lock still pins plugins #88. The lock moves to #89 in v0.1.238. The Slack adapter is not among the locked builtin files anyway. #89 says its five Slack module files must ship together as an exact versioned module.
A release tag identifies a source revision. It does not mean any agent runs it. The PR bodies of #337, #338, #343 and #89 say the operator asked for rollout, and that was not verified for this post. The #337 evidence file reports that most installations it inspected at 08:39 still identified themselves as 0.1.229, and that some had moved to 0.1.231 during the analysis. It says nothing about any of them running the boundary. When the window closed, core main was at 63c2fa1 (15:36). Plugins main was observed after the cutoff at 999e011, a merge from 15:31.
On an installation that runs the boundary, last week's tasks are still on the list, with every checkpoint and budget intact. The ones nothing proves alive stay asleep until someone names one.
Sources
- Core #337: https://git.vanillacore.net/harness/core/pulls/337 (merge
6c41314d051d476ed36edae046c05a068a32364f) - Core #338: https://git.vanillacore.net/harness/core/pulls/338 (merge
52ac9d4b023ef96e7f74aee6fd4b677a8757336a) - Evidence file: https://git.vanillacore.net/harness/core/src/commit/52ac9d4b023ef96e7f74aee6fd4b677a8757336a/development-evidence/stale-task-reactivation-2026-11-11.md
- Boundary documentation: https://git.vanillacore.net/harness/core/src/commit/52ac9d4b023ef96e7f74aee6fd4b677a8757336a/docs/historical-recovery-boundary.md
- Core #335: https://git.vanillacore.net/harness/core/pulls/335 (merge
9be19ad62da6d982756ad2222dbc6ce059672cf8) - Core #339: https://git.vanillacore.net/harness/core/pulls/339 (merge
6892f3a541247e3e52a3994a969d5aa9799c0979) - Core #341, #343, #344: https://git.vanillacore.net/harness/core/pulls/341, https://git.vanillacore.net/harness/core/pulls/343 (merge
ee24686ee1bdf381af459a2f8c4f2a1c812c4afb), https://git.vanillacore.net/harness/core/pulls/344 (mergef9a0ca9fe994a7d9695c84487a99ca03c0670022) - Core #334, #342: https://git.vanillacore.net/harness/core/pulls/334, https://git.vanillacore.net/harness/core/pulls/342
- Core #333, #336, #345, #346: https://git.vanillacore.net/harness/core/pulls/333, https://git.vanillacore.net/harness/core/pulls/336, https://git.vanillacore.net/harness/core/pulls/345, https://git.vanillacore.net/harness/core/pulls/346
- Open core #347 and #340: https://git.vanillacore.net/harness/core/pulls/347, https://git.vanillacore.net/harness/core/pulls/340
- Plugins #85 to #90: https://git.vanillacore.net/harness/plugins/pulls/85, https://git.vanillacore.net/harness/plugins/pulls/86, https://git.vanillacore.net/harness/plugins/pulls/87, https://git.vanillacore.net/harness/plugins/pulls/88, https://git.vanillacore.net/harness/plugins/pulls/89 (merge
86853e3a4bbfb083c9171bafa84ebd00fce16dee), https://git.vanillacore.net/harness/plugins/pulls/90 - Releases v0.1.233, v0.1.234, v0.1.237: https://git.vanillacore.net/harness/core/releases/tag/v0.1.233, https://git.vanillacore.net/harness/core/releases/tag/v0.1.234, https://git.vanillacore.net/harness/core/releases/tag/v0.1.237
- This morning's post on core #331: https://samaschke.com/Blog/Post/hayate-agent-coordinator-can-no-longer-take-over-failed-work