Drowning in "Job Finished" Notices? Hermes Now Coalesces Ready Background Completions Into One Turn


You start three jobs at once: one running the test suite, one pulling dependencies, one generating assets in the background. You go to a meeting. When you come back, the screen is a wall of “job finished” — and each one kicked off its own model turn, when all you wanted was “they are all done, carry on”.

The problem was never the notifications themselves; it was that every notification counted as the start of a separate conversation. A recent change tightens that: background completions that are already ready are now coalesced into one notification turn on interactive surfaces.

The new behavior: ready completions become one turn

The rule is short. Conversation surfaces coalesce consecutive background-process completions that are already ready for one conversation into a single notification turn. Failures stay in the batch alongside successful output — bad news does not get carved out. And a single completion keeps its original text instead of being wrapped in a batch envelope.

One clarification, because it is easy to misread: this is not delayed batching. The system does not hold an early finisher back so more can join it. It only combines what is already ready at that moment, and it explicitly does not promise to combine jobs that finish at different times. So you will not see a test that finished long ago sitting on a notification instead of waking the agent.

What does not get coalesced

The batching covers background process completion events. Two things are deliberately left outside it:

  • Watch results (subscriptions to output changes) and async delegation results stay separate notifications, delivered in their original order.
  • The messaging gateway — the bot answering you on Telegram, Slack and friends — keeps its own route-keyed short-window batching. This change does not replace that mechanism or alter adapter sends.

Scripted use is unaffected too: noninteractive consumers such as an API request, an ACP client, or a one-shot CLI call do not gain any new autonomous notification loop.

Consumed processes no longer notify

This is the most practical part of the change and the easiest to miss.

Process identity stays available until dispatch. So if you have already taken a background process’s result explicitly — reading its log through process_manage, waiting on it, or killing it — the CLI will not append a completion notice afterwards, even if that process has left the registry and is sitting in the input queue. A batch that was entirely consumed starts no turn at all.

In other words: going to look at the result yourself is treated as telling Hermes “I already know about this one”.

Who owns this

The same behavior lives in different modules per surface. Knowing the layout saves time when something looks off:

Surface Module What it does
Classic CLI hermes_cli/cli_process_notifications.py Owns the idle/post-turn drain, compression-aware ownership, and final input unwrapping
TUI and Desktop tui_gateway/session_notifications.py Groups the poller’s ready snapshot after checking ownership; each process still emits its own UI status; busy sessions requeue structured events rather than rendered batch strings
Post-turn TUI tui_gateway/prompt_turn.py Same routing and rendering path; desktop and dashboard chat clients share this backend

The shared renderer is ProcessNotificationBatch in tools/process_registry_notifications.py. Two design constraints are worth remembering:

  1. Neither the batch nor its delivery status is persisted into the system prompt. The noise never enters the model’s context — which is exactly the half of this that saves tokens.
  2. Addressed events need a provable owner. Another live session cannot adopt them. Delegation delivery continues through its own durable claim/complete ledger: once per delegation rather than once per process batch.

Verifying it yourself

There is a probe in the repository you can run locally:

python3 evals/completion_backlog_probe.py /path/to/repo /tmp/out.json

It starts real shell children in temporary directories, reads their real completion events, and drives the production CLI, the TUI poller and the post-turn notification routes (a loopback HTTP turn sink stands in for actually submitting a prompt). It records real dispatches but does not exercise model inference.

Mind what it measures: turn admission at the notification boundary, not token savings. The two are related but not the same — fewer batches means fewer turns and shorter context, but the probe will not compute the characters you saved.

The short version

The value here is not a shiny feature; it is noise landing in a predictable shape. Ready completions become one turn, failures stay, consumed results do not come back, watches and subagents keep their own order, and batches never reach the system prompt. If you habitually fire off several background jobs, the difference after upgrading is simple: one summary is waiting, and you carry on.

Background work usually starts with scheduled jobs — see the hermes cron reference. If the scheduler itself is misbehaving, cron doctor is the closer read. And for how subagents are controlled and delivered in real time, see live subagent orchestration.