Skip to content

Feral v2026.08.01

Choose a tag to compare

@github-actions github-actions released this 01 Aug 12:14

Reliability on long tasks. Every fix here is something you only hit after the
agent has been working for a while — which is exactly when it hurts most.

This is the first release measured against a walk-away benchmark: real tasks run
end to end, unattended, nine times each. It is why several of the entries below
name a number instead of a hunch.

It also closes out a hardening pass against the agent runtime, using two mature
agent runtimes as behavioural references. Most of what that pass produced is not
in this list, because most of it was evidence rather than repair: the four
capabilities most likely to fail a user quietly — surviving a restart, switching
provider mid-session, resuming memory, and writing memory — now have tests that
prove the behaviour instead of code that looks correct.

Added

  • Feral stops when it is provably stuck. If a tool returns byte-identical
    output for the same arguments twenty times over, repeating it cannot make
    progress, so the turn ends and says which tool got stuck — instead of quietly
    burning up to 500 iterations or your whole time budget on the same call. A
    tool whose output keeps changing (a build still running, a job still queued)
    is left alone: waiting is not looping.

  • feral update — pulls the latest release and restarts the gateway, so a
    connector already running on Discord or Slack picks up the new build instead
    of quietly serving the old one until you notice.

Fixed

  • Web search works, with no setup. It did not work at all before, on any
    install: the only real backend was a SearXNG server you had to host yourself,
    and both paths meant to cover its absence were dead — the no-backend fallback
    called a DuckDuckGo endpoint that answers definitions rather than searches,
    and the escalation to deep_research searched through a service that now
    requires a paid key. Three dead paths, so nearly every search failed, each
    time with a different-looking error.

    Search now runs keyless on DuckDuckGo out of the box, and it is paced
    one query every 5 seconds, with parallel calls queuing rather than bursting.
    The pacing is the fix, not a precaution: DuckDuckGo throttles by rate, so a
    burst of 14 searches got 7 answered and then a ten-minute block, while the
    same 14 paced through the new limiter returned 14 for 14. If the limit is
    tripped anyway, Feral backs off and says so instead of reporting an empty web.
    A self-hosted SearXNG is still worth it — several engines, no rate limit, no
    pacing delay — and if it goes down, searches fall back to DuckDuckGo and say
    which backend answered.

  • A tool with a backup now actually falls back to it. Tools can declare a
    standby to try when they fail. If that tool also declared a retry policy,
    the standby was skipped for exactly the failures it was meant to cover — a
    missing file, a bad argument — and you got the original error instead of the
    working result. The two code paths that handled this had drifted apart; they
    are now one path and cannot drift again.

  • A hung tool can no longer hang the whole agent. One internal failure shape
    left a tool call waiting forever with its own timeout already switched off.
    Nothing could recover it short of restarting. It is now impossible to reach
    that state.

  • Parallel tool calls all run now, instead of just the first one. When the
    model batched two actions into a single reply — "pause the losing campaign and
    raise the winner's budget" — only the first was executed, nothing recorded the
    loss, and the model reported both as done. Half a task, reported as finished.
    On the benchmark this failed 5 runs out of 17; after the fix, 0 out of 9. A
    call that genuinely can't be read is now counted and named back to the model,
    so it re-sends what's missing rather than assuming it landed.

  • A batch of calls no longer collapses into one. Providers that omit the
    wire-format index had every call in a batch folded into the same slot, where
    names overwrote each other and arguments concatenated into garbage. Three lead
    imports became one; a "check the CRM, then write" dropped the check and
    duplicated a person already on file.

  • Tool calls in the format the model actually speaks. Feral now reads the
    shapes models fall back to when they abandon the format they were asked for —
    <function=…>, [tool:name], Harmony channels — instead of showing them to
    you as prose. Unrecognised tool names are rejected, never invented.

  • "No model loaded" no longer hides the real error. On a cloud model, a
    failed request used to fall back to the local engine — which the app had
    deliberately unloaded when you switched to cloud — and report its complaint
    ("no model selected") instead of what actually went wrong. You now see the
    real cause: rate limit, expired key, or a conversation grown past the model's
    context. As a side effect, a single cloud hiccup no longer drags a multi-GB
    local model back into memory.

  • A tool call cut off mid-argument is retried instead of run blind. When a
    reply hit its token limit while the model was still writing a tool call's
    arguments, the truncated JSON was silently treated as no arguments at all
    so write_file ran with no path and no content, and nothing flagged it. Feral
    now recognises the truncation and asks the model to re-send the call.

  • Compressing a long conversation no longer throws away the recent half.
    The summary that replaces older turns was built from only the first few
    thousand characters — the opening of the session — so every file path,
    command, and fix from the actual work was lost. That is why the agent forgot
    paths it had just written and repeated steps it had already done. The summary
    now covers both ends of what it replaces and is told to keep paths, commands,
    and outcomes verbatim.

  • Extension (MCP) tools are available on the very first message. They
    connect in the background at startup, and a message sent in that window used
    to be answered with "I don't have a tool for that" for a server you could see
    was connected.

  • shell_exec tells the truth about its timeout, and the 5-minute ceiling
    is now raisable (FERAL_SHELL_MAX_TIMEOUT_MS) for builds that legitimately
    run longer.

  • Discord DMs reach the agent. They never had. The connector asked Discord
    for direct-message events but not for the one extra flag that lets an
    uncached DM channel through, so every DM was dropped before any Feral code
    ran — while the connector's own documentation said it always answers DMs.
    Confirmed against a live bot: two DMs arrived at a client carrying the flag
    and neither arrived at Feral's.

  • A bare @mention gets an answer instead of silence. Mentioning the bot with
    no other text stripped down to an empty message, which was discarded without
    a reply, a reaction, or a log line — indistinguishable from a dead bot. It now
    acknowledges and asks what you need.

  • In a server, Feral answers you without an @mention by default. Naming
    channels (feral connectors set discord --channel …) still narrows it to
    exactly those. The allowlist is unchanged and remains the real gate: only
    people you list are ever answered, so this means "answers you anywhere it can
    see you", not "joins every conversation".

  • feral doctor no longer reports a healthy sidecar as dead on Windows and
    macOS.
    It contradicted itself in the same output — api port said the
    sidecar was alive, sidecar said it was DOWN and pointed you at the log to
    investigate a failure that had not happened. The liveness guess came from a
    PID-file check that only ever worked on Linux; everywhere else it answered
    "cannot tell" and the caller printed that as "dead". Liveness now comes from
    the gateway itself, which is the only thing that knows.

  • The endpoint allowlist stays an allowlist after you switch models. If you
    pin the servers Feral is permitted to send your conversation to
    (FERAL_TRUSTED_BASE_URLS), that list used to be quietly thrown away and
    replaced the first time the model was changed — and the check that was
    supposed to enforce it then validated the new address against itself, so it
    could never refuse anything. The list now holds across model switches: a
    switch picks from it and cannot widen it. Unchanged if you never set the
    variable, which is the default.