Repository navigation
v2026.8.19
Full Changelog
474 PRs from 5 contributors since v2026.7.31.
Highlights
- Security hardening — dozens of fixes closing SSRF vectors, path traversal in skill/channel IDs, XSS in canvas and OAuth callbacks, credential redaction gaps, and durable atomic writes throughout the daemon to prevent partial-state corruption.
- Managed configuration mode — new
managedmode locks provider config routes so self-hosted deployments can enforce a fixed LLM setup; pairs with opt-in model discovery and API key support for custom and local providers. - Long-form audio/video transcription — recordings are now processed in sliding windows and written directly to a file, removing the previous length cap on transcription.
- Non-blocking agent messaging and smarter task waking —
agent_sendis now non-blocking by default when called from within an agent turn; posted tasks automatically wake their assignee without requiring a separate trigger declaration. - Polish localization and i18n fixes — Polish (
pl) added as a supported language; Japanese, Spanish, and French error message translations restored and completed.
Added
- Scan the runtime container image for vulnerabilities with Trivy, on
mainand on every native release digest, so OS packages and the bundled Node.js / Python dependencies stop sitting outside the source and dependency checks.
In the release pipeline the scan sits between the per-architecturepush-by-digestbuild and the manifest publish, anddocker-manifestnow depends on it — no user-facing tag (:VERSION,:latest,:lts) can be created or moved onto a digest that failed the gate, and nothing downstream of the manifest (publish_arch_repo,deploy_fly,deploy_render,sync_aur_docker) consumes one either.
Each run publishes a job summary naming the platform digest, scanner and database versions, and every CRITICAL / HIGH finding with its package, installed version, fixed version, CVE, and severity; the raw JSON, the SARIF, and a machine-readable verdict are retained as artifacts and the SARIF is uploaded to code scanning under a per-image category.
The gate ships report-only: issue #6694 measured a 10-critical / 95-high backlog with Trivy 0.57.0, so arming it today would failmainon the first run.
The enforcement threshold is a single default — thefail-oninput of.github/actions/trivy-image-scan— that a maintainer movesoff → critical → highonce the backlog is remediated, and neither workflow overrides it.
Nothing is suppressed to achieve that: no--ignore-unfixedand no severity filtering, so the report is complete either way and only the pass/fail decision is narrowed to fixable findings.
A vulnerability-database download failure is kept distinct from a clean scan — refreshed in its own retried step, with the scan then running--skip-db-update— so a scanner outage fails loudly instead of passing as a finding-free image (#6694, #6712) (@houko) - Add a managed configuration mode so a deployment can own
config.tomlinstead of treating it as application state.
LIBREFANG_CONFIG_PATHrelocates the file — useful on its own, for a Compose bind mount or a ConfigMap mounted outsideLIBREFANG_HOME— andLIBREFANG_CONFIG_MODE=managedlocks it.
The two are deliberately independent: relocating a file is not a statement about who owns it, and inferring the lock from the path would hand a read-only dashboard to an operator who only wanted the file somewhere else.
The mode is read from the process environment and never from the config file, so a write through the API cannot unlock the very file it is being refused access to.
When managed mode is active, every API surface that persists deployment configuration answers423 Lockedwith{"code": "config_managed", "source": "<path>"}and leaves the file untouched — enforcement lives in the handlers rather than relying on a read-only mount, because a filesystemEACCESsurfaces as a 500 with an errno and tells an operator nothing about why.
GET /api/config/statusreports the mode, the source path, writability, a SHA-256 over the file's bytes, and its last-modified time, so the dashboard can present managed settings as read-only from server-supplied metadata rather than by attempting a save and reading the refusal back.
Boot-time schema migration no longer tries to write the migrated config back when the file is managed; it logs a single targeted warning instead.
That write previously failed against a read-only mount with nothing but awarn!, so the migration re-ran silently on every boot forever.
Mutable mode remains the default and is unchanged (#6695, #6717) (@houko) - Polish (pl) is now a supported UI language across the dashboard SPA, the backend Fluent error catalogue, and the webchat widget.
The channel bridge also emits a Polish failure suffix for tool-failure progress lines.
(#6696) (@leszek3737) - Opt-in live model discovery for custom OpenAI-compatible providers, via a
discover_modelsflag on the provider and a toggle in the dashboard's Add / Configure Provider dialogs.
Discovery was gated on a hard-coded id allowlist (ollama | vllm | lmstudio | lemonade), so a self-hosted endpoint registered under any other id was never probed: its model list stayed empty forever and the only recourse was to register every model by hand, or to squat the built-invllmid and override its base URL.
A provider that opts in joins exactly the paths a built-in local one already walks — the 60-second probe loop, thePOST /api/providers/{name}/testrefresh, and the live-model filter on/api/models.
The predicate ORs the flag with the id check rather than replacing it, so the built-in ids keep discovering regardless of the flag and an existing install sees no change.
PUT /api/providers/{name}/discoverytoggles it and persists the value into the provider's own TOML, so the opt-in survives a restart — for a provider you created, which is the case the feature exists for; on a registry-shipped file the boot-time sync still reverts any local edit (#6702, #6714) (@houko) - Run the Python SDK test suite in CI.
sdk/python/tests/held roughly 1900 pytest cases covering the HTTP client and every stdlib-only sidecar channel adapter — slack, discord, telegram, mastodon and the rest — and no workflow ran a single one of them, so the production code path for every sidecar channel shipped with CI fully green regardless of what broke.
Thesdk/prefix was already routed to the Rust lane, but only as an openapi codegen drift guard, which runs cargo and never pytest.
The new lane installs the package with itsdevextra and runs the suite on anysdk/python/**change, in under a minute (#6741) (@houko) - Teach a task-board trigger to fire on unowned work via
pattern = { task_posted = { assignee_match = "unassigned" } }.
Previously the only options were "every posted task" or a specific agent, so an agent that should pick up whatever nobody has claimed had to match everything and filter in the prompt.
The keyword matches both spellings of unowned that reach the event — an absent assignee and the empty string — because neither thetask_posttool norPOST /api/tasksnormalises the field, while both do reject an empty title and description.
A client that sends an empty assignee means "nobody", and a filter that only understood the absent form would silently ignore it.
(#6742) (@houko) media_transcribecan now transcribe a recording in bounded windows and write the transcript to a workspace file, which is what a recording longer than a few minutes needs to reach an agent at all.
Previously the tool transcribed whole files and returned the transcript inline, so two limits unrelated to file size decided how long a usable recording could be: a single transcription request is bounded by a wall-clock timeout that does not scale with the input, and the kernel spills any tool result over[tool_results] spill_threshold_bytes(16 KB by default) to the artifact store and hands the agent a stub instead.
Both are reached around ten minutes of speech, at roughly 2.5 MB of extracted audio — far belowMAX_AUDIO_BYTES, and further still belowMAX_VIDEO_BYTES, so no size limit is anywhere near being involved.
start_secandmax_secsbound the request to one window and the response carrieshas_more/next_start_secto walk the rest;out_pathwrites the transcript as UTF-8 and returns only the path, byte count, sha256 and a 200-character preview, following the contractweb_fetch_to_filealready established.
Windows starting at0begin a new file and later windows append, so repeated calls assemble one transcript without any of it passing through the agent's context.
Both mechanisms are needed rather than either alone: window size varies with how much was said, so a fixed window straddles the spill threshold instead of staying under it, andout_pathis what makes the outcome independent of that.
Callers advance by the produced window length rather than the requested one, read back from the Ogg granule position — a seek lands on a keyframe and a window overlapping the end of the recording is short, so an assumed edge drifts and eventually skips audio.
Consecutive windows are separated by a newline: a boundary lands mid-sentence by design and each window's transcript arrives trimmed, so concatenating them directly would fuse the last word of one window to the first of the next at every boundary.
A call that names neither window field keeps its previous behaviour exactly, including adding no ffmpeg pass. (#6748, #6773) (@nevgenov)
Fixed
- Escape every TOML control character when the dashboard serializes agent manifest strings, preserving carriage returns, tabs, and other control bytes as valid round-trippable TOML instead of producing a manifest the daemon cannot parse. (@TechWizard9999)
browser_read_pageno longer drops the destination of every link nested inside a list item, and no longer returns a single card from a feed or search-results page.
The extraction script'slibranch flattened the item totextContentand returned before descending, so a nested anchor reached the model as bare text with no URL — 1,100 of 1,723 anchors on the Rust Wikipedia article and 11 of 11 on a DuckDuckGo results page.
Clicking the text was not a fallback for those: 408 of the 1,100 do not resolve to themselves underbrowser_click's substring matcher, so there was neither a URL nor a working text handle.
The branch now recurses and folds its children back onto one line, so a bullet still renders as a bullet and the links inside it keep their identity.
Root selection usedquerySelector('main, article, [role="main"], .content, #content'), which returns the first match — on a page built from sibling<article>cards that is one card, measured at 13.7% of a DuckDuckGo results page.
Selection now climbs to the ancestor holding repeated siblingarticleelements, the way Readability resolves the same shape by walking to the common ancestor of its close-scoring candidates.
Selection tests a node's direct children rather than everything below it: at least three of them must each carry an article of their own, so an ordinary post page whose "related posts" widget is built out ofarticlekeeps selecting the post instead of widening to the document and taking the widget with it.
The tree is searched rather than climbed from the first article's ancestors, since the container is not always an ancestor of whichever article comes first — a featured card above a grid puts that article in a branch the grid does not sit under.
A candidate that contains another loses to it, so a feed flanked by single-article widgets selects the feed rather than the ancestor holding all three, and between candidates that do not contain one another the one with the most article-carrying children wins, so a small widget nested deeper than the feed does not beat it for being deeper.
A page with amainor[role="main"]landmark is unaffected, and a page with a singlearticlestill selects it (#6624, #6745) (@nevgenov)- Fix four ways the release pipeline mishandled a large changelog, all of which fired on v2026.7.31 and left the release stuck.
cargo xtask releasepassed the whole changelog section as the PR body, which GitHub rejects over 65,536 characters withGraphQL: Body is too long— the version bump committed and pushed, but no PR opened, sotag_on_mergenever got the<!-- release-tag: -->marker it tags from and the release simply stopped.
The body is now capped with a prefix-preserving truncation (the marker sits at position 0 and the Highlights lead, so a tail cut drops only the least-important bullets), cut on a character boundary, closing a code fence the kept prefix left open, and handed toghvia--body-filerather than an argv entry.
release.ymlfed the same section togh release create --notes-file, where the API's own 125,000-character ceiling would have failed the job after the tag was already pushed; it now truncates the same way.
Separately,tauri-actionwas invoked withtagName, whosegetOrCreateReleasepath PATCHes an already-published release with whateverreleaseBodyholds — and the desktop matrix passes none, so every desktop build overwrote the release notes with an empty string.
That is why v2026.7.21 and v2026.7.27 both shipped with a blank body despitecreate_releasewriting one correctly.
Bothrelease.ymlandrelease-desktop.ymlnow address the release byreleaseId, which only uploads assets and leaves the notes alone.
release-notify.ymlbounded its Discord announcement by line count (head -20) but not by length, and a single bullet here runs past 1,800 characters, so the webhook would answer 400Must be 2000 or fewer in length; it now truncates to a character budget that leaves room for the build-status block.
The release commit also stagesxtask/baselines/now: the run regenerates the schema baselines just before committing, so stagingopenapi.jsonwithout them left a driftedopenapi.sha256in the working tree that would failschema-check checkon every subsequent PR.
(#6689) (@houko) - Fix the two
xtaskchangelog tests that run against the repo's ownCHANGELOG.mdfailing on any release branch, which blocked the v2026.7.31 release PR (#6688) on a state the release flow itself creates.
cargo xtask releasedrains## [Unreleased]into the dated section it cuts, so on achore/bump-version-*branch the section is empty — anddrains_the_repos_own_unreleased_section_without_tripping_the_guardopens by asserting it is not, whilefolds_into_the_repos_own_changelogasserted a###heading count that only holds when### Changedalready exists.
Both now read through a helper that reconstitutes the pre-release shape by hoisting the newest dated section back into[Unreleased], so the real-file coverage survives on release branches rather than being skipped there, and the subsection assertion is a delta that permits exactly the one heading the fold may legitimately create.
Doing that surfaced a second, older defect in the same two tests: they checked headings withstr::contains/str::matches, which count substrings anywhere on a line, while theawkextractor they mirror anchors at column 0.
A curated bullet that quotes a heading in its prose — the #6628 entry says "appended its bullet to the single## [Unreleased]section" on an indented continuation line — therefore read as a boundary overrun and as 19[Unreleased]headings.
Both checks are now line-anchored, matching the extractor.
This had never fired because the assertions had only ever run against an empty[Unreleased].
(#6690) (@houko) - Fix
Sign Release Artifactsfailing every release since #6677, which shipped debug symbols as their own assets without updating the sibling-count guard that assumed one.sha256per platform target.
The guard exists to catch matrix drift in either direction and is deliberately an equality, so the four newlibrefang-<target>-debug-symbols.tar.gz.sha256files read as four extra platforms: v2026.7.31, the first release cut after #6677 landed, failed withexpected exactly 12 .sha256 siblings, got 16after every artifact had already been built and published.
The two kinds of sibling are now counted separately, because only one of them is one-per-matrix-target.
Platform binaries keep the strict= 12equality that makes a dropped or added target stop the release loudly.
Debug symbols get a2..4range instead, matching how they are produced:cli_macfails outright when its.dSYMis missing, so both macOS targets are guaranteed, while the cross-compiledcli_linuxtargets only warn when the.dwpis absent — a count below 2 means the macOS hard-failure path did not hold and is worth stopping for, and a count of 2 or 3 emits a warning rather than taking a release down over a diagnostic aid.
The manifest itself is unchanged: the download loop andls *.sha256still read the full asset list, so every hash including the debug-symbols ones stays inSHA256SUMSand under the cosign signature.
(#6691) (@houko) - Always offer the API-key field for a provider that declares
key_required = false, instead of hiding it.
The flag says whether a key is mandatory, not whether one is accepted: every built-in local provider (ollama,vllm,lmstudio,lemonade) declares it false, yet a self-hosted vLLM or an Ollama behind a reverse proxy answers 401 without one — and the runtime has always forwarded whatever key is stored asAuthorization: Bearer.
Hiding the input made those servers impossible to configure from the dashboard even though the daemon would have used the key, and the onboarding wizard dropped a typed key on the same reasoning.
The provider list now also reportskey_present, so a keyless provider that does have a key stored offers "replace" and "remove" rather than pretending none exists —auth_statuscannot carry that, since it collapses tonot_requiredeither way.
The registry conflict error stops pointing at?allow_overwrite=true, a query parameter no UI surface sends, and names the endpoints that actually edit a provider (#6703, #6714) (@houko) - Open external links from the desktop app in the user's real browser instead of silently discarding them.
The Tauri window registered no new-window handler, and wry connects WebKitGTK'screatesignal — plus the WKWebView and WebView2 equivalents — only when one exists, so everytarget="_blank"anchor andwindow.open()call in the dashboard died on arrival inside the desktop app: the EveryAPI partner panel, marketplace and plugin links, the skill-workshop PR link, and the command palette's registry entries all did nothing on click, and right-clicking a link and choosing "open link" did nothing either.
New-window requests are now handed to the OS default handler and the in-app window is denied, with the scheme restricted tohttp/https/mailtoso that afile:orjavascript:target coming from agent output or a server-controlled catalogue is never forwarded to the shell (#6706, #6711) (@houko) - Manual schedule runs now deliver successful agent and workflow output through configured primary and fan-out targets, matching timed cron fires instead of returning output only to the API caller (#6708) (@Kvitral)
- A task assigned to an agent now wakes that agent, instead of reaching it only when an operator had separately registered a matching
task_postedtrigger.
Nothing in the kernel ever created such a trigger, so delivery was entirely operator-supplied: with none declared — or with one deleted, lost, or never added for a newly onboarded worker — an addressed task satpendingindefinitely and no log line said so.
The kernel now synthesizes the wake itself as one more entry in the dispatch list, so it inherits the existing trigger lane, per-agent semaphore, per-fire timeout, ordering and cycle guard, and persists nothing: no record appears intrigger_jobs.jsonortrigger list, and the new[task_board] assignee_wakeknob (defaulttrue, per-agent override on the manifest) fully reverts it.
A stored trigger that can currently fire for the assignee still owns delivery, so an operator's prompt, cooldown, session mode and workflow routing are untouched and no agent is woken twice; a trigger that is disabled or has exhaustedmax_firesis treated as a gap to fill rather than as a decision to stay silent, since a dead record is indistinguishable from a lost one and that ambiguity is what the outage was made of.
Only agents thattask_claimactually reaches are woken, which leaves installations whose board is drained by an external claimer on the agent's behalf exactly as they were: withholding the tool through any of the three mechanisms the runtime honours — acapabilities.toolslist without it, a narrowingtool_allowlist, or atool_blocklistentry — also withholds the wake, while an agent that declares nothing at all is unrestricted and is woken.
Four diagnostics now cover the ways delivery can still break — an assignee that resolves to no registered agent, a wake switched off with no trigger to take over, an assignee that cannot claim, and a trigger that exists but can no longer fire.
[task_board]is also reclassified from restart-required to no-op in the config-reload plan and its documentation table: the sweeper has always re-read its three knobs on every tick, so the promised restart was never required.
Delivery no longer depends on the event surviving at all: the task-board sweeper — already a reconciler, sincetask_reset_stuckreacts to task state rather than to any event — gains a second rule of the same shape, waking the assignee of anything stillpendingpast[task_board] pending_grace_secs.
That makes a dropped event a latency question instead of a lost task, which matters because a trigger cooldown discards events for distinct subjects rather than deferring them (#6756), and an event-driven wake can only ever be as reliable as the event.
The reconcile deliberately does not consult the trigger-coverage check the event path uses: a task still pending past the grace window is evidence that whatever was configured did not deliver it, whatever the configuration says.
It rate-limits per assignee rather than per task, since the wake prompt is drain-style, and backs off exponentially towake_backoff_max_secswhenever a wake leaves the pending set unchanged, so an agent that cannot make progress is not woken on every tick. (#6744) (@nevgenov) - Restore the Windows test lane, which had aborted on every push to
mainsince #6711 and takenCI Gatedown with it, so for three days no PR merged with a Windows signal behind it.
librefang-desktop's test binary links on Windows but cannot be loaded — the process dies at start with0xc0000139(STATUS_ENTRYPOINT_NOT_FOUND) — and nextest executes every test binary with--listto enumerate its tests, so the lane died before running a single one.
The crate is now excluded from that lane's nextest invocation and built there link-only instead, which keeps the Windows compile+link coverage that nothing else in CI provides while its 11 platform-independent URL/scheme tests continue to run on Ubuntu, macOS and the unit-fast lane.
The missing DLL export behind the abort is still unidentified, so this is a workaround carrying a note to remove it once the real cause is found.
A greenmainrun also no longer closes an openmain-redissue unless a Rust test lane actually ran on that commit.
Thechangesgate skips every Rust lane on a docs- or dependency-only push while the run still concludessuccess, which is how this one breakage came to be filed and auto-closed three times (#6716, #6721, #6729) before anyone noticedmainhad been red for days — nine of the last sixteen greenmainruns would have closed it.
The failure notice now names the head commit and the run URL without asserting that the commit caused the failure, because three consecutive filings told readers to revert an unrelated openrouter model-snapshot PR for a breakage introduced days earlier.
(#6735) (@houko) - Close the three provider routes that managed configuration mode left able to rewrite
config.toml, soLIBREFANG_CONFIG_MODE=managednow enforces what #6717 documented rather than most of it.
Setting a provider key, pointing a provider at a different base URL, or switching the default provider each persisted into the deployment-owned file —[default_model],[provider_urls],[provider_proxy_urls]— and answered200, so an operator whose configuration comes from a ConfigMap could silently drift the running daemon away from the manifest and lose the change on the next rollout with nothing in the response to suggest anything had gone wrong.
Setting a provider key is refused in full rather than only at its config write, because that write is conditional on live daemon state the caller cannot see: guarding it alone would accept or refuse the same request depending on timing, having already rewrittensecrets.envin the refusing case.
The operator-facing known-gaps list is corrected in the same pass — it named three files without their write sites and missed the sidecar-channel and init routes entirely, which meant anyone reading it to decide whether managed mode was a complete seal was reading a list assembled from the wrong evidence (#6737) (@houko) - Stop two Slack formatting defects that made long replies unreadable or invisible.
Runs of blank lines now collapse to the single blank line Slack uses as a paragraph separator; the Markdown converter mapped lines one-for-one, so a model that padded its answer turned a short reply into a wall of whitespace.
The collapse runs while fenced code is masked as a single token, so code interiors keep their own blank lines.
An interactive Block Kit reply longer than Slack's 3000-character per-section limit is now split across as many sections as it needs instead of being rejected wholesale and dropped with nothing but a log line, and the section count is budgeted against the 50-blocks-per-message cap so the buttons — the functional payload — are never the thing that gets dropped.
The plain-text path was never affected: its chunker already emitted pieces under the limit (#6741) (@houko) - Stop the Slack adapter leaving a permanent 👀 on messages the daemon never answers.
The reaction was added the moment the adapter received a message, butdispatch_messagehas roughly two dozenreturnpaths above the first adapter-visible lifecycle signal — mention-only group gating, per-user and per-channel rate limits, RBAC, command policy, slash commands the bridge handles itself — and none of them was visible to the adapter, so the mark stayed on a message no agent ever read.
Both halves of the receipt now ride the turn lifecycle instead: 👀 on thequeuedphase, ✅ ondone, ❌ onerror.
That closes every one of those paths structurally rather than one at a time, gives a failed turn a terminal state it previously never got, and honours the daemon'sclear_done_reactionknob on Slack for the first time.
Keying strictly on the triggering message's own id also deletes a "pick the first pending message in this channel" fallback that fired on every in-thread reply — the send hook looked up the thread root while the 👀 was tracked under the message's own timestamp, so the ✅ landed on an unrelated sibling, sometimes one of the leaked marks (#6741) (@houko) - Report a mis-declared
channel_overrides.group_trigger_patternsentry when an agent's manifest is accepted, because the usual mistake is invisible by construction and cost a reporter hours.
Writing the naturalgroup_trigger_patterns = ["(?i)\bvivi\b"]in a TOML basic string does not produce a word-boundary regex:\bis TOML's backspace escape, so the kernel receives(?i)<U+0008>vivi<U+0008>, and the regex crate accepts a bare control character as a verbatim literal.
The pattern therefore compiles — the bridge's existing "invalid regex" error never fires — and then matches nothing, so the agent simply never answers to its own alias in group chats and every message is dropped asmention_only_no_mention.
The new check names the offending codepoint and prescribes the fix (a TOML literal single-quoted string, or doubled backslashes), and runs at spawn, hand-role activation, on-disk hot-reload,update_manifestand boot-time restore from persistent storage, so an operator iterating on a broken alias sees it on the next reload rather than never.
The restore path is the load-bearing one: it registers a persisted agent without going through the spawn path, so on any daemon past its first run it is the only route the diagnostic could fire on at boot.
It warns and never rejects: an unreachable alias is a typo, and failing the spawn would turn a cosmetic mistake into a missing agent.
The bridge's own compile path could not serve this purpose — it is lazy, memoised per distinct pattern set, and does not consider the control-character case an error at all.
The channels and agent-overrides docs gain the escaping rule and the previously undocumentedgroup_trigger_patternsfield, and no longer claimMentionOnlyis what an unsetgroup_policydoes, which #6445 made false.
(#6742) (@houko) - Make a superseded agent turn visible in the log instead of discarding it at
debug!level, below the default filter.
When a newer message arrives for the same(agent, session)the in-flight turn is aborted and produces no reply at all, and in a group channel the error text is suppressed too, so the only symptom an operator ever saw was a bot that silently ignored them.
The abort key matters more than it looks: for a group the session spans the whole channel, not the thread, so a message from any user in any thread preempts whatever turn is running for that agent there.
The warning now names that mechanism and reports how long the discarded turn had been running, and the channel bridge separately records that the aborted turn emits nothing.
Logging only — the supersede policy itself is unchanged.
(#6742) (@houko) - Bound nested workflow runs by the inter-agent call-depth quota, which the
workflow_runtool had been bypassing entirely.
Running a workflow executed it inline on the calling agent's task and each step nested a complete agent turn, but nothing counted that nesting — so an agent whose workflow step targets an agent that runs a workflow again recursed with no bound other than the wall-clocktriggers.max_workflow_secs, long after the tokio worker's stack had run out.
Workflow nesting is now charged to the samemax_agent_call_depthbudget thatagent_sendhops already use, becauseA --agent_send--> B --workflow--> Cstacks real agent turns exactly the wayA -> B -> Cdoes and one operator knob should cap both.
A run entered too deep is refused as a policy error before the run record is created, so a capped chain reports the quota instead of leaving an orphanPendingrun behind.
Two hops between the kernel and the agent were flattening that refusal into a generic server error, so the agent was told its workflow had crashed rather than that it had hit a limit.
It now arrives as a permission denial, which also stops a capped agent from losing its whole turn to a repeated-failure abort.
The daemon and the desktop app's embedded server also raise their tokio worker stack from tokio's 2 MiB default to 8 MiB: an agent turn is a chain of very large futures and a nested turn restacks it, so overflowing it aborted the whole process — with only two workers that took the HTTP API and every cron job down together.
The CLI TUI's in-process kernel mode runs the same turn chain on its own long-lived runtime and on the dedicated thread that streams a turn's events, so both get the same 8 MiB stack.
The stack change is headroom rather than a bound, and neither change is proven to be the cause of the crash reported in #6659, whose original report is unrecoverable; the next occurrence on the larger stack is what will tell unbounded recursion apart from bounded depth with fat frames.
(#6743) (@houko) - A sub-list nested inside a list item keeps its own bullets, indented, rather than being folded into its parent's line.
Folding a list item's children onto one line is what lets a bullet still read as a bullet, but a nestedlihas already emitted its own-by the time the outer one folds, so the whole sub-list collapsed into the parent as- Fruits - Apple - Banana— markers mid-sentence that read as list items, or as a numeric range on text like- Price - 5 - 10.
Anarticlealso now earns a blank line the waysectionanddivalready do, so entries on a feed page that carry no heading of their own stay distinct instead of running together into one block (#6624, #6745) (@nevgenov) - Image generation works again against OpenAI's
gpt-image-*models.
The request always carriedresponse_format, which that family rejects with400 Unknown parameter, so every generation against those models failed while DALL-E was unaffected.
The parameter is now sent only to models that accept it, and unrecognised model names keep the previous behaviour so third-party OpenAI-compatible endpoints are unchanged.
(#6750) (@houko) - The media integration tests no longer depend on the developer's shell lacking provider credentials.
With an API key exported, tests asserting the missing-key path stopped exercising it and instead made real, billable calls — one generated an mp3 through the live OpenAI TTS endpoint while asserting that no provider was configured.
The harness now clears every credential variable the media drivers read, and a dedicated test fails if that ever stops happening.
A unit test that resolved a provider before reading its input file had the same dependency and could have reached a live transcription request; its input path is now guaranteed absent.
(#6750) (@houko) - Transcription of
.mp4and.movrecordings works again instead of silently returning nothing.
The audio track was extracted by piping the container to ffmpeg, but these formats keep their index at the end of the file and demuxing it requires seeking backwards, which a pipe cannot do — so the extraction produced a valid container carrying no audio at all, and whatever the transcription provider made of a soundless file was what the operator saw.
Because ffmpeg reports success in this case, neither existing check noticed.
The input is now staged to a scratch file so it can be seeked, and a stream that arrives without audio is rejected outright rather than uploaded.
.mkvand.aviwere never affected, being streamable formats.
(#6751) (@houko) - A trigger's cooldown no longer discards an event because a different event happened a moment earlier.
The window was keyed on the trigger alone, so it could not tell "the same thing fired twice" from "two things happened a second apart", and the second was dropped rather than delayed — with nothing to re-announce it, sinceevaluate_with_resolverproduced no match at all andfire_countnever moved.
On a task board that meant a completed task's notification vanished, or worse, a posted task's wake did, which is how work went missing while the log said nothing abovedebug.
The window is now scoped to what the event is about — the task id for the three task-board patterns, the memory key forMemoryUpdateandMemoryKeyPattern, the agent forAgentSpawnedandAgentTerminated— so two distinct subjects arriving inside one window are two windows, while a repeat of the same subject is suppressed exactly as before.
Patterns that name a category rather than a transition (All,System,SystemKeyword,Lifecycle,ContentMatch) keep the trigger-wide window they have today: they match streams whose subjects are nearly always distinct, so keying on the subject would turn "at most once per window" into "once per event" for a trigger whose bounded firehose is the point.
Per-subject windows live in memory and are pruned once they can no longer suppress anything, solast_fired_aton disk keeps meaning "when this trigger last fired"; a subject-scoped trigger therefore starts a restart with no window at all, including for a subject it fired on moments earlier, which trades an extra delivery against growingtrigger_jobs.jsonby one entry per subject ever seen.
Worth knowing if you paircooldown_secswithmax_fireson a scoped pattern: the trigger now fires once per subject, so a burst of distinct subjects consumes the fire budget as fast as they arrive rather than at one per window, and a trigger that exhaustsmax_firesdisables itself.
The CLI help and the trigger/config documentation described the window as per-trigger and have been corrected alongside. (#6918) (@nevgenov) - The public Rust channel message splitter now always consumes at least one complete character, including when an incomplete HTML entity begins a chunk or the byte limit falls inside a multi-byte UTF-8 character.
Custom Rust channel consumers can no longer enter a non-progressing split loop on those inputs (#6777) (@houko) - Proactive-memory extraction now moves its prompt-size cutoff to a valid UTF-8
boundary before searching for a newline or truncating. Long conversations that
place a CJK character, emoji, or other multi-byte character across the 8,000-byte
boundary no longer abort automatic memory extraction with a slicing panic
(#6778) (@houko) - Session stream garbage collection now retains broadcast channels while a turn forwarder is active, so reconnecting and late-attaching clients continue receiving in-flight events instead of joining an orphan replacement channel (#6785) (@houko)
- Agent identity persistence now remains blocked after an existing registry file fails to load, preserving recoverable on-disk mappings instead of replacing them with an empty fallback snapshot on the next mutation (#6787) (@houko)
- Sticky assistant routing now reads the same sender- and thread-scoped cache keys it writes, while
explicit_onlychannels remain on their configured agent when no route has been explicitly cached instead of invoking classification (#6788) (@houko) - Generate release articles with working CHANGELOG anchors, safe validated tag URLs, fence-aware section extraction, and opt-in replacement of existing hand-edited output. (@houko)
- Make the live channel-progress smoke fail when the kernel emits no tool event, supervise and clean up its foreground daemon reliably, and safely construct its dedicated test-agent manifest. (@houko)
- Repair the Go SDK streaming example so it compiles, validates dynamic agent IDs instead of panicking, and reports stream error events instead of silently succeeding. (@houko)
- Package the real
librefangPython module tree in legacy setuptools builds, with distribution name and version metadata kept in sync withpyproject.toml. (@houko) - Keep the website GitHub statistics section stable when optional translations change, and cancel its in-flight statistics requests when the section unmounts. (@houko)
- Restore registry responses after an empty or expired worker cache by routing inline refreshes through the repository synchronization path instead of calling a removed function. (@houko)
- Restore
xtaskbuilds on the workspace Rust 1.94.1 MSRV by keeping itssysinfodependency on the compatible 0.38 release line. (@houko) - Make the
xtasklicense fallback inspect the full Rust dependency graph and evaluate SPDX AND/OR expressions correctly instead of silently skipping third-party crates or matching license-name substrings. (@houko) - Keep Dashboard string-map edits from replaying parent change callbacks under React Strict Mode. (#6799) (@houko)
- Preserve Dashboard struct-list expansion and focus while editing valid JSON values. (#6800) (@houko)
- Keep Dashboard confirmation dialogs open until asynchronous actions succeed, with retry support after failures. (#6801) (@houko)
- Preserve large resource quotas when agent manifests pass through the Dashboard visual editor. (#6802) (@houko)
- Prevent an unmounting Dashboard drawer from closing a newer drawer that has taken over the shared slot. (#6803) (@houko)
- Keep empty and incomplete number-map edits as local drafts until they become valid numbers, and restore the last committed value when an invalid draft loses focus. (@TechWizard9999)
- Keep an empty structured-list textarea as an uncommitted JSON draft and restore its last valid item on blur instead of silently replacing that item with an empty object. (@TechWizard9999)
- Enforce
xtask license-check --denyagainst Rust dependency metadata even when cargo-deny is installed, while retaining the repository's cargo-deny policy as the first gate (#6807) (@houko) - Make
xtask license-check --denymatch denied SPDX license ids case-insensitively.
The denied-list comparison used exact string equality against the canonical SPDX id, so a custom--denyentry with different casing than the canonical form (e.g.gpl-3.0-onlyvsGPL-3.0-only) silently failed to match and let the license through (#6807) (@houko) - Enforce denied SPDX licenses for web dependencies from pnpm's JSON report, including Commons Clause rejection and fail-closed command or report errors, instead of printing a truncated report and always succeeding (#6808) (@houko)
- Stop RL exporter tests from mutating the process-wide environment while exercising secret indirection and the public SSRF dispatch. (@houko)
The tests previously calledset_varandremove_varwhile Rust's test harness was free to run other cases on parallel threads, making outcomes dependent on shared process state and creating a future Rust 2024 safety blocker.
Production still resolves configured secret names throughstd::env::var; the crate-private dispatcher now accepts the lookup function so tests can supply deterministic values and missing-variable errors without touching the real environment. - Reject non-object
paramson known Python sidecar commands as a recoverable protocol error. (@houko)
Truthy arrays, strings, booleans, or numbers previously escapedparse_commandasAttributeError, killed the reader task, and left the sidecar waiting forever instead of reporting the malformed frame and processing the next command.
Unknown future command methods retain their raw parameter shape for forward compatibility. - Stop and restart Python sidecars when their command reader encounters an unexpected fatal error. (@houko)
An exception from the stdin source, parser, or protocol-error emitter previously killed only the reader task while the main runtime waited forever, leaving a live process that could no longer receive commands or shutdown.
The runtime now logs the traceback, signals cleanup, raises a cause-preservingReaderCrashed, and maps it to a nonzero stdio-process exit so the daemon supervisor can recover the adapter. - Tie Fly deployment progress to the real request lifecycle instead of a cosmetic timer. (@houko)
The deploy page previously marked one setup step complete every 1.5 seconds even while/api/deploywas still pending, and left that interval running when the form unmounted.
Pending deployments now show only the request as active, mark completion only after a successful response, and abort plus ignore late results when the form unmounts. - Always close generated Python SDK streaming responses when iteration ends. (@houko)
The SSE generator previously leaked its HTTP response when it returned on[DONE], the caller stopped iteration early, or a read or decode operation raised before the loop reached the trailingclose()call.
Response cleanup now lives infinally, covering normal EOF, protocol completion, generator close, and exceptional exits while preserving the original event and error semantics. - Apply a bounded timeout to every generated Python SDK HTTP request.
Both ordinary API calls and SSE stream setup previously calledurlopenwithout a timeout, leaving connection establishment and stalled socket reads without an inactivity bound.
Clients now use a 30-second default for both paths and accept a constructor-level timeout override for deployments that need a different network budget.
A slow-to-respond server now raises the SDK's ownLibreFangErrorinstead of a bareTimeoutError, keeping the new failure mode inside the same error contract callers already rely on for connection and HTTP errors (#6823) (@houko) - Wrap generated Python SDK connection failures in
LibreFangError. (@houko)
Both ordinary and streaming requests previously leakedurllib.error.URLErrorfor failures such as DNS resolution errors, refused connections, and connection timeouts.
Callers can now handle HTTP and connection-level API failures through the SDK's documented error type, with connection failures represented by status0and an empty response body. - Preserve split UTF-8 characters in generated Python SDK streams. (@houko)
The SSE reader previously decoded each 4096-byte network chunk independently, so a multibyte character split across reads raisedUnicodeDecodeErrorand aborted the stream.
Streaming now buffers raw bytes and decodes only complete SSE lines, making text decoding independent of transport chunk boundaries. - Report generated Go SDK stream body encoding failures. (@houko)
The streaming helper previously discardedjson.Marshalerrors and continued with an empty request body, hiding unsupported values from callers.
It now emits a status-0error event and closes the stream before constructing or sending an HTTP request. - Handle generated Go SDK stream request-construction failures. (@houko)
The streaming helper previously ignoredhttp.NewRequesterrors and dereferenced a nil request, allowing malformed methods or URLs to panic its goroutine and terminate the process.
It now emits a status-0error event and closes the stream before accessing the invalid request. - Decode email bodies with their declared MIME charset. (@houko)
The IMAP email helper previously forced UTF-8 for multipart plain text, HTML fallback, and non-multipart bodies, silently dropping bytes from common encodings such as ISO-8859-1 and GB2312.
Each body part now uses itscharsetparameter while retaining UTF-8 as the fallback for missing or unknown charset labels. - Decode complete RFC 2047 email subjects. (@houko)
The IMAP email helper previously decoded only the first subject segment, truncating mixed plain/encoded subjects and subjects composed of multiple encoded words.
It now joins every decoded segment in order and retains a UTF-8 fallback for unknown charset labels. - Guarantee IMAP session cleanup in the email reader. (@houko)
The helper previously logged out only on selected success and handled-error paths, leaking the connection when an unexpected exception occurred after login.
It now closes every constructed IMAP session through a non-masking cleanup path, including login failures and all post-login exits. - Validate IMAP FETCH responses before parsing email bytes. (@houko)
The email helper previously indexed the server response without checking its shape, producing opaque index/type errors or passing flag-only data into the MIME parser.
Empty, truncated, non-tuple, and non-byte responses now fail with a clear malformed-response diagnostic. - Escape sender values in IMAP email searches. (@houko)
The helper previously interpolated the sender directly into a quoted SEARCH criterion, allowing quotes, backslashes, or line controls to alter or break the command structure.
Quotes and backslashes are now escaped as IMAP quoted-string data, while CR, LF, and NUL are rejected before opening a connection. - Surface generated Rust SDK stream transport failures. (@houko)
The SSE reader previously stopped silently when a response body chunk returned an error, making truncated connections indistinguishable from clean stream completion.
It now emits a status-0stream errorevent before closing the channel, while preserving any valid events received before the failure. - Encode generated Rust SDK path parameters as URL segments. (@houko)
Generated endpoints previously interpolated path values directly, so slashes, query/fragment delimiters, whitespace, and Unicode could change the request target or address a different resource.
URLs are now assembled withUrl::path_segments_mut, preserving base-path prefixes and percent-encoding each parameter as one segment; literal.and..segments fail closed instead of being normalized away. - Bound generated Rust SDK stream buffering to 256 events. (@houko)
Streaming previously used Tokio's unbounded channel, allowing a fast server and stalled consumer to grow memory without limit.
The producer now awaits a bounded channel, applying transport backpressure and stopping promptly when the receiver is dropped. Stream methods consequently returntokio::sync::mpsc::Receiver<Value>; callers with explicitUnboundedReceiverannotations must update the annotation, while normal inferred.recv()usage is unchanged. - Add default network timeouts to the generated Rust SDK.
The default reqwest client previously had no connect timeout, and ordinary API requests could wait indefinitely for a server that accepted a connection but never responded.
All requests now use a 10-second connect timeout, while non-streaming calls additionally use a 60-second total timeout; SSE bodies remain exempt from the total deadline so long-lived streams continue normally (#6836) (@houko) - Fixed the generated JavaScript SDK dropping a final server-sent event when a stream ended without a trailing newline, the same defect class fixed for the Rust and Python SDKs in this release.
_streamsplit incoming bytes on\nand only processed complete lines, so a clean EOF right after the lastdata:line left it sitting unprocessed in the leftover buffer (#6837) (@houko) - Fixed the generated Python SDK dropping a final server-sent event when a stream ended without a trailing newline, the same defect class fixed for the Rust SDK in this release.
_streamsplit incoming bytes on\nand only processed complete lines, so a clean EOF right after the lastdata:line left it sitting unprocessed in the leftover buffer.
The trailing-buffer flush now decodes as strictly as the per-line decode in the main loop above it, instead of silently replacing truncated multi-byte UTF-8 with a�placeholder and yielding a{"raw": ...}event that hid the corruption (#6837) (@houko) - Fixed the Rust SDK dropping a final server-sent event when a stream ended without a trailing newline. (#6837) (@houko)
- Added a Rust SDK constructor that accepts a configured
reqwest::Client, enabling authenticated requests and other custom HTTP settings across all generated resources. (@houko) - Updated the Rust SDK basic example to report unexpected API response shapes instead of silently displaying zero items. (@houko)
- Made the Rust SDK basic example honor
LIBREFANG_URL, while retaining the local daemon URL as its default. (@houko) - Reduced the Rust SDK's Tokio feature set to the runtime, synchronization, and macro capabilities it actually uses, avoiding unnecessary downstream feature unification. (@houko)
- Made the Rust SDK's reqwest TLS backend explicit and selectable: existing users retain default TLS, while downstream crates can choose rustls or disable TLS features. (@houko)
- Aligned the Rust SDK with the workspace's thiserror 2 dependency, avoiding duplicate major versions in monorepo builds. (@houko)
- Removed Tokio's multi-thread scheduler from the Rust sidecar SDK's published dependency features and moved its echo example to the current-thread runtime. (@houko)
- Added an explicit crates.io package allowlist for the Rust sidecar SDK so unrelated local files cannot enter published archives. (@houko)
- Declared the Rust sidecar SDK's tested serde, serde_json, and Tokio version floors instead of accepting untested early 1.x releases. (@houko)
- Simplified the Rust sidecar SDK quick-start imports so the minimal adapter example lists only the APIs it uses. (@houko)
- Breaking: Aligned Rust sidecar poll builder option IDs with the kernel's
u8wire contract, preventing adapters from constructing out-of-range poll payloads. The Telegram sidecar now rejects out-of-range upstream option IDs at its translation boundary. (@houko) - Documented the Rust sidecar SDK's deliberate fail-closed handling of missing required command fields and its compatibility difference from the legacy Python parser. (@houko)
- Avoided cloning the full JSON parameter tree while parsing known Rust sidecar commands. (@houko)
- Bounded Telegram streaming state with stale-entry eviction, concurrent-stream and per-stream buffer caps, and graceful-shutdown cleanup. (@houko)
- Rejected malformed Telegram update payloads that omit required response, update, or message identity fields instead of silently defaulting their IDs. (@houko)
- Warned in the Telegram dashboard schema that leaving
ALLOWED_USERSempty permits all users. (@houko) - Prevented Telegram's degenerate HTML chunking path from emitting chunks above the configured UTF-16 limit. (@houko)
- Escaped raw HTML metacharacters in Telegram sanitizer text nodes while preserving already-valid HTML entities. (@houko)
- Fixed the Python sidecar's Telegram HTML sanitizer emitting invalid crossed tags (e.g.
<b><i>x</b>→<b><i>x</b></i>) when a closing tag matched an entry below the top of the open-tag stack.
The sanitizer now closes every tag above (and including) the match, innermost first, matching the Rust sanitizer's stack-drain behavior. (#6856) (@houko) - Prevented self-closing Telegram HTML tags from being emitted as literal
<tag/>markup in the Python sidecar's sanitizer, matching the Rust sanitizer's fix.
Telegram's HTML subset has no self-closing-tag syntax, so a literal<tag/>risked either an "Unclosed start tag" error from the Bot API or the tag staying open for the rest of the message; self-closing input is now rebuilt as a balanced<tag></tag>pair instead. (#6856) (@houko) - Prevented self-closing Telegram HTML tags from wrapping all following text during sanitization. (#6856) (@houko)
- Rejected Telegram location payloads with missing or non-numeric coordinates instead of silently sending
(0, 0). (@houko) - Prevented self-closing and void HTML tags from leaking into Telegram chunk carry state. (@houko)
- Enforced Telegram's UTF-16 chunk limit against the actual generated HTML close-tag suffix instead of relying only on a fixed reserve. (#6859) (@houko)
- Rendered every adjacent single-star italic run in Telegram messages instead of leaving alternate runs as literal Markdown. (@houko)
- Preserved Telegram HTML tags containing
>inside quoted attribute values instead of truncating and corrupting them. (@houko) - Added a complete Markdown-to-sanitized-and-chunked Telegram formatting helper and routed text sends through it. (@houko)
- Kept rendering content after an unclosed Telegram Markdown code fence instead of swallowing the remainder into one code block. (@houko)
- Restored Telegram inline-code placeholders in one linear pass instead of repeatedly rescanning and reallocating the whole message. (@houko)
- Honored delta-seconds from Telegram HTTP
Retry-Afterheaders in the Python sidecar'ssendMessage/ multipart-upload retry paths before falling back to the JSON body or default backoff, matching the Rust adapter's fix.
_extract_retry_afterpreviously only readparameters.retry_afterfrom the JSON body, so a server that only set the HTTP header (and omitted the JSON field) fell straight through to the 2s default instead of honoring the server's requested delay.
Also capped the retry sleep atMAX_RETRY_AFTER_SECS(300s): the Python retry paths had no cap at all, so a flood-wait response with an extremeretry_afterwould have slept for that entire duration instead of skipping the retry, unlike the Rust adapter. (#6866) (@houko) - Honored delta-seconds from Telegram HTTP
Retry-Afterheaders before falling back to the JSON body or default backoff. (#6866) (@houko) - Returned recoverable Telegram API errors if a retry loop ever exhausts instead of panicking and killing the sidecar. (#6867) (@houko)
- Scaled Telegram multipart upload timeouts with payload size so valid large media can complete on slower links. (#6868) (@houko)
- Accepted Telegram message IDs encoded as JSON integers as well as decimal strings for edit and delete commands. (#6869) (@houko)
- Rejected malformed non-object Telegram media-group entries instead of silently dropping them from the outgoing group.
Also rejected a media-group item missing its requiredurlfield instead of sending Telegram an emptymediavalue. (#6870) (@houko) - Dropped Telegram callback events without chat context instead of routing them into an empty synthetic channel. (#6871) (@houko)
- Detected Telegram Ogg/Opus voice uploads from the Ogg page's actual first-packet offset instead of assuming a fixed header layout. (#6872) (@houko)
- Logged Telegram
getFilefailures before falling back to media placeholders, making persistent media degradation visible to operators (#6873) (@houko) - Reported invalid Telegram channel and reaction message IDs consistently across typing, reaction, interactive, streaming, and ordinary send commands. (#6874) (@houko)
- Normalized emoji variation selectors consistently before mapping Telegram progress reactions. (#6875) (@houko)
- Preserved typed JSON decoding errors in the Telegram sidecar error source chain for diagnostics and downcasting. (#6876) (@houko)
- Removed the unnecessary
T: Defaultbound from Telegram API response envelopes while preserving their default field values. (#6877) (@houko) - Added the registry version required for publishing the Telegram sidecar's local SDK dependency once that SDK is available on crates.io. (#6878) (@houko)
- Expanded Telegram schema regressions to cover the type and visibility of every dashboard configuration field. (#6879) (@houko)
- Removed a per-link allocation from Telegram href scheme validation while preserving case-insensitive and UTF-8-safe checks. (#6881) (@houko)
- Grouped consecutive Telegram Markdown quote lines into one multi-line blockquote, matching the Python adapter. (#6882) (@houko)
- Logged Telegram typing-action and reaction-update API failures while preserving their best-effort command semantics. (#6883) (@houko)
- Made Telegram chunk progress derive solely from the newly selected input, preventing formatting carry from skewing boundary consumption. (#6885) (@houko)
- Logged dropped Telegram stream deltas and stream-end events whose stream ID has no active state, while preserving best-effort handling. (#6886) (@houko)
- Rejected malformed Telegram poll options and missing or out-of-range quiz answers before issuing a Bot API request.
Also enforced the Bot API's question, option, and explanation length bounds locally so an oversize poll fails fast instead of a 400 from Telegram. (#6887) (@houko) - Logged best-effort Telegram callback acknowledgment failures with control-safe callback and error details (#6891) (@houko)
- Fixed the
librefang-rl-exporttest call sites that still passed&Valuetoredact_metadataafter it became by-value, which brokecargo check --all-targetson the aarch64 lane and blocked every open PR behind a red CI Gate. (#6896) (@houko) - Harden the discussion-to-issue and weekly-report workflows against partial failures and unsafe assumptions.
The discussion backfill now serializes through a concurrency group, bounds every job with a timeout, and records per-discussion failures instead of continuing past them silently.
The manual/to-issuecommand now requires an exact token match instead of a substring match, so a comment that merely contains that text can no longer trigger a promotion.
The weekly report now fails closed on any command error, resolves the repository fromgithub.repositoryinstead of a hardcoded name, and surfaces Discord delivery failures instead of swallowing them (#6904) (@houko) - Warn about duplicate Dashboard map keys and preserve compact struct-list JSON drafts until blur. (#6906) (@houko)
- Require approval before unrecognized channel senders can use network or tool-discovery capabilities. (#6908) (@houko)
- Simplify canonical agent identity registration and remove silent mutex-poison recovery. (#6910) (@houko)
- Proactive-memory extraction now keeps its kernel-handle slot usable when a thread panics while holding the lock, instead of silently ignoring all later handle reads and updates.
Conversation prompt assembly also uses a preallocated buffer and writes each message directly, avoiding a temporary allocation per turn.
(#6911) (@houko) - Link understanding now compiles its URL extraction pattern once and shares it across messages, avoiding repeated regex parsing and allocation on the message processing path. (#6912) (@houko)
- Canvas sanitization now enforces its configured byte limit before appending each output fragment, preventing entity escaping from temporarily growing a rejected document several times beyond the limit. (#6913) (@houko)
- The dashboard agent editor now blocks periodic schedules without a cron expression and JSON-schema response formats whose schemas are empty, malformed, or cannot be represented faithfully in TOML.
Validation errors automatically open their sections and are exposed to assistive technology.
It also removes a redundant schedule parsing branch, clears duplicate tag submissions, and gives the stream-thinking toggle an accessible name.
(#6914) (@houko) - Release changelog generation now fails closed when git or GitHub metadata is incomplete, bounds external commands, rejects model-generated section headings, preserves the Unreleased section on a first release, and reuses compiled title patterns. (#6915) (@houko)
- Repository automation now preserves devcontainer build failures, cancels only superseded ignored-test PR runs, and pins first-party actions in the supply-chain audit. (#6916) (@houko)
- Session repair now removes prompt-injection markers after international text without corrupting Unicode byte boundaries. (#6917) (@houko)
- Harden trajectory export by using the existing audited SHA-256 dependency, preserving hexadecimal identifiers during blob redaction, respecting workspace path-component boundaries, and surfacing JSON serialization failures (#6920) (@houko)
- Reuse stable session-scoped files when loading inline history images, move their filesystem work off Tokio worker threads, keep empty-session response fields consistent, localize malformed session IDs, and enforce the documented 100 KiB tool-result cap in UTF-8 bytes (#6921) (@houko)
- Closed two holes in the AI-attribution guards that between them let the harness footer reach 285 PRs and issues unchallenged.
Both layers matched only the "with" spelling of the generated-by line while the footer uses "by", so every check in front of it reported clean; both now match either verb plus the footer's own link-and-host shape, which leaves a genuine claude.ai artifact link alone.
Nothing inspected a PR body at all — the existing rule reads onlygit commit -m, and the git-side hook cannot see a body that never enters a commit — sogh pr,gh issueandgh releasebodies are now checked too, reading the file behind--body-filerather than only inline flags, since the convention mandates the file form.
A third defect surfaced while pinning the corpus: the Python predicate required a space inside the product name where the shell hook allowed none, so two variants the shell hook's own corpus lists as must-block were waved through one layer up.
check-bash-rules.pydecides every PreToolUse verdict and had no test of any kind, which is how a one-word gap survived; it now has a mutation-checked corpus wired into thegithook-testsCI job (#6936) (@houko) - Bound the rendered MCP summary cache, which grew one entry per distinct allowlist combination for the lifetime of the daemon.
Agent manifests control the allowlist, so a caller cycling through one-off combinations (or stale generations left behind by config reloads) could grow the cache without limit.
The cache now caps at 256 distinct entries and clears wholesale before admitting a new key past that cap, while preserving current-generation cache hits and rendered summary content (#6939) (@houko) atomic_writefsynced the staged temp file before the rename but never synced the containing directory afterward, so the rename itself was not guaranteed durable.
A crash between the rename syscall and the next unrelated fsync of that directory could still lose the update on some filesystems, even though the write looked atomic from the caller's side.
On Unix, the parent directory is now fsynced after the rename so the new directory entry survives a crash (#6942) (@houko)- Secret writes to
secrets.envcould report success while a staging-filefsyncfailure went unnoticed, or leave a 0600 secret-bearing staging file behind after a failed write or rename.
The staging file now propagatesfsyncerrors instead of discarding them, gets removed on any write or rename failure, and the parent directory is fsynced after the atomic rename on Unix so a completed write survives a crash immediately afterward (#6944) (@houko) - Sidecar config writes used
fs::writefor the staging file, which never fsyncs, so a crash between the write and the rename could leave the renamed file pointing at stale or truncated data, and a rename failure left the staging file behind instead of being cleaned up.
The staging file is now opened withcreate_new, fsynced before the rename, removed on any write or rename failure, and the parent directory is fsynced after a successful rename on Unix so a completed write survives a crash immediately afterward (#6945) (@houko) - Skill secret writes staged to a fixed
.tmpsibling name, so concurrent writers to the samesecrets.envcould clobber each other's staging file, and a write or sync failure left that 0600 secret-bearing staging file behind on disk.
The staging file now gets a name unique per process and call, is removed on any write or sync failure, and the parent directory is fsynced after the atomic rename on Unix so a completed write survives a crash immediately afterward (#6947) (@houko) - Cron script TOML writes opened their staging file with
File::create, which truncates and silently reuses an existing file of the same name instead of failing loudly on a staging-name collision.
The staging file is now opened withcreate_newso a collision surfaces as an error rather than being silently overwritten, and the parent directory is fsynced after the atomic rename on Unix so a completed write survives a crash immediately afterward (#6948) (@houko) - Memory consolidation and the per-user spend ranking used
.filter_map(|r| r.ok()).collect()over their SQLite row iterators, which silently dropped any row that failed to decode instead of surfacing the failure.
A corruptedagent_idcould make consolidation skip a tenant's memories with no error, and a corrupted usage row could make a user vanish from the spend ranking rather than showing up as a failed query.
Both call sites now collect intorusqlite::Result<Vec<_>>and propagate the decode error (#6951) (@houko) - Paginate the GitHub API queries in the issue-inactive and issue-pr-link workflows, which previously only read the first page of results.
A repository with more than 100 open assigned issues could skip inactive-issue reminders for issues past the first page, and a repository with more than 100 open pull requests could havehas-princorrectly stripped from an issue that a later-page PR still linked (#6959) (@houko) - Restore Vite's default dev-server proxy error logging, which a custom logger and a set of no-op
errorhandlers on the/apiproxy, its outgoing request, and its incoming response were silently swallowing.
A backend that was down or unreachable duringnpm run devproduced no diagnostic output at all, making the failure look like a hang instead of a connection error.
The WebSocket (ws: true) and five-minute proxy timeout behavior are unchanged (#6965) (@houko) - The audit trail's boot-time integrity check verified as intact even when a row failed to decode from SQLite, because the loader silently skipped the malformed row instead of treating the load as incomplete.
AuditLog::with_dbnow records the first load error it hits — a bad connection, a failed query, or a row that fails to decode — andverify_integrityfails closed whenever one is present, so a partially loaded chain never reports as verified (#6968) (@houko) - Fixed a race in
CronScheduler::add_jobwhere concurrent creators could each pass the global and per-agent job-limit checks before any of them inserted, letting the total job count exceed the configured cap.
Capacity checks, validation, and insertion are now serialized on a dedicated lock so the whole add sequence is atomic (#6970) (@houko) max_content_charsnow bounds the link table's opening line along with its entries, so the extraction stays inside the ceiling an operator set rather than overshooting it by that line's length.
The budget summed the entries and stopped there, but the rendered block also opens with a line naming the marker form and the base origin, and that line reaches the model with the entries — 50,093 characters against a 50,000 cap on the Rust Wikipedia article, the 93 being that line for a 24-character origin.
The test could not have caught it: it re-derived the table's cost in its own port and asserted against that same derivation, so the budget and the assertion agreed by construction whatever the renderer did.
Every ported test in this module now asserts that the template still contains the rule it models, since a port is only evidence about the script while the two agree — and nothing else would have noticed the script and its port drifting apart.
It now asserts against whatrender_page_bodyactually produces, which is the string that reaches the model (#6624, #6973) (@nevgenov)- CLI commands that rewrite
config.toml, channel configs, MCP server entries, and ChatGPT OAuth secrets used a plain truncatingfs::write, so a crash or kill mid-write could leave a corrupt or empty file behind.
These call sites now go through a shareddurable_atomic_writehelper that stages content in a unique sibling file, fsyncs it, and atomically replaces the target viarenameon Unix orMoveFileExWon Windows, fsyncing the parent directory afterward on Unix so the replacement survives a crash.
New secret files are created at 0600 and an existing file's permissions are now preserved exactly, including bits a restrictive process umask would otherwise silently strip from the creation mode (#6974) (@houko) - The MCP migrator wrote synthesized
[[mcp_servers]]configuration with a truncating write, so a crash or kill could leaveconfig.tomlempty or partial.
The config is now staged, fsynced, and atomically published; existing Unix permissions are preserved, newly created config files use mode 0600, parent-directory sync failures after a successful publish are logged without misreporting the migration as skipped, and Windows publishes with write-through semantics. (#6975) (@houko) - Serialized local skill installs (
POST /api/skills/install) behind the same per-skill file lock already used by evolve and uninstall.
Previously the handler checked destination existence, then copied the skill directory outside any lock, so two concurrent installs of the same skill could both pass the existence check and race to write into the same directory, and a failed loser's cleanup (remove_dir_all) could delete a winner's just-installed files.
The existence check now happens after the lock is acquired, and cleanup on a failed copy only ever removes the failed copier's own attempt (#6977) (@houko) - Add a global React Query
MutationCacheerror fallback so a rejected mutation without its ownonErrorhandler now surfaces a localized toast instead of failing silently.
Mutations that already register a mutation-specificonErrorare left untouched to avoid duplicate feedback (#6978) (@houko) - The cron scheduler's final persistence attempt during kernel shutdown discarded its result with
let _ = …, so a failed flush of execution state (a full disk, an unwritable data dir) left no trace anywhere.
run_cron_scheduler_loopnow logs a structuredwarn!with the underlying I/O error when the shutdown-time persist fails, while still letting shutdown proceed (#6979) (@houko) PATCH /api/memory/configread and wroteconfig.tomlwith untorn but non-atomicstd::fs::write, so a crash mid-write could leave the file truncated, and two concurrent dashboard saves could interleave a read and a write and silently revert each other's change.
The managed-mode guard now runs before the file is touched, the full read-modify-write-reload transaction is serialized under the shared config write lock, the read moved off the blocking thread, and the write goes through the durable atomic writer (temp file, fsync, rename, directory fsync) on the blocking pool (#6982) (@houko)- Registry content creation raced on the no-overwrite check: two concurrent
POST /api/registry/content/{type}calls for the same identifier could both observe an absent file and each write, silently discarding whichever write lost.
The existence check and the write are now serialized under the sameconfig_write_lockused by the other config-mutating endpoints, and the write itself goes through the fsync-based atomic writer instead of a plainfs::write.
A rejected provider definition is now rolled back to its prior contents (rather than merely deleted), so a failed overwrite of an existing provider no longer leaves it missing (#6984) (@houko) GET /api/sessionsran itscount_sessionsandlist_sessions_paginatedSQLite calls directly on the async handler, so a large or contended sessions table could stall the Tokio worker thread and delay every other request being served by it.
Both calls now execute together ontokio::task::spawn_blocking, and a query or blocking-task failure is now logged server-side instead of being silently discarded (#6986) (@houko)POST /api/initchecked and wroteconfig.tomlwith unsynchronized blockingstd::fscalls directly on the async handler, so two concurrent requests could race past the existence check and one write could clobber the other, and every call blocked an async worker thread on disk I/O.
The existence check now uses async metadata, the write path serializes on the sameconfig_write_lockused by the other config-mutating routes and rechecks existence after acquiring it, and directory creation plus the atomic config write both run on Tokio's blocking pool (#6988) (@houko)- Added the five error-message translations missing from the Japanese Fluent locale (an agent invalid-sort key and four webhook error keys), preserving every Fluent interpolation variable used by the English source.
Added a regression test asserting the Japanese locale covers every English error key so a newly introduced key can no longer ship without a translation (#6998) (@houko) - Restored missing diacritics and inverted punctuation across the Spanish error-message locale (
válido,sesión,configuración,¿agente no encontrado?, and similar), and corrected a few literal, unnatural phrasings alongside unit formatting for size limits.
Added regression assertions for representative accented translations so a future edit cannot silently strip them again (#6999) (@houko) - Restored missing diacritics across the French error-message locale (
déjà,échec,création,déclencheur, and similar), and corrected unit-abbreviation typography for size limits.
Added regression assertions for representative accented translations so a future edit cannot silently strip them again (#7000) (@houko) - Proactive-memory lock recovery from a poisoned state is now logged for the runtime config lock and the decay/cleanup/counter-prune maintenance locks, instead of recovering silently.
Config reads and writes, and background maintenance scheduling, remain usable after recovery (#7003) (@houko) - Channel agent-router lock recovery from a poisoned state is now logged for both the binding list and the broadcast configuration, instead of recovering silently.
Routing and broadcast resolution both remain usable after recovery (#7004) (@houko) - A2A task-store lock recovery from a poisoned state is now logged for both the in-memory task map and the backing SQLite connection, instead of recovering silently.
Task loading, persistence, lookup, and mutation all remain usable after recovery (#7006) (@houko) - Audit-log lock recovery from a poisoned state is now logged with the specific state involved — entries, tip, chain anchor, or load-error — instead of recovering silently across every accessor.
Recording, verification, and retention all continue to operate correctly after recovery, with the hash chain's integrity preserved (#7007) (@houko) - Command lane read/write lock recovery from a poisoned state is now logged with the affected lane, instead of recovering silently.
The lock's poison flag is cleared once the recovered state has been read out, so a single panic produces one diagnostic log line rather than a permanent per-access warning for the rest of the process (#7013) (@houko) - Hand activation now logs when the activation mutex recovers from a poisoned lock, instead of recovering silently.
The mutex only serializes the check-and-insert critical section and guards no data of its own, so recovering viainto_inner()was already safe — the gap was visibility into a prior panic, not correctness.
The recovery path also clears the mutex's poison flag once the inner state has been read out, so a single panic produces one diagnostic log line rather than a permanent per-call warning for the rest of the process, matching the fix already applied toCommandQueue's locks (#7013).
This bringsactivate_with_idin line with the existingpersist_lockpoison-recovery logging (#7028) (@houko) - Recover the agent context cache after a mutex poisoning event instead of permanently disabling it.
get_cachedandstore_cachedused to give up silently once the lock was ever poisoned, which meant every future turn served no cachedcontext.mdand every write became a no-op for the remaining life of the process.
The cache now recovers the poisoned guard and logs a warning so the corrupted synchronization state stays observable (#7029) (@houko) - Recovered the checkpoint snapshot concurrency counter after mutex poisoning instead of panicking at snapshot entry or silently skipping the decrement in the cleanup guard.
A panic while the counter lock was held used to either abort the current snapshot attempt outright or leave the permit accounting off by one forever, since the old cleanup path only decremented onOk.
The lock is now recovered viainto_inner()and the poison flag cleared so the mutex stops re-poisoning every later lock attempt, and a regression test exercises the poison-then-recover-then-release sequence (#7030) (@houko) - The stuck-task reset sweep silently dropped any
task_queuerow it could not decode from SQLite, so a single corrupt row (e.g. a non-numericretry_count) caused the rest of that sweep's stuck tasks to be skipped with no error surfaced to the caller.
task_reset_stucknow decodes the full candidate set before applying any reset update, so a row decode failure fails the sweep closed instead of silently reducing its coverage (#7031) (@houko) - Session search (
SessionStore::search_sessions/search_sessions_paginated) now propagates a row-decode failure fromsessions_ftsas an error instead of silently dropping the corrupt row and returning a partial result set.
A single malformed row previously vanished from search results without a trace; the same failure is now surfaced to the caller so the underlying corruption gets noticed and investigated (#7032) (@houko) - Group roster storage (
RosterStore::upsert,members,remove_member,member_count) swallowed every SQLite pool-exhaustion and row-decode error, returning empty results or fixed defaults instead of failing.
A corruptedgroup_rosterrow was silently dropped frommembers()rather than surfacing as a query failure, and a pool outage duringupsertorremove_memberlooked identical to success to every caller.
All four methods now returnLibreFangResult, and the channel bridge and kernel handle boundaries propagate the error instead of discarding it (#7033) (@houko) TraceStore::query,query_by_trace_id, andcountswallowed SQLite failures and poisoned-mutex errors, returning an empty list,None, or0indistinguishably from a genuine empty result.
A corrupt row or a failing query on the hook-trace store was therefore reported to callers as "no traces found" rather than as a failure.
These methods now returnrusqlite::Result, andGET /api/context-engine/traces/:trace_idsurfaces a scrubbed HTTP 500 instead of a false 404 when the store itself fails (#7034) (@houko)- Approval audit queries used to swallow SQLite failures and return an empty list or a zero count, which looked identical to "no audit history exists" on the dashboard and in the duplicate-resolution helper used by channel bridges.
query_auditandaudit_countonApprovalManagernow return a typed result, the/api/approvals/auditroute surfaces a scrubbed HTTP 500 on failure instead of a fabricated empty page, and the channel-bridge duplicate check logs a warning and falls back to its prior no-match behaviour rather than pretending the query succeeded (#7035) (@houko) Path::parent()yieldsSome("")rather thanNonefor a bare relative filename, and three recently added atomic writers treated that empty-but-present case as an error.
In the cron script writer the parent is opened for the post-rename directory fsync, so an empty parent would fail with ENOENT after the rename had already succeeded — reporting failure for a write that landed on disk.
In the Skillhub and skill-evolution writers it only anchors the staging file beside the target, where an empty parent happened to work because the join and the rename both resolved against the process directory, making the same-directory invariant that keeps the rename atomic hold by accident.
All three now resolve an empty parent to., matching the API crate's atomic writer, which already handled it (#7036) (@houko)- The shared metering budget snapshot (
MeteringEngine::budget_status) silently converted a usage-store query failure into zero spend via.unwrap_or(0.0), so a broken SQLite read looked identical to "no spend yet" everywhere the snapshot was consulted.
budget_statusnow returns aLibreFangResult<BudgetStatus>; the/api/budget,/api/budgetupdate, and/api/system/health/detailroutes return a scrubbed HTTP 500 on failure instead of a fabricated zero-spend response, and the WebSocketbudgetcommand and channel-bridge budget reply now report an explicit "temporarily unavailable" message instead of misleading zero values (#7037) (@houko) - Stop channel message dispatch when the recovery journal fails to persist an entry, instead of logging the failure and continuing as if the write-ahead record existed.
MessageJournal::recordnow returnstrueonly once the entry is durable and indexed, and bothdispatch_messageanddispatch_with_blocksabort with a user-facing retry notice onfalserather than proceeding without crash-recovery coverage (#7040) (@houko) GET /api/sessionsswallowed a failedcount_sessionscall with.unwrap_or(0)and both it andGET /api/sessions/searchfell back to an empty200 OKpage on a database error, so a broken sessions table looked identical to "no sessions yet" from the client's side.
search_sessionsalso leaked the raw SQLite error string (e.g. table names) straight into the response body viaApiErrorResponse::internal(error.to_string()).
Both handlers now propagate the failure as a scrubbed500with a generic message, logging the real error server-side withtracing::error!, consistent with the rest of the file (#7041) (@houko)- Move dashboard archive extraction and installation off Tokio's async worker threads. (@xiaomo)
- Load WASM agent modules asynchronously so filesystem latency cannot block Tokio worker threads. (@xiaomo)
- Return an internal error when backup directory entries or metadata cannot be read instead of reporting a misleading empty or zero-sized backup list. (@xiaomo)
- Propagate malformed prompt-version, experiment, variant, and metrics rows instead of replacing invalid UUID, JSON, or timestamp fields with default data. (@xiaomo)
- Abort sidecar config and secrets.env read-modify-write operations when an existing file cannot be read instead of treating the failure as an empty file. (@xiaomo)
- Abort auto-dream lock acquisition when an existing lock file cannot be read instead of treating it as an unowned stale lock. (@xiaomo)
- Return a scrubbed server error when an extension install or uninstall cannot apply its on-disk MCP configuration, instead of reporting success against stale runtime state. (@xiaomo)
- Move agent-template directory and manifest reads off synchronous filesystem APIs, and surface corrupt or unreadable listings instead of returning an empty or incomplete template list. (@xiaomo)
- Make the sidecar configuration include check asynchronous and fail closed when the root or included configuration cannot be read or parsed, instead of continuing with a potentially shadowing write. (@xiaomo)
- Move agent identity-file writes, renames, canonicalization, and deletes off Tokio worker threads while preserving containment checks and atomic replacement. (@xiaomo)
- Read skill supporting files asynchronously with a real 256 KiB buffer limit, and surface canonicalization errors instead of disguising every filesystem failure as a missing file. (@xiaomo)
- Fail closed when reading a hand manifest fails instead of silently returning a lower-priority or synthesized manifest, and move the file read off the async request worker. (@xiaomo)
- Read exported configuration asynchronously so downloading
config.tomlcannot block API request workers. (@xiaomo) - Serialize session compaction with concurrent message writers so an LLM compaction cannot overwrite messages saved after its initial snapshot (#7070) (@houko)
- Use effective dashboard i18n defaults, make channel save warnings coherent, stop unavailable QR polling, and keep channel selections synchronized. (#7072) (@houko)
- Clear kernel router cache lock poison after recovering routing state (#7126) (@houko)
- Recover poisoned background watcher state and close the stop-versus-registration race so stopping an agent aborts its in-flight tick and promptly releases the shared LLM concurrency permit. (#7129) (@houko)
- Recover poisoned channel-bridge abort-handle state so tracked tasks still stop during shutdown and hot reload. (#7139) (@houko)
- Recover poisoned terminal activity tracking so live PTY sessions retain accurate idle-timeout behavior. (#7141) (@houko)
- Recover poisoned command-catalog skill registry reads so installed slash commands remain visible. (#7142) (@houko)
- Recover poisoned agent skill-assignment registry reads so available skills remain visible without repeated recovery. (#7143) (@houko)
- Recover poisoned agent-message default-model reads so provider preflight keeps the active override without repeated recovery. (#7144) (@houko)
- Recover poisoned system-status model overrides and return provider/model from one consistent snapshot. (#7145) (@houko)
- Recover poisoned per-agent watcher slots and close registration races so background tasks are aborted when agents stop. (#7146) (@houko)
- Recover poisoned skill-catalog registry reads so installed skill lists and details remain available. (#7147) (@houko)
- Reject malformed and non-base64 image data URIs in OpenAI-compatible chat requests instead of forwarding corrupt vision blocks. (#7148) (@houko)
- Treat invalid session creation dates as undated so malformed timestamps cannot hide newer sessions in the dashboard. (#7149) (@houko)
- Validate dashboard date and uptime inputs so epoch timestamps render correctly and malformed values use a stable placeholder. (#7150) (@houko)
- Report malformed quoting, duplicate headers, and accurate source row numbers during dashboard user CSV imports. (#7151) (@houko)
- Make memory decay sweeps atomic, surface malformed access timestamps, and document zero-TTL behavior. (#7152) (@houko)
- Preserve goal run start times and validate deterministic persistence metadata. (#7153) (@houko)
- Reject malformed rate-limit counts and timestamps without panicking on provider headers. (#7154) (@houko)
- Preserve usage accounting errors and allow records that exactly reach configured quotas. (#7155) (@houko)
- Disable durable audit appends after an incomplete database reload and reject unknown persisted actions without coercion (#7179) (@houko)
- Restore the dashboard Hooks correctness gate and align lint overrides with test, config, and clipboard helper boundaries. (#7321) (@houko)
- Let dashboard section-label callers reliably override layout classes, centralize compact-label typography, and harden Overview range, memoization, timestamp, and typed-navigation contracts. (#7322) (@houko)
- Keep the dashboard Comms page resilient to partial snapshots and query failures, and align polling, refreshes, and counts with the active tab. (#7323) (@houko)
- Reject malformed Hand metadata in the dashboard chat picker and preserve agents that hold multiple Hand roles, instances, or memberships. (#7332) (@houko)
- Make generated agent-manifest Markdown resilient to table delimiters, embedded code fences, repeated blank lines, large backtick inputs, non-decimal costs, unsupported extras, and unknown schedule modes. (#7333) (@houko)
- Avoid recording canvas undo history or reallocating graph state when a stale context-menu node or connection target is deleted. (#7334) (@houko)
- Validate continuous agent schedule intervals, surface invalid values in the visual editor, and keep parsed manifest list identities stable across reloads. (#7335) (@houko)
- Preserve existing chat metering and memory metadata when delayed terminal frames omit optional fields. (#7336) (@houko)
- Wait for dashboard translation initialization before mounting and normalize detected regional locales to supported language codes. (#7337) (@houko)
- Pin the dashboard Lucide version used by curated deep imports and enforce every icon mapping and the exact-version contract with smoke tests. (#7338) (@houko)
- Honor the user's reduced-motion preference across the dashboard, reuse filter-free shared dialog variants, and remove paint-heavy blur keyframes. (#7339) (@houko)
- Align the dashboard session-selector documentation and short-ID fallback with their actual guarded contracts. (#7340) (@houko)
- Make dashboard skill-hub lookup null-safe, configure self-hosted registry URLs per deployment, and shell-quote copied install commands. (#7341) (@houko)
- Normalize video task statuses and stop dashboard polling after terminal states. (#7394) (@houko)
- Paginate dashboard memory records, apply agent and level filters before pagination and search caps, and return grouped per-agent counts without N+1 polling. (#7395) (@houko)
- Refresh stale dashboard version data when a long-lived window regains focus. (#7397) (@houko)
- Allow unfiltered dashboard cron queries while preserving explicit caller opt-outs. (#7400) (@houko)
- Update dashboard session truncation reactively and stop reconnecting completed streams. (#7401) (@houko)
- ClawHub CN skill details now share the same one-minute freshness window as the other dashboard marketplace detail views. (#7402) (@houko)
- Dashboard user filters now share one cached full-list request, match roles case-insensitively, and tolerate malformed channel-binding values without breaking search. (#7404) (@houko)
- Dashboard workflow detail, run, and operator-pause queries now preserve required-ID guards even when callers provide query enablement overrides. (#7405) (@houko)
- Session stream attachments now support authenticated WebSockets and release connection slots immediately when clients disconnect. (#7406) (@houko)
- Disclose when audit queries and exports can only inspect a truncated in-memory history window. (#7408) (@houko)
- Label authorization denial audit records with the endpoint that rejected the request. (#7409) (@houko)
- Restore both
config.tomlandsecrets.envwhen a sidecar configuration write fails, reuse the runtime's canonical dotenv parser for shadow detection, and serialize registry metadata directly from its typed response. (#7412) (@houko) - Give builtin slash commands precedence over colliding skills and release the skill registry lock before formatting command responses. (#7413) (@houko)
- Bound manual provider-test and pending A2A discovery caches with named, expiring entries, and remove stale route dead-code suppressions. (#7415) (@houko)
- Recover poisoned OFP peer rate-limiter locks without discarding active message or token counters. (#7417) (@houko)
- Recover a poisoned supervised-subprocess cooldown lock while preserving its respawn-storm guard. (#7418) (@houko)
- Recover a poisoned MCP OAuth refresh-lock registry without losing active single-flight entries. (#7419) (@houko)
- Recover poisoned plugin state-file and persistent-process registries without discarding active lock or process slots. (#7420) (@houko)
- Recover the external memory-provider slot after a provider panic poisons its lock, preserving the registered provider and allowing later hot swaps. (#7421) (@houko)
- Return an ACP internal error when an agent prompt stream closes before reporting its completion reason instead of presenting the aborted turn as successful. (#7422) (@houko)
- Let editor-backed filesystem calls fall back to local files when the optional ACP reverse-RPC times out or loses its response channel. (#7424) (@houko)
- Fixed ACP
session/resumereplaying persisted history to clients that already have the conversation. (#7425) (@houko) - Fixed dashboard sparklines failing to render large data sets that exceed the JavaScript function argument limit. (#7426) (@houko)
- Fixed unknown workflow operator actions crashing the dashboard action bar. (#7427) (@houko)
- Fixed the dashboard schedule editor accepting out-of-range cron field values. (#7428) (@houko)
- Fixed shared dashboard buttons submitting surrounding forms unless explicitly configured as submit controls. (#7429) (@houko)
- Fixed dashboard input error styling disappearing while the field is focused or hovered. (#7430) (@houko)
- Fixed clickable dashboard cards and KPIs being inaccessible from the keyboard. (#7431) (@houko)
- Fixed multi-select free-text duplicates and active option announcements. (#7432) (@houko)
- Fixed status pills defaulting unknown states to running and labeling denied states as rejected. (#7433) (@houko)
- Fixed unnamed shared select controls for assistive technology. (#7434) (@houko)
- Fixed missing accessibility state and names in the skill output panel. (#7435) (@houko)
- Harden dashboard route parsing and stale-asset recovery without risking unbounded reloads when browser storage is unavailable. (#7465) (@houko)
- Respect reduced-motion preferences across CSS animations, transitions, scrolling, and their delays (#7467) (@houko)
- Validate canvas imports before replacing React Flow state and detach imported canvases from previously selected workflows.
Dependency selections and imported legacy labels use stable step-node IDs; invalid restored references and stale runtime state are rejected or cleared. (#7468) (@houko) - Preserve unsaved user-policy edits across background refreshes without overwriting unrelated concurrent server changes.
Channel rule keys are normalized before duplicate checks, and successful saves immediately become the clean form baseline. (#7469) (@houko) - Reject invalid analytics budget values before submitting a partial update.
CSV exports now neutralize spreadsheet formulas and control-character prefixes in agent and model identifiers. (#7470) (@houko) - Keep mobile pairing countdowns, QR rendering, and concurrent device removals synchronized with their actual request state.
Invalid expiry timestamps now fail closed instead of displayingNaN(#7471) (@houko) - Keep the memory embedding provider and model controls synchronized when switching catalogs.
The custom-model input remains available while a new value is entered, and provider changes reset stale model and key settings. (#7472) (@houko) - Prevent an explicit terminal disconnect from suppressing reconnects on a replacement WebSocket. (#7473) (@houko)
- Validate dashboard locale files and preserve array structure when checking translation-key parity. (#7497) (@houko)
- Restore strict dashboard dependency build enforcement so installs fail when scripts are not explicitly approved. (#7498) (@houko)
- Restore setup instructions in the pinned MCP registry fixtures by keeping them at the catalog root. (#7499) (@houko)
- Align the pinned Bedrock provider fixture with the bearer-token credential used by the runtime driver. (#7500) (@houko)
- Finish every failed MCP reconnect transition so health status no longer remains stuck in an in-progress state after connection or configuration errors. (#7514) (@houko)
- Honor HandsHub retry guidance as the complete wait before the next request so rate-limit responses do not stack server-directed delays with client backoff. (#7515) (@houko)
- Keep missing extension resources as distinct typed errors so API clients receive accurate 404 responses without losing the original failure text. (#7516) (@houko)
- Bound cron token-cap trimming with a binary search and compare estimates in u64 space so large limits remain correct on 32-bit targets. (#7523) (@houko)
- Keep deferred approvals pending when the kernel self-handle needed to resume them is unavailable so the decision remains retryable. (#7524) (@houko)
- Neutralize every triple-backtick sequence in untrusted reviewer context, including longer backtick runs that previously rebuilt a valid code fence. (#7525) (@houko)
- Preserve not-found and external-edit conflict status codes when wiki vault errors cross the kernel handle boundary. (#7526) (@houko)
- Return a typed not-found error when goal updates target an absent store or missing goal while keeping malformed goal storage as an internal error. (#7527) (@houko)
Changelog truncated — the release body is capped at 125,000 characters.
Read the full section in CHANGELOG.md.
Install / Upgrade
Homebrew (macOS):
brew install librefang # CLI (stable) — official homebrew-core
brew tap librefang/tap # pre-release CLI + desktop app
brew install librefang-beta # CLI (beta channel)
brew install librefang-rc # CLI (rc channel)
brew install --cask librefang # Desktop (stable)
brew install --cask librefang-beta # Desktop (beta channel)
brew install --cask librefang-rc # Desktop (rc channel)CLI (Linux/macOS): curl -fsSL https://librefang.ai/install.sh | sh
npm: npm install -g @librefang/cli · pip: pip install librefang · cargo: cargo install librefang
Docker: docker pull ghcr.io/librefang/librefang:latest
Coming from OpenClaw / OpenFang? librefang migrate --from openclaw (or --from openfang)
Documentation · Discord · Contributing Guide
Full diff: v2026.7.31...v2026.8.19