Skip to content

14 hand roles stop ticking when librefang requires an explicit [autonomous] declaration (refs librefang/librefang#6595) #104

Description

@houko

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions