Back to all writing

Field notes

Starting Over in Hayate Agent Now Stops the Old Work

Starting a fresh conversation with Hayate Agent used to leave the old tasks, timers, workers and scheduled jobs running. Two fixes make a fresh start stop that work first, and the hard part is deciding which scheduled jobs a conversation actually owns.

Starting Over in Hayate Agent Now Stops the Old Work

冬の宵
新たと言えば
火も消える

Winter evening.
When you say "new",
the fire goes out too.

An operator wanted to start over with an AI agent installation called Minerva in a Matrix room and used the agent's command for a fresh conversation. The agent did what that command promised at the time: it forgot the old context, aborted the running turn and dropped the waiting messages. The old tasks, their timers, their child workers and the conversation's loops kept going in the background, and so did scheduled jobs that had been created from that same room. The reply even said so: "Context reset does not cancel this work or recurring sources", followed by a pointer to /stop and to managing recurring sources separately. That was honest and documented, and it was still the wrong behavior for someone who asked for a clean slate.

I like how the two fixes answer this, because the easy version would have been dangerous. Stopping the tasks of a conversation is the simple part. The hard part is deciding which scheduled jobs belong to that conversation, since a job that only delivers its report to the room is not the room's job, and stopping it would silence someone else's work. In Hayate Agent, core pull request #422, which its author calls an "explicit operator correction", makes the fresh-start command, /new, revoke the old work before the fresh context opens. Follow-up #423 adds the scheduled jobs and refuses to guess when ownership is unclear.

Hayate Agent cover on a dark indigo background with a faint wave pattern at the bottom: small slate 疾風 logo with the label Hayate Agent, then a two-line red Japanese headline, Hayate Agentでやり直すと / 古い作業が止まるようになった (when you start over in Hayate Agent, the old work now stops), above the white English headline Starting Over in Hayate Agent Now Stops the Old Work, and github.com/hayate-agent at the bottom

All times are Europe/Berlin. The window is 13 November, 08:00, to 13 November, 20:00. #422 merged at 18:35 and #423 at 19:55.

What a fresh start used to keep

Before #422, /new and its alias /reset were context commands. They opened a fresh runtime history in the same conversation, aborted the parent turn if it was running and discarded the parent queue. Everything durable stayed: continuations with their wait timers, Dispatcher executions and subagents, the conversation's loop, heartbeat and standing goal. The reply listed that remaining work and pointed to /stop, which revoked the tasks and children but, by its own reply text, left recurring sources enabled. No single command ended everything a conversation had started, and cron jobs were outside both.

The old design had a reason. A context reset that silently keeps an authorized task alive is useful when a long task outgrows its context. That use now belongs to the separate handover and context maintenance mechanisms, and docs/conversation-reset.md at #423 says plainly that the new command "intentionally replaces the former context-only reset".

What #422 stops

/new now runs the existing scoped stop before it opens anything new. It aborts and drains the old parent turn, discards its queue and its late reply, persists stops for durable tasks and continuations (including waiting and blocked ones), and revokes the conversation's Dispatcher workers and subagents. It clears the loop, heartbeat and goal, disables the inherited heartbeat for that conversation, and drops the old retry input, recent prompt context and the hashtag reference cache. A handover that was already prepared for the old context is cancelled, so it cannot seed the old work into the fresh one.

The order matters more than the list. The reset marker is written to disk before anything is interrupted, and new context is never opened while the old parent turn is still running. If the reset fails halfway, it does not report success: the operator retries /new, and after a restart the host finishes a persisted reset before it admits any worker. Late results from revoked workers cannot restart the old mandate. The TUI uses the same path, and the private same-user operator socket accepts one exact reset-conversation request, so an operator can honor a reset that was issued before the fix without inventing a user message.

The scheduled jobs that stayed behind

The #423 author writes that after #422 the old continuations and parent records of that conversation were stopped, and that five enabled legacy cron jobs created from the same room were still there. That is an author report; this post did not check the installation.

#423 binds a cron job to a conversation only through creator provenance. The job must carry the user id captured when it was created, and its creation platform, chat and thread (if there is one) must match the conversation exactly; the user id itself is required but not compared, so it does not separate two users in the same room. A job whose delivery target merely points at the room is not owned by it, and the tests check that a delivery-only job, a job from another thread and a foreign job get no pause override. Owned jobs are paused with a persisted override, not deleted. Their running script gets aborted and drained within its existing execution deadline, and their own cron:<name> sessions and children are reset as well.

When ownership is ambiguous, for example two jobs with the same name created from different chats, the reset fails closed. The test for that case checks that no override was set, the saved conversation state is unchanged and no fresh context was opened. I prefer that failure to a guess, because a reset that pauses the wrong job does damage that nobody asked for, while a refused reset is visible and can be retried.

Infographic on a dark indigo background with a faint wave pattern at the bottom. Red headline What /new does now, subtitle in the conversation where it is typed (core #422 and #423). Three columns. Stopped: old parent turn, drained; queued messages; tasks and continuations, incl. waiting ones; Dispatcher workers and subagents; loop, heartbeat, goal; prepared handover; retry input and context. Paused: cron jobs created from this conversation (platform, chat, thread, captured user id); running script drained; override saved to disk, job not deleted; their cron sessions reset. Untouched: jobs that only deliver to this room; other threads and other conversations; independent schedules; global memory; archived evidence; effects already done (nothing is undone). An outlined box below reads Owner unclear: reset refused, nothing paused, state unchanged, no fresh context. Footer: Fresh context opens only after the old turn has drained. Source: core #422, #423, docs/conversation-reset.md at d9538c1

The same PR fixes a second problem the author found on Minerva: about 75 MB of state and repeated V8 out-of-memory crashes at 2 and 3 GiB, caused by full snapshots that queued up before each disk write. A new StateStore.capture builds each snapshot only when its turn in the write queue comes up, and every caller still waits for its own write; an unchanged snapshot skips the redundant disk write.

What the tests show

The AI agent that drafts these changelog posts ran the final versions of all eight test files that #422 and #423 added or changed, as #423 left them, at three commits: the #414 merge just before #422 (v0.1.295), the #422 merge (v0.1.296) and the #423 merge (v0.1.297). These are controlled local tests, mostly with a fake runtime, and some drive a real script and real files in a temporary folder. They are not a live system.

Commit 41 final tests
Before #422 (v0.1.295) 18 pass
#422 (v0.1.296) 28 pass
#423 (v0.1.297) 41 pass

Before #422, 23 tests fail: the conversation revocation, chat and TUI reset, prepared handover and hashtag cache cases, the eleven cron ownership cases and the two snapshot capture cases. At the #422 merge, only the eleven cron cases and the two capture cases still fail, which is exactly the part #423 adds. Each commit's own version of the files passes in full: 24 tests before, 28 at #422 and 41 at #423.

The #422 and #423 descriptions report full-suite runs of 2,353 passing tests with one skip and 2,366 with one skip. Both say the fix is not yet verified on installed hosts. Neither claim was rerun for this post.

What merged and what shipped

v0.1.296 resolves to commit 505e4b4, the #422 merge, and was published at 18:46. v0.1.297 resolves to d9538c1, the #423 merge, and was published at 20:02, two minutes after the window closed. A release tag identifies the tagged source commit; it says nothing about which installations run it, and both PR descriptions say activation is still pending.

#422 and #423 were the last two of 33 merges in the window, 28 in core and 5 in plugins, with 23 core releases from v0.1.274 to v0.1.296. #421, merged at 17:24 and first contained in v0.1.294, makes the first worker of an execution recoverable after a restart, in addition to the later handovers that were already covered. When core was fetched at 20:21, main was at the #389 merge from 20:04, outside the window.

Limits

/new stops and pauses; it does not undo. External effects that old work already performed stay done, and archived task and effect evidence is kept. Other conversations, other threads, independent installation schedules and global memory are untouched by design. Cron jobs that never had a captured user id have no creator provenance and are not paused. Cron scripts are not isolated as a process tree, so processes a script started independently may survive its abort. A cooperative script stops quickly, while one that ignores the abort can take its full existing execution deadline, and the reset is not acknowledged before that drain finishes. An ambiguous ownership case leaves everything running until someone resolves the name collision.

Verdict

A command called "new" should not leave the old conversation working in the background, and #422 and #423 make it mean that for everything Hayate Agent can prove the conversation started. The careful part is what it refuses to touch: a job is the conversation's only if the conversation created it, and when that is unclear, nothing gets paused.

Sources used