Feral v2026.08.01
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 todeep_researchsearched 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-formatindexhad 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 —
sowrite_fileran 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_exectells 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 doctorno longer reports a healthy sidecar as dead on Windows and
macOS. It contradicted itself in the same output —api portsaid the
sidecar was alive,sidecarsaid 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.