Repository navigation
Releases: opensquad-ai/opensquad
Release list
v0.9.0
Added
- A plugin can own the startup screen. The window that covers startup is no longer
built into the shell: a plugin contributes it, rotates a playlist until the app is
ready (audio included, stopped on exit and never stalling at a seam), and can keep a
private clip private. - Claude Code mods: a host, and slots in the app. A Node host speaks NDJSON to the
agent process;mod_slot/mod_commandsevents carry the validated render tree and
the commands a mod contributes,ModSlotHost/ModPaneViewrender them, the/mods
routes and the mod slash commands wire them up, and Settings gains a "mods" entry
gated on themods_hostplugin. Mods declare the tools they proxy in their manifests. - A built-in browser the agent and the panel share. Playwright renders in the
launcher and exposes a CDP port; the Playwright MCP is pointed at that port, so a
plugin that believes it owns a browser drives the very page the agent'sbrowser_*
tools and the panel see, instead of a second and invisible one. The panel gains a
read-only "Agent browser" tab that polls frames — a change made over CDP, which calls
none of the session's own methods, still shows up a poll later. Input is never
forwarded: watching is the feature, driving stays with the agent. - A terminal in the file rail, hosted by the launcher. A shell is a workspace tool,
not an agent capability, so it must work with the agent stopped — it runs in the
launcher and the panel polls its output. The shell is picked from what the machine
actually has (cmd / PowerShell / pwsh / Git Bash / WSL / zsh / fish), you type in the
terminal surface itself, and a terminal now keeps its scrollback across a tab switch,
a panel close or a reload because it is addressed by slot and the launcher keeps its
buffer. There is no PTY: the panel says so rather than letting a full-screen program
look broken. - The right panel becomes 浏览器 / 终端 / 文件, and the workspace pane's tab bar
offers all three. - A first-launch tour, with the language switch inside it.
- A messenger-style chat layout for the agent web. The rail becomes one contacts
list for group chat and 1:1; talking to one agent gets its own whole-page layout with
bubbles, attachments in an always-visible footer, quoting, paged history and history
search. Both rails share one footer action set. - Collaboration, end to end. A collaboration-task card protocol with participant
state, a task-scoped channel agents talk in, and a task window that draws the work the
way the board does and renders markdown progress. An invitation is delivered to the
agent and only the coded invite is shown; work is assigned once the whole team has
accepted; one collaboration runs at a time, enforced at every door in; 补充 asks for a
change on an approval card without deciding it; the closure is announced in the window. - Pairing across machines that survives a restart. The invite names this machine's
LAN address instead of127.0.0.1, the host is detected rather than typed, a paired
machine shows whether it has ever connected, and a restart resumes the pairs instead of
forgetting them. - Relay: queue, retry and backfill a push the peer could not receive.
- Guided, step-by-step setup for the plugins that talk to external services.
- A wake mode in the composer's + menu, wired to the agent's own setting, and a
cross-group task strip above the composer with the merged task list behind it. - A process says which code it is running, and shouts if the files beside it have moved
on. A fix that landed after a process started is not running, and that has been
mistaken for "the fix did not work" more than once. - A per-agent token statistics page in the session sidebar, with the input split and
cache share on the cards. - A long group message folds to its first stretch, with a faded edge, and a plugin's
full details open in a dialog. - Mods: the compatibility surface is documented, with the host's trust boundary
written down (declared capabilities, install-time consent) instead of implied. - Desktop: background updates that don't interrupt work. The app already
downloaded installers in the background, but only when the user clicked
"download in background", and installing always forced an immediate restart.
A new Automatic updates section in Settings → About adds three switches:
download new versions in the background (on by default — the main-process
checker starts the download as soon as a release is found, deduped against the
manual button), install on quit (on by default — a downloaded installer is
applied when the app is closed normally, instead of a forced mid-work
restart), and restart & install when idle (off by default — restarts only
after no agent work has run for a couple of minutes). Preferences persist to
update-prefs.jsonin userData so the main process can auto-download and apply
on quit even before the renderer mounts. The install itself is unchanged — the
silent NSIS installer still replaces the files while the app is closing — so
this changes when the restart happens, not whether the app restarts. - Model cards: the OpenAI
/v1/responsesprotocol (api_protocol: openai_responses).
OpenAI now ships two wire formats, so the model-card editor's API Protocol
control is a dropdown offering both:OpenAI Chat Completionsand
OpenAI Responses(the newopenai_responsesvalue), alongside
Anthropic Messagesand the other providers.openaiandopenai_compat
are one wire format (both driveChatAPIover/v1/chat/completions), so
they share a singleOpenAI Chat Completionsentry; a card keeps whichever
of the two values it already had.ResponsesAPI
(opensquad/responses_api.py) reusesChatAPI's whole turn engine — context
compression, retries, stop handling, Native Function Calling, usage accounting
and session persistence — and swaps only the transport: a small client bridge
converts the request intoinstructions+inputitems (tools flattened to
the Responses shape) and translates the typed event stream
(response.output_text.delta,response.function_call_arguments.delta,
response.completed, …) back into the Chat-Completions chunk shape the shared
loop reads. Existing OpenAI models keep working unchanged; only cards set to
openai_responsesuse the new endpoint. - Agent Web: a scheduled task's detail pane now lists its run history.
The pane showedRun count 90with nowhere to see any of those 90 runs — the
history only existed in the 执行 tab, mixed across every task. The detail now
shows that task's runs in a bounded, scrollable box (max 320px) split by local
day (9/24, 9/22, …), each row carrying status, start time, 手动 marker, error
and duration, and each row opens that run's workflow. Because the store keeps a
window (≤200) whilerun_countkeeps counting, the header says
显示最近 49 次,共 90 次rather than passing either number off as the other.
While there, the pane's labels/statuses/schedule summary follow the UI language
(common.enabled,scheduledTasks.info.*,scheduledTasks.status.*) instead
of the hardcoded English the same screenshot showed. - Agent Web: goal mode is reachable from the "+" attach menu. It existed only
as a hand-typed/goal, so it stayed invisible to anyone who never guessed the
command. The new 目标模式 row inserts that exact text and focuses the composer
rather than adding a second implementation: the existing parser still opens the
subcommand picker (pause / resume / clear), the objective hint and the send path. - Agent Web: a branch chip and a Local/Worktree mode switch in the composer
footer. Up to now the only way to see or change git state from the app was
to ask the agent to rungitfor you. Two compact chips sit next to the
folder. The branch chip names the current branch and opens a menu grouped by
recently used, with remote branches and their last-commit age, and supports
create / switch / delete — a dirty worktree is offered a stash first, an
unmerged branch a confirmed force delete. The mode chip switches a draft
session between 本地 and a per-folder Worktree (one stable worktree under
.os-worktrees/per root), and turns read-only once the session's first
message pins its cwd, so a running conversation never changes ground under
itself; the session-level cwd is pinned at that moment so local and worktree
sessions cannot drag each other along. A directory without a repository
renders both chips grey and inert (非 Git 仓库) instead of hiding them. The
launcher gained the matching/git/*routes (status/branches/diff,
the write + network operations, and the worktree prepare), which run each
repository's updates under a cross-process lock and run fetch/pull/push as
pollable background tasks. - Agent Web: committing from the file panel's 改动 view. The view listed what
the conversation had touched; it now also answers "what is uncommitted in git"
and acts on it. A 本对话 / 全部 switch chooses between the two lists —本对话
keeps its existing meaning (the session baseline) and全部adds everything
else git reports, marked with which files the conversation also touched. Rows
carry git's state and can be checked, and the footer stages, unstages, discards
and commits: a discarded selection asks first, and asks again about deleting
files git does not track yet; the commit form names what is missing (a title,
then a non-empty index) rather than letting the refusal come back from the
server; and 撤销上次提交 appears only while the last commit can still be local
(no upstream, or commits ahead of one) and asks before running. Each row's diff
comes from the same viewer as the rest of the panel, read from the worktree or
the index depending on where the row actually is.
Fixe...
v0.9.0-beta.2
Changes since v0.9.0-beta.1
- chore(release): prepare v0.9.0-beta.2 (3c598ed)
- fix(tools): native mode falls back to XML when the provider's native FC is not implemented (6fb65a3)
- fix(ci): the two red tests my own changes caused (fbc62ba)
Full Changelog: v0.9.0-beta.1...v0.9.0-beta.2
v0.9.0-beta.1
Changes since v0.9.0-alpha.5
- chore(release): prepare v0.9.0-beta.1 (383ecd0)
- fix(collab): the ghost-id filter must read the id out of the record, not test the record (d0045f2)
- chore(gitignore): ignore the task stores and session-cwd signal files (b483093)
- fix(gateway): refuse a pane message that has no session to be delivered to (c93f22c)
- test(cwd): pin concurrent-turn isolation with a real interleaving test (7478a73)
- fix(cwd): a gateway message with no session id applies no working directory (54593f1)
- fix(cwd): report and describe the executing session's folder, not the process-wide one (fe2f104)
- feat(cwd): log which session asked for a working directory, and what it got (9d2a848)
- fix(cwd): every call that sets a working directory names its session (68ccb93)
- fix(chat): starting a session no longer stops a turn running in another (d3cd986)
- fix(cwd): the turn's directory comes from the message, not from the last session seen (cc4df9c)
- fix(cwd): the workspace switch scopes the directory to the session that asked (3637d93)
- fix(cwd): a session with no folder of its own reports nothing, instead of borrowing another pane's (ea0379b)
- fix(cwd): a working directory belongs to its session, not to the whole agent (1743c6a)
- fix(collab): a gate gets one live card, however many sessions ask for it (2116231)
- feat(chat): a remote member says where it is once, not twice (1d2c55b)
- fix(chat): a mention highlights the name, not the whole message (8f14dff)
- fix(chat): a new session is anchored to the workspace on screen, not the chrome's active one (587f17b)
- feat(chat): the pane's welcome offers a new session, not only the views (de4a40d)
- fix(chat): the task capsule shows the running count as a number, 0 included (5491db6)
- fix(chat): the task capsule counts what is running, and says 无 when nothing is (f54dd57)
- feat(collab): 补充 on the approval cards — ask for a change without deciding (e809843)
- docs(collab): the four message rules — @ the user sparingly, talk in the task window, deliver in a sentence (fba70e7)
- fix(collab): the collaboration card keeps reading the board, so it changes without a refresh (15a257e)
- fix(gateway): a relayed member is matched by the agent the peer named, not by a user id from elsewhere (d3f4cbc)
- fix(pairing): the LAN address is asked for under /ai-web, which is why detection never worked (bc41f05)
- feat(pairing): only the coded invite is shown, and the host is detected rather than typed (9a80545)
- fix(chat): fold only what a person wrote, at a thousand characters, and keep its formatting (2c004aa)
- fix(chat): a long message folds wherever it appears, tables and code included (a2a7ef2)
- fix(chat): only the folded edge is blurred, and the rest of the message stays crisp (1ad7535)
- fix(chat): the folded tail fades on the text itself, using the panel's own mask (08ff883)
- feat(chat): a long group message folds to its first stretch, with a faded edge (df2b5d9)
- fix(chat): the wake-mode row narrows the optional handler inside its own closure (0ed9bac)
- fix(collab): the collaboration card shows the board's state, not the snapshot in its own message (f9c3b3b)
- test(collab): the whole chain, end to end, against a real board (b9317eb)
- feat(build): a process now says which code it is running, and shouts if it is the older one (6d65659)
- fix(collab): a blocked gate now says which gate, how long, and that it never opens by itself (ff228bf)
- fix(chat): the new-session greeting is back, and the tip that stayed showed why (d1ad416)
- docs(collab): the documentation now describes the board that exists, and a test keeps it that way (d52c771)
- fix(collab): a board read-modify-write is atomic across processes, and the lock fails loudly (3e43fda)
- feat(chat): the wake mode choices move behind a second-level row in the + menu (7b1fb41)
- feat(chat): wake mode in the composer's + menu, wired to the agent's own setting (cfe7be1)
- feat(models): the provider filter is a select, not a tag per vendor (183656b)
- fix(terminal): a character split across two reads survives, and output stops repeating (4e1cf27)
- fix(chat): the task strip sits after the @ button on the composer toolbar (faf9c31)
- fix(gateway): a group member on a paired machine no longer reads as offline (c4533d9)
- feat(chat): the task strip is a control on the composer toolbar, and it looks like one (c6bdae3)
- feat(chat): a cross-group task strip above the composer, and the merged task list behind it (f7d479e)
- fix(chat): a re-delivered steer no longer repeats as a new row in the tool stream (e56be9b)
- fix(collab): a member id that cannot be an agent name no longer blocks dispatch (5306ca8)
- fix(collab): members given as a string no longer becomes one member per character (064c329)
- fix(chat): switching away and back no longer loses your place in a conversation (9381bcf)
- fix(collab): board_view lost three zones, a subscription advertised an unreachable address, and a redundant push is reverted (4415e09)
- fix(collab): the gate check now reaches the machine that owns the board (937c1cd)
- fix(collab): a worker that accepted no longer looks invited on the collaboration card (19cedf1)
- fix(models): assume Function Calling for an unlisted model instead of forcing XML (14399b2)
- fix(models): a model card can declare Function Calling, and the card wins (d1ff1ad)
- fix(presence): a member on a paired machine is not offline, it is elsewhere (8a5ab38)
- feat(collab): an agent's task-window message is pushed now, not only readable (6a03826)
Full Changelog: v0.9.0-alpha.5...v0.9.0-beta.1
v0.9.0-alpha.5
Changes since v0.9.0-alpha.4
- chore(release): prepare v0.9.0-alpha.5 (f4fbf9c)
- fix(relay): the group-message branch had the same lost-push bug, and a loopback subscription now says so (0c38d9d)
- fix(relay): a task push that missed once was lost forever, and our own docs called it a design boundary (697ee63)
Full Changelog: v0.9.0-alpha.4...v0.9.0-alpha.5
v0.9.0-alpha.4
Changes since v0.9.0-alpha.3
- chore(release): prepare v0.9.0-alpha.4 (8ff5351)
- fix(peers): make a rejected pairing token say which way it failed (f4e6090)
Full Changelog: v0.9.0-alpha.3...v0.9.0-alpha.4
v0.9.0-alpha.3
Changes since v0.9.0-alpha.2
- chore(release): prepare v0.9.0-alpha.3 (1a483ef)
- fix(collab): a worker joining from another machine can reach the board again (0762c40)
- fix(approval): read the marker the way it actually arrives - a hand-built card with raw newlines (cc0f795)
- fix(approval): read the marker the way it actually arrives - a hand-built card with raw newlines (8fd6c27)
Full Changelog: v0.9.0-alpha.2...v0.9.0-alpha.3
v0.9.0-alpha.2
Changes since v0.9.0-alpha.1
- chore(release): prepare v0.9.0-alpha.2 (2abcce2)
- fix(approval): an approval card is a card, and an answered one is a quiet line - never raw JSON (240f4c4)
- fix(im): list_groups must include the groups joined on a paired machine (a2eb684)
Full Changelog: v0.9.0-alpha.1...v0.9.0-alpha.2
v0.9.0-alpha.1
Changes since v0.8.49
- chore(release): prepare v0.9.0-alpha.1 (bac0f76)
- fix(ci): the last Linux failure - my product fix exposed one more Windows-shaped fixture (4ff2250)
- fix(ci): the four remaining Linux failures, from Windows-shaped fixtures (96a2d83)
- ci(browser): give CI a real Chromium, and skip the session tests where there is none (0ad3f30)
- feat(desktop): background updates that apply on quit (452e6dc)
- feat(model-cards): add the OpenAI Responses protocol (openai_responses) (765b243)
- fix(browser): say why a page is blank instead of leaving it a mystery (12b0e34)
- revert(browser): the panel is the user own browser again - real DOM, no agent session (b7df0c0)
- feat(browser): a real browser window (option A), shared with the agent (dbf3770)
- fix(browser): show the page at real size, and make the view usable (46c1b50)
- feat(browser): the panel is a view of the browser the agent drives (4b5fbfa)
- feat(browser): browser_use — the agent gets hands on the built-in browser (9193fb5)
- fix(terminal): talk to a Windows console shell in its own code page (49be650)
- feat(browser): a Playwright session the agent's tools and the panel both drive (8475af3)
- test(terminal): assert the picker's calls as they are written (29b3e1f)
- feat(terminal): pick the shell this machine has, and type in the terminal itself (0e5e75c)
- fix(terminal): a user's terminal is not fenced to the agent's workspace (d780667)
- feat(agent-web): the terminal is hosted by the launcher, not the agent (ffd7ef7)
- fix(agent-web): the rail tabs were gated behind treeOnly, and the terminal never dialled (00705b9)
- feat(agent-web): the file rail's title row becomes 浏览器 / 终端 / 文件 tabs (bf77f1b)
- feat(agent-web): terminal and browser get visible buttons in the tab bar (4460460)
- fix(agent-web): put the terminal/browser entry where it is actually seen (70b79a1)
- feat(agent-web): terminal and browser are tabs of the workspace pane (dc95aa4)
- feat(agent-web): the right panel becomes browser / terminal / files tabs (5d23359)
- feat(plugins): guided, step-by-step setup for the external-connection plugins (3f72492)
- feat(relay): queue, retry and backfill a push the peer could not receive (df5c8ae)
- fix(collab): commit the shared task-step parser the window and board import (bafb589)
- feat(collab): the task window draws work the way the board does, and the project has a directory (3f39a50)
- feat(cross-machine): the invite names this machine's LAN address instead of 127.0.0.1 (beed9c0)
- docs(collab): the PM is told to split by boundary, not by volume (6efd738)
- feat(collab): work is assigned only once the whole team has accepted (0bb7112)
- feat(cross-machine): a restart resumes the paired machines instead of forgetting them (02575f0)
- feat(collab): the closure is announced in the task window, and the rule leads the prompt (30b585a)
- feat(collab): an agent's task message wakes the other members, and the prompt says who the window is for (21a6265)
- feat(collab): assignments stop posting cards, an answered approval leaves the chat, and invitations are delivered to the agent (52939b4)
- fix(collab): the creator is a member at once, and a gate with no card reads the board (10f16ea)
- feat(collab): one collaboration at a time, enforced at every door in (ee16ffe)
- feat(collab): the gates and the invitation are enforced, and the board reads as a board (caa70db)
- test(collab): the board's peer token is read from either store (39771a3)
- feat(collab): the task thread is the group chat's bubble, pictures included (0a11c68)
- fix(collab): a task-window message wakes a strict agent, and the board finds its peer token (128b117)
- feat(collab): the task window renders markdown and shows progress; the prompt draws the task-vs-group line (b029bd9)
- fix(relay): a message from the other machine keeps its files fetchable (8a111b0)
- feat(relay): a task-window message crosses to the agent on the other machine (b7e2908)
- fix(cross-machine): the agent-push frame is the canonical one, and leaving a peer unsubscribes (62eb68f)
- fix(relay): the agent no longer receives a relayed copy of its own message (1d24418)
- fix(cross-machine): joining by invite subscribes, and a stale peer entry cannot shadow a fresh one (c559b37)
- fix(relay): the agent-push route relays, so a server-side push reaches a paired agent (f36296a)
- fix(relay): one file per writer, a bounded fan-out, and a secret bound to its user (058df50)
- feat(group): mark members that joined from a paired machine (af97bb0)
- chore(gitignore): ignore Command Code state and the _promo scratch dir (5ada945)
- feat(cross-machine): a group joined on a peer now relays its messages home (e35aaf2)
- fix(cross-machine): collaboration notices go to the group's owning machine (024476a)
- docs(cross-machine): the relay the interim arrangement is standing in for (08e943c)
- fix(cross-machine): a board joined on a peer routes back to that peer (33d7b33)
- docs(skill): the board routing a paired peer does not get for free (70cb671)
- fix(cross-machine): pairing a second machine no longer takes the agent off its own (f386eec)
- fix(agent-web): a message that arrives mid-turn renders in the tool flow, live (2aa6f4a)
- fix(boot): the cross-machine join tools are mandatory, not config-gated (66b6f7c)
- feat(cross-machine): one string to send, and a skill so the agent drives it (5cddf45)
- fix(ui): an invite names the gateway, not the page the browser is on (0db9ffe)
- fix(agent-web): a mid-turn DM is labelled by its origin, not as 插话 (c04d495)
- fix(ui): bucket token statistics in the viewer's local time (6bc1a4b)
- Revert "fix(runner): a private message that lands mid-turn is not a 插话" (b817408)
- fix(runner): a private message that lands mid-turn is not a 插话 (fa0c911)
- fix(ui): a quoted private message no longer prints its marker (c1ef4fd)
- test(invite): drop a monkeypatch line that patched nothing (25979ac)
- test(nodes): cover the peer side of pairing end to end (d4359d1)
- feat(ui): the pairing panel — code, waiting machines, paired machines (fb59a67)
- feat(ui): the group page shows the invite string and the join queue (cfc3040)
- feat(ui): invite strings and the join-request API, ready for the group panel (a02952d)
- feat(nodes): spend the peer token where it is needed (5a3fa45)
- feat(nodes): the peer side of pairing — paste the code, get a token (bcf0ca9)
- feat(nodes): a paired machine gets its own scoped token (62684c1)
- feat(invite): join a group on another machine by pasting one string (7b7eedc)
- test(groups): case-insensitive membership-insert assertion (1f36441)
- feat(groups): a member can see the asks, the owner decides them (446d1de)
- feat(groups): a private group can be asked to join (b4472f0)
- feat(nodes): bind an @ai account to the node that claimed it (d22a648)
- fix(im): say why an @ai account could not be taken over (ea79209)
- fix(gateway): refuse to steal an account that is in use (1fccbdd)
- fix(launcher): no token means localhost-only, not wide open (35d8037)
- fix(gateway): a token may only reset its own password (721332e)
- feat(collab): the user talks in the task window too (27d1f74)
- test(collab): repair the attachments suite after the board-only change (81ef1b2)
- feat(im): one send tool, a task id decides group vs task (aaffc11)
- feat(collab): keep task talk out of the group chat (1c2f619)
- feat(chat): one markdown pipeline for group chat, plus working @mentions (9c3326e)
- feat(collab): attach files and images to a task, from any machine (a1b0ff8)
- fix(remote): talk to the right gateway and fetch its uploads (e417ffd)
- docs: how to run collaborating agents on several machines (ca9eebb)
- feat(collab): one board per group, owned by the gateway (630ac8f)
- feat(collab): approval gates inside the task window (772fd6f)
- docs(prompts): tell agents when to send a window card (2910afb)
- feat(cards): cards you can answer, not just read (0499e8d)
- feat(cards): generic, agent-sent window cards for group chat and DMs (280ce7b)
- feat(ui): quote a message in the DM window (9e904de)
- feat(collab): accept an invite from the card, close the loop on finish (3a7066c)
- feat(ui): collaboration cards open a task window (e9e2888)
- feat(collab): agents talk in a task-scoped channel (5526ccc)
- feat(collab): collaboration-task card protocol and participant state (822ee6f)
- feat(ui): attachments and an always-visible footer in the DM window (00c56ec)
- feat(ui): show paths and line deltas for Code's changed files (e7f81a5)
- feat(ui): one footer action set for both rails (59873c8)
- fix(agent): keep one event-notification block in the request (7c67be5)
- fix(plugins): declare sensevoice's bundled ffmpeg again (f498294)
- fix(agent): deliver each inbound event exactly once (fae1341)
- fix(agent): drop a duplicate im.send_message inside a short window (76871b2)
- feat(ui): make the rail footer fit the layout it sits in (57c3c35)
- fix(ui): give the DM window's drawer a real project and status (8dff7bb)
- refactor(ui): make chatting a single surface, not two (b5b8ffc)
- fix(ui): give the DM window the detail drawer back (dfbca80)
- refactor(ui): point the 聊天 switch at the contacts shell (47c756b)
- feat(ui): page a long DM thread instead of loading it whole (b9647e5)
- perf(ui): make the DM window live off the websocket, not the poll (51a4910)
- feat(ui): search the DM history in the chat layout's detail drawer (050268a)
- feat(ui): talk to an agent over the DM channel in the chat layout (e3f50ea)
- feat(ui): bring the join/create group panel into the rail (0945630)
- style(ui): give the group chat the chat layout's stage padding (7e1724d)
- feat(ui): one contacts rail for both group chat and 1:1 chat (9a8066c)
- feat(gateway): let /direct-messages take a contact and a keyword (a715b96)
- feat(ui): drop the tab strip from the chat layout (01b2d1a)
- feat(ui): make chatting its own whole-page layout, bubbles and all (e11f3f5)
- feat(ui): put the input split and cache share on t...
v0.8.49
Mostly a reliability cycle driven by three field reports from a pip-installed
deployment: plugin services that crashed or stalled at boot, a reranker whose
1.2GB of weights could not be downloaded or found, model-download status files
that collided across processes, and a built-in ASR card that was never shipped
in the wheel. Two long-standing invisible-config gaps went with them — a
downloaded model and an edited voice setting both now reach the running
service/agent without a restart.
Added
- Test builds (alpha / beta / rc) are now installable the way a real user
installs OpenSquad. Both release workflows used to exclude those tags, so
taggingv0.8.49-beta.1only built desktop installers — nothing reached PyPI
or npm, and the Release that was created was not flagged pre-release (i.e.
the update check would have offered it to stable users). Everyv*tag now
runs the full pipeline: the PyPI upload carries the PEP 440 spelling
(0.8.49b1, still invisible topip install opensquad), npm publishes under
thenextdist-tag (npm install -g opensquad-ai@next), Docker is skipped so
:latestcannot move, and the GitHub Release is flaggedprerelease. Testers
install withpip install --pre opensquad==0.8.49b1; stable users keep
getting the last stable release from every channel. The tag↔version pair is
validated up front byscripts/sync_version.py --check-tag. - The model-provider list can be refreshed, and a configured provider's API key
can be replaced. The "connect provider" dialog offered about a dozen vendors
while the catalog holds 225 providers / 8292 models: a v1localStoragecopy
won over the backend, the in-memory copy had no TTL, and the refresh button had
been deleted (its i18n keys were still in the bundle, unused) — so a stale
short list could never heal. There is a refresh action again, and the dialog
now states the provider count, where the list came from and whether it is
stale. Each configured vendor gets a "change API key" action that rewrites
api_keyon the vendor's existing model cards in place (merge semantics — the
parameters a user edited by hand survive) instead of asking them to edit JSON.
On the backend, a failed upstream refresh no longer quietly keeps an old disk
cache: after 7 days without a successful fetch the bundled catalog is preferred
and the response says so (source,cache_age_seconds,cache_stalein
meta).
Changed
- The
whisperplugin is no longer shipped. Its declared dependency
openai-whisperwas never what provided thewhisperimport: that name came
fromwhisper1.1.10, an unrelated round-robin-database package, whose import
raisesTypeError: argument of type 'NoneType' is not iterable. Installing
openai-whisperon top would not have fixed it either — both distributions
claim the same top-level module. So the launcher's dependency self-check
reported the dependency missing on every start and refused to start the
service: a plugin nobody could use, surfacing as a service that would not run.
The plugin, its admin panel (plugin-views/whisper/) and its tests are gone.
The ASR surface is unchanged — sensevoice is still the built-in ASR card,
services.whisper_urland thebuiltin_service: whisperhandling are
untouched, and a Whisper-compatible service you run yourself on that port
still works.
Fixed
- Plugin services crash-looped on a pip install with
ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'.external_api,feishuand
telegram(adapter + config), the feishu/telegramsend_tools,
websearch/websearch.pyandplugins/plugin_manager.pyput a computed project
root at the front ofsys.path. In a pip layout that root is a
site-packages (…/site-packages/plugins/external_api/adapter.py→ three
levels up), and plugin services are executed by the bundled Agent Python (3.11)
even when the tree was installed by 3.12 — soinsert(0)shadowed the runtime's
own compiled packages with cp312 binaries, andpydantic_core/__init__.pythen
could not find its own_pydantic_coreextension. They all append now, which
keepsplugins.*/opensquadimportable without giving them priority.
Reproduced against the real bundled 3.11 runtime with a 3.12-tagged
pydantic_coretree: exactly the reported error withinsert(0), clean import
withappend. - Every plugin service waited ~66s at boot because of a dependency no running
service used. The startup batch answered "is this declared pip dependency
installed?" by importing it in the plugin interpreter. A cold
import lark_oapimeasures 10-13s and blew the 60s probe budget under startup
load, and the batch is a global gate (_plugin_deps_ready), so one disabled
feishu's dependency kept websearch, sensevoice and the rest in "Starting…" while
the agent's calls to 127.0.0.1:9001 were refused. The batch now answers from
importlib.metadata— one cached subprocess for the whole run, an installed
distribution is "present" with no import — and no longer pre-installs
dependencies for services that are disabled or not set to auto-start; those are
installed on demand when the user starts the service. Measured here: 29 light
dependencies went from 26.3s to 3.6s with identical verdicts. The import probe
stays as the fallback for a dependency that is not installed (it fails fast),
and the per-service check keeps using it. - The reranker weights could not finish downloading through the mirror, and a
stale "Download failed" outlived the download. The legacyhuggingface_hub
fallback setHF_ENDPOINTandHF_HUB_DISABLE_XETinside the download thread
— i.e. afterimport huggingface_hub, which freezes both into
huggingface_hub.constantsat its own import time (verified on
huggingface_hub 1.26.0: they keep their old values whatever the environment
says later). So the download went to huggingface.co rather than hf-mirror.com,
and with Xet enabled the 1.19GBmodel.safetensorsdied at ~79% on a 401 from
cas-server.xethub.hf.co, which the mirror does not proxy. Both variables are
now set before the import. Separately, once the weights had been completed by
another path nothing ever cleared the persistedstate: error, so the card kept
showing "Download failed: HTTP 502" for a model that was already loaded —
reranker_model_store.get_status()now reconciles a complete model toready
instead of reporting a failure that no longer applies. - Plugin services were told their dependencies were not installed when they
were, so websearch / sensevoice / feishu / telegram refused to start. The
launcher decides by importing each declared pip dependency in the plugin
interpreter. Two things made that decision wrong. (1) The probe guessed the
import name from the distribution name, sopython-telegram-botwas probed as
python_telegram_bot— a module that has never existed under any version. The
package was reinstalled on every startup and reported missing every time, the
circuit breaker opened, and the service never ran; the live launcher log shows
six such failures. The import name now comes from the interpreter's own
metadata (importlib.metadata.packages_distributions(), re-read after every
install), with the static map as an explicit override and the dash-to-underscore
guess only as a last resort. (2) The probe budget was 15 s and a timeout was
read as "missing". Cold imports are not fast — measured on the bundled Agent
Python,lark_oapitakes 10-13 s andtorch~4 s — and the box is busiest
exactly when the launcher starts a dozen services, so a healthy dependency
timed out and blocked its service. Probes now have 60 s, verified modules are
cached (a positive cannot become a negative without an uninstall), and a probe
that still runs out of time is reported as inconclusive: it is installed to
be safe and the service is started, with a warning, instead of being refused.
A realModuleNotFoundErrorstill blocks the start, as before. Separately,
plugin children were handed the launcher's ownPYTHONPATH(its 3.12 package
tree) although they run on a different interpreter;_build_child_process_env
now takes the child's interpreter and clearsPYTHONPATHwhenever it differs
from the launcher's, mirroring what the frozen branch already did. Agent
children are unaffected — they resolve to the launcher's interpreter. - An agent boot could freeze the whole process stack, with nothing in any log
saying why.opensquad startpipes each child's stderr and only read it
after the child exited. A pipe holds ~4 KiB on Windows; once it filled, the
child's next write blocked forever — and here that write was a log call made
while holdinglogging's handler lock. Every thread that logs then queued
behind the lock, so the launcher never reached the line that starts the
log-forwarding thread for the agent it had just spawned, so the agent's own
stdout pipe was never drained, so the agent's event loop froze inside its own
log write: agents never registered and the UI showed "reconnecting" until the
tree was killed. Three independent guards now break the chain, any one of
which is sufficient: (1)opensquad startdrains every child's stderr from
the moment it is spawned, viaStreamTail— a daemon reader with a bounded
ring buffer, whose tail is what gets printed if the child dies; (2) the
launcher starts a child's log-forwarding thread before logging anything
about that child (agent and plugin-service paths both); (3) the console copy
of every logger is a bounded queue drained by a daemon thread
(log_setup.nonblocking_console_handler) whose console writes go through a
private duplicate of the file descriptor, so a stalled console drops records
instead of blocking the ...
v0.8.49-alpha.5
Changes since v0.8.49-alpha.4
- chore(release): prepare v0.8.49-alpha.5 (8ccefb9)
- feat(plugins): pick up a downloaded model and edited voice settings live (0b3d838)
- fix(websearch): load the reranker from the workspace, and migrate old weights (87559a9)
Full Changelog: v0.8.49-alpha.4...v0.8.49-alpha.5