What is changing upstream
librefang/librefang#6595 reported that a hand role declaring max_iterations — a per-turn tool-loop depth cap — silently became a 30-second autonomous tick loop. librefang/librefang#6603 attempted a fix but did not resolve it: the merged predicate still keys on manifest.autonomous.is_some(), which is true for a role that declared nothing but max_iterations, because that value is carried in a synthesized AutonomousConfig. The issue has been reopened.
The follow-up change requires an explicit declaration before a hand role leaves ScheduleMode::Reactive:
- an explicit
schedule on the role — honoured verbatim, and the only route to Periodic / Proactive; or
- an explicit
[autonomous] block on the role — yields continuous ticking at that block's heartbeat_interval_secs.
Two consequences for hands in this registry:
max_iterations alone no longer starts a loop. It keeps working exactly as its name says — the agent-loop iteration cap — so there is no reason to remove it from any hand.
[metadata] frequency does not control scheduling. It never did in the kernel; #6603 briefly made it load-bearing and the follow-up reverts that, so it returns to being catalog-display metadata.
Audit of this registry
Run against ff697677 (current main), parsing every hands/*/HAND.toml per role. No hand in this registry currently declares an explicit [autonomous] block or an explicit schedule on any role. So every role that ticks today does so through the implicit path, and these 14 will stop:
| hand |
role |
[metadata] frequency |
max_iterations |
analytics |
main |
continuous |
60 |
apitester |
main |
continuous |
60 |
browser |
main |
continuous |
60 |
collector |
main |
continuous |
60 |
devops |
implementer |
continuous |
80 |
devops |
main |
continuous |
60 |
lead |
main |
continuous |
50 |
linkedin |
main |
daily |
50 |
predictor |
main |
continuous |
60 |
reddit |
main |
hourly |
50 |
researcher |
main |
continuous |
80 |
strategist |
main |
continuous |
60 |
trader |
main |
continuous |
80 |
twitter |
main |
hourly |
50 |
No role starts ticking that did not before.
Two things worth noting about the shape of that list:
- With one exception it is entirely
main coordinator roles. Sub-agent roles (researcher/scholar, devops/monitor, devteam/qa, …) never carried max_iterations, so they were already reactive and are unaffected. devops/implementer is the only non-coordinator on the list.
clip, creator, and all four wiki roles declare max_iterations but sit at frequency = "on-demand", so #6603 already stopped their loops. They are not in the table because they do not tick on current upstream main either.
What needs deciding, per hand
For each row: was the loop intended?
librefang/librefang#6595 argues it was not, at least for researcher, quoting the author comment sitting next to the value:
[agents.main]
max_iterations = 80
# Raise the history cap above the kernel default. Deep research workflows do
# extensive web_search → web_fetch → summarize
# loops with multi-source synthesis: 80 iterations × ~4 messages each
# easily produces 200+ messages per user turn.
max_history_messages = 120
That reads as loop depth for a single user turn, not a wake-up cycle. If that is the intent for a given hand, nothing needs to change — the follow-up makes the behaviour match the comment.
Where a hand genuinely should wake itself up, add the declaration explicitly:
[agents.main.autonomous]
heartbeat_interval_secs = 300
max_iterations = 80
heartbeat_interval_secs is the tick interval, so this is also the opportunity to pick a real cadence rather than inheriting the 30-second default that the implicit path used. A monitoring hand almost certainly does not want to wake every 30 seconds; linkedin at frequency = "daily" and reddit / twitter at hourly were ticking every 30 seconds regardless, since frequency was inert.
Both the flat agent format and the nested [model] format accept the block — the follow-up fixes a separate defect where the flat format (which every hand here uses) silently dropped schedule, [autonomous], and [exec_policy] because LegacyHandAgentConfig has no deny_unknown_fields and is tried first during parsing.
Also worth knowing
frequency = "reactive" now parses. It was not a valid HandFrequency variant and produced a TOML parse error, which librefang/librefang#6595 notes is a natural thing to reach for when trying to stop unwanted ticks. It is accepted as an alias for on-demand.
Separately, a hand that lists shell_exec in tools no longer receives exec_policy.mode = "full" at activation — it inherits the operator's global [exec_policy] instead (librefang/librefang#6594). A hand that genuinely needs elevated exec must declare its own [exec_policy], which is respected. Nothing in this registry declares one today, so every hand here now follows whatever the operator configured.
Happy to send the PR adding [autonomous] blocks once you have decided which hands should keep their loops and at what cadence.
What is changing upstream
librefang/librefang#6595reported that a hand role declaringmax_iterations— a per-turn tool-loop depth cap — silently became a 30-second autonomous tick loop.librefang/librefang#6603attempted a fix but did not resolve it: the merged predicate still keys onmanifest.autonomous.is_some(), which is true for a role that declared nothing butmax_iterations, because that value is carried in a synthesizedAutonomousConfig. The issue has been reopened.The follow-up change requires an explicit declaration before a hand role leaves
ScheduleMode::Reactive:scheduleon the role — honoured verbatim, and the only route toPeriodic/Proactive; or[autonomous]block on the role — yields continuous ticking at that block'sheartbeat_interval_secs.Two consequences for hands in this registry:
max_iterationsalone no longer starts a loop. It keeps working exactly as its name says — the agent-loop iteration cap — so there is no reason to remove it from any hand.[metadata] frequencydoes not control scheduling. It never did in the kernel;#6603briefly made it load-bearing and the follow-up reverts that, so it returns to being catalog-display metadata.Audit of this registry
Run against
ff697677(currentmain), parsing everyhands/*/HAND.tomlper role. No hand in this registry currently declares an explicit[autonomous]block or an explicitscheduleon any role. So every role that ticks today does so through the implicit path, and these 14 will stop:[metadata] frequencymax_iterationsanalyticsmaincontinuous60apitestermaincontinuous60browsermaincontinuous60collectormaincontinuous60devopsimplementercontinuous80devopsmaincontinuous60leadmaincontinuous50linkedinmaindaily50predictormaincontinuous60redditmainhourly50researchermaincontinuous80strategistmaincontinuous60tradermaincontinuous80twittermainhourly50No role starts ticking that did not before.
Two things worth noting about the shape of that list:
maincoordinator roles. Sub-agent roles (researcher/scholar,devops/monitor,devteam/qa, …) never carriedmax_iterations, so they were already reactive and are unaffected.devops/implementeris the only non-coordinator on the list.clip,creator, and all fourwikiroles declaremax_iterationsbut sit atfrequency = "on-demand", so#6603already stopped their loops. They are not in the table because they do not tick on current upstreammaineither.What needs deciding, per hand
For each row: was the loop intended?
librefang/librefang#6595argues it was not, at least forresearcher, quoting the author comment sitting next to the value:That reads as loop depth for a single user turn, not a wake-up cycle. If that is the intent for a given hand, nothing needs to change — the follow-up makes the behaviour match the comment.
Where a hand genuinely should wake itself up, add the declaration explicitly:
heartbeat_interval_secsis the tick interval, so this is also the opportunity to pick a real cadence rather than inheriting the 30-second default that the implicit path used. A monitoring hand almost certainly does not want to wake every 30 seconds;linkedinatfrequency = "daily"andreddit/twitterathourlywere ticking every 30 seconds regardless, sincefrequencywas inert.Both the flat agent format and the nested
[model]format accept the block — the follow-up fixes a separate defect where the flat format (which every hand here uses) silently droppedschedule,[autonomous], and[exec_policy]becauseLegacyHandAgentConfighas nodeny_unknown_fieldsand is tried first during parsing.Also worth knowing
frequency = "reactive"now parses. It was not a validHandFrequencyvariant and produced a TOML parse error, whichlibrefang/librefang#6595notes is a natural thing to reach for when trying to stop unwanted ticks. It is accepted as an alias foron-demand.Separately, a hand that lists
shell_execintoolsno longer receivesexec_policy.mode = "full"at activation — it inherits the operator's global[exec_policy]instead (librefang/librefang#6594). A hand that genuinely needs elevated exec must declare its own[exec_policy], which is respected. Nothing in this registry declares one today, so every hand here now follows whatever the operator configured.Happy to send the PR adding
[autonomous]blocks once you have decided which hands should keep their loops and at what cadence.