# Changelog One line per change, newest first. The README keeps only the last few; everything is here. The matching code is in the [commit log](https://github.com/CatCatUncle/openworkbuddy/commits/main) — each message says *why*, not *which file*, so it reads better than this list. 中文:[CHANGELOG.md](CHANGELOG.md) ## 2026 - **Nov 10** v0.19.3: fixes attachment names with a Chinese closing parenthesis being cut short. 报告(终版).docx no longer shows up as a broken 报告(终版 attachment, and local engines get the full path. - **Nov 6** v0.19.2: fixes library tabs staying highlighted when Notes is open. All Files, Recommended and other tabs now clear, so only one tab is selected at a time. - **Nov 6** Evals add blind comparison: two runs' results side by side per task with sources hidden, revealed only after every task is judged. Self-evolution proposals can run an eval with the candidate rule and compare against the baseline before adoption. - **Nov 6** The admin console adds Member Access and Storage pages: control which connectors and skills members can use, turn on read-only task sharing within an organization, see per-account disk use and set per-member quotas. Connector setup gains built-in checks that catch download-and-run commands, key file paths and cloud metadata addresses without toolward installed. - **Nov 6** Memories can belong to a project, so one client's terms stay out of another client's tasks while global memories still apply everywhere. Self-evolution now counts corrections in the next message as negative feedback instead of relying only on rarely clicked thumbs. - **Nov 6** The canvas adds a / quick panel (or double-click empty space), image grid splitting, batch video creation and generation for selected cards, and saved workflows that keep prompts, settings and links without media or results. Measured card height no longer takes an undo step. - **Nov 6** The engineering lane adds a working-folder bar and a changes panel: folder, branch and change count above the input, per-file diffs, and discarding one file's edits. Folders with source code run in place, matching the CLI. Git calls made by the app now run in the sandbox and override repository settings that can execute commands, so an agent cannot escape through .git/config. - **Nov 6** v0.19.1: multi-user Linux now runs member commands in a bubblewrap sandbox and refuses them if isolation cannot start, closing a blocklist bypass that could read key files (reproduced locally with a test key; no production incident found). **Upgrade note:** install bubblewrap first on Linux multi-user deployments. The Docker image includes it and compose must keep SYS_ADMIN and related options, or member commands are refused. Set `security.allow_member_unsandboxed` to opt out. If you gave out member accounts before upgrading, consider rotating keys. macOS and Windows already had a system sandbox. - **Nov 6** The library responds immediately with tens of thousands of files: scans moved to a cached background thread, views paint existing content before refreshing, and listing fell from about 100ms to 5ms. Concurrent tasks queue by available memory, heavy rendering has its own limit, child processes run at lower priority, and queue time does not count against task deadlines. - **Nov 6** CLI coding profile: 75% less prompt text per turn (49,635 to 12,643 characters), tools down from 44 to 23 with load_tools on demand, in-place runs inside source folders, and a --profile flag. - **Nov 6** Eval sets can be uploaded as CSV, Excel, TXT or JSON, or built in the app. Criteria support weights, required items and penalties with HealthBench-style scoring, and human and judge agreement reports Cohen's kappa. Three office tasks cover PPT, Word and Excel. - **Nov 6** Built-in Ark retirement table warns ahead of shutdown and hides retired models; image and video generation fall back in order without resubmitting accepted or moderated jobs. Doubao voice, transcription and custom speech APIs are added, built from the official docs; test them against your account first. Admins can control member-supplied models, and first owner setup asks for the password twice with a reminder. - **Nov 6** Hover a message to see its time. Reply actions are icons with a short token count, and ratings sit on the right. Jump from a reply to its trace and back, and read TTFT, TPOT and tok/s in traces. The sidebar lists projects directly without pushing tabs away, sized to window height. AGENTS.md adds a licensing notice for AI assistants. - **Nov 4** v0.18.4: cloud onboarding supports selecting and refreshing models or entering Ark endpoint IDs. Discovery caches are scoped by URL, key and protocol; saved credentials cannot follow changed destinations. Fixed Claude/Gemini/Ollama discovery, Ark modality metadata and stale replies; refreshed Ark/DeepSeek defaults. Endpoint access errors distinguish unavailability from invalid IDs, and Seedance 2.x uses JSON options with current duration/resolution limits. - **Nov 3** v0.18.3: intermediate-file cleanup and library deletion use recoverable trash, with native desktop Trash/Recycle Bin or server-side original bytes and recovery records. Failures never fall back to permanent deletion. Generic output directory names are retained, dependency caches are unchecked by default, active work blocks cleanup, and the new UI refuses deletion through older servers. - **Nov 3** Fixed-width web pages and Office previews fit automatically on first display. Preview resizing, reveal and content changes trigger remeasurement while preserving manual zoom. Mixed-size OFD pages fit the widest page; resize callbacks are coalesced and listeners released on close. - **Nov 3** The built-in Agent periodically reconciles checklists and holds new actions after an ignored reminder while allowing inspection. Manual continuation restores successful unfinished checklists. Progress shows confirmation times separately from current activity, isolates expert plans, and preserves accurate empty, historical and unfinished states. Added the architecture comparison with Plan / Execute and LLMCompiler. - **Nov 1** v0.18.2: task deletion selects its dedicated folder for cleanup by default, with an option to keep files. Desktop builds use the system Trash or Recycle Bin; server builds use `.trash/tasks/` under the original workspace with a restore manifest. Project roots, shared folders and scheduled-task output folders are retained. Historical project folders are checked against both disk and in-memory ownership. - **Nov 1** Deletion waits for shutdown, uploads and final saves, then validates the confirmed folder identity. Symlinks, replaced folders and expired confirmations are rejected. Failures retain the conversation and report any partial trash moves. Success removes records, backups and archives, refreshes library entries and stale previews, and prevents final saves from recreating deleted tasks. - **Oct 30** Office previews add document pages, zoom and expansion. DOC/WPS and XLS/ET have built-in compatibility readers. Open With remains optional and does not change defaults. See the [format capability matrix](docs/文件预览与转换.md). - **Oct 30** Export Office, PDF and OFD to Markdown. OFD uploads retain extensions and bytes. Legacy PPT uses live-slide text; scanned pages and truncation are reported. CEB requires an authorized engine. - **Oct 30** Library previews use their own reading area with compact borders and separate title/actions. Old conversation drawers close, stale reads cancel, and spreadsheets load one sheet and 100 rows at a time. - **Oct 30** Inbox events stay in date order when read, including successful results. Project selection keeps shortcuts in place. CLI history has its own retention window and directory-based project ownership, with --project support. - **Oct 30** Temporary approvals are scoped to account and task. External files can be imported as working copies; legacy output links recover only with matching path, size and modification-time evidence. - **Oct 29** Library grouping and layout switches now update locally without reloading directory or task indexes. File rows, thumbnails and previews are reused, and offscreen layout work is reduced. - **Oct 29** Automation editing now gives instructions a larger area that grows with content, scrolls long tasks, and keeps Save visible on smaller windows. - **Oct 29** The sidebar shows up to five current and frequent projects, with pins, recent use and full-list search. The picker opens beside the Projects heading; editing and removal move into a row menu. - **Oct 27** Fixed same-name image mix-ups on the canvas. Thumbnails and previews stay bound to the original file and project; missing files are never substituted, and late file lists are ignored after project switches. - **Oct 27** The mobile app UI adapts to the visible viewport, uses horizontal settings tabs and full-screen file previews, and closes navigation after selection. - **Oct 27** Mobile website layouts now wrap navigation, resize headings and media, and improve English and Chinese documentation on narrow screens. - **Oct 27** Recommended skills now include software copyright template filling and resume coaching, installed on demand from the author's public repositories. - **Oct 27** Project and automation search preserve the input and caret, wait for IME confirmation, and reuse loaded lists. Ignore late run-history responses after switching views. - **Oct 26** Fix missing traces across projects, split ledgers when switching directories during a run, missing local-engine tool events, and duplicated usage totals. Show read failures explicitly, refresh the trace page automatically, and restore the full Experts · Skills · Connectors sidebar label. - **Oct 26** Library search preserves the input and caret, waits for IME confirmation, and ignores stale responses to prevent reordered text. - **Oct 26** Recover historical artifact root fingerprints after verified directory moves. Assistant API/local engine selection now matches execution without changing scheduled task engines. A dedicated Feishu message channel accesses the keychain through the app while preserving sandbox settings and receipt deduplication; explicitly unsent reminders are no longer reported as completed. - **Oct 25** Feishu bot notifications support an explicit recipient and verify message receipts through the app, without requiring a local engine to access the system keychain. Scheduled runs record execution and delivery separately; notification failures do not rerun tasks or advance the delivery baseline, and uncertain sends are not retried automatically. - **Oct 24** Keep task navigation in one desktop workbench. Remove the session menu’s new-window action, reuse the main window for task links and preview redirects, and retain separate file previews. - **Oct 23** Remove three dependency cycles; share workspace context across tools and connectors, and avoid loading the tool executor just to locate connector files. Skills and plugins share a pure metadata parser while keeping the existing skill export. Admin routes inject live seat counts into license facts; dependency-cycle exceptions are locked at zero. Regression checks cover concurrent media isolation, nested context restoration, independent loading and account counts. - **Oct 23** Open text-model edit/duplicate forms in their current overview, handle legacy models without channels, and preserve drafts during catalog refresh. Add confirmed local Feishu logout with pending sign-in cleanup and explicit server-authorization limits. Track scheduled Feishu delivery receipts in built-in tools and relayed CLI calls to prevent another bot/profile from resending the result; uncertain outcomes require verification and each scheduled run has independent state. - **Oct 23** Keep Agent model choices within the active app’s configuration. Supply the same media inventory to API and local engines, reject node writes with unregistered IDs, and restore the current default from invalid-model cards without triggering generation. - **Oct 23** Compute batch card growth before moving nodes, preventing repeated displacement, leftover overlaps and excessive link updates while preserving manual overlaps. Keep failures and pending jobs visible on compact result cards. Retain feedback while quotes load and cancel startup if the project changes during confirmation. - **Oct 23** Show immediate feedback when starting a generation list, recover from startup errors and time out stalled quote requests. Keep generation failures on cards. Stop duplicating shot outputs; merge unused old copies with undo. Preserve video-frame bytes, explain Ark portrait rejections without automatic resubmission, and accept approved asset IDs in explicit reference mode without mixing local frame inputs. - **Oct 22** Fix local engines mistaking withheld direct-generation tools for missing video configuration. Supply model IDs and the canvas proposal workflow through instructions and canvas reads, without credentials. Treat video and speech separately and retain confirmation before paid generation. - **Oct 22** Add inline descriptions and image-model selection to character and location cards. Show the default model, let an explicit default override stale script settings, and catch invalid or missing model configuration before submission. Refresh cards after model settings change and cover request parameters and result writeback in a real browser. - **Oct 22** Reworked canvas entry points and media cards: edit prompts, models, references and advanced options inline; compact existing assets when unselected; continue images as variations or videos, assign first/last frames explicitly and compare versions after renaming. Agent opens on demand; drama workflows start with the script. Added streaming canvas-and-asset export, incremental synchronization, frame-coalesced navigation and regression coverage for interaction and export boundaries. - **Oct 21** Opening the inbox no longer marks everything read. Keep up to 300 task notifications per account across reloads, include linked scheduled results, and filter all/unread/scheduled notifications. Add a visible conversation search button and clickable match snippets, retain previous/next navigation and older-message search, and clear stale results when switching conversations. - **Oct 21** Fixed double-escaped batch arguments in Windows installer verification. Main CI now checks the real command interpreter, bundled runtime, Unicode/space paths and launch directory before tagging. Failed candidates keep their tags and use a new version for the next release - **Oct 20** File links retain their project and full local path; previews and deployments no longer switch to same-name files or accept stale responses. Resolve external project roots for the local owner and cover relative assets. Release builds reuse only a successful main push of the exact commit within 24 hours; otherwise all tests run again - **Oct 20** Installed apps register a bundled-runtime CLI without changing the launch directory; source links can be repaired with `npm link`. Account menu labels stay horizontal and scroll within the window; the canvas entry moves below automation - **Oct 20** Reduced the home screen to two groups of five common tasks with input guidance; pin directly on task rows, rename and stop from the menu - **Oct 20** Disabled local CLI session imports and organization read-only sharing, including discovery, import endpoints and existing share links - **Oct 20** Streamlined account and chat controls; persistent read notifications, one-pass bulk updates and summary-only inbox payloads; distinct ended, stopped and incomplete states - **Oct 20** Extracted inline script validation and protected video publication against overwriting existing files - **Oct 20** Added explicit Windows installer updates and document thumbnails; source, portable and macOS builds keep their own update paths - **Oct 20** Clarify that source update checks only read GitHub Releases; require a clean, fast-forwardable checkout before pulling, and keep modified or divergent source trees intact while preparing an isolated copy that reuses the existing data root - **Oct 19** Fix the release CI list so it runs the Goal step-resume suite; add the missing test copyright line; redraw the architecture diagram around actual hosts, runtime, security gates and delivery paths - **Oct 19** Replace Mermaid with bilingual SVG diagrams to prevent rendering failures and clipped labels; shorten the README updates - **Oct 19** Show each task's creation date beside its title, add a direct rename button, and label previews from the active task as “task name · date / filename.” Keep stored directory paths stable so old links continue to work. Goal mode now checks unmet criteria and resumes in bounded rounds after a per-run 100-step cap. Explain that Codex CLI sandbox blocks do not create background approval cards - **Oct 19** Increase isolated HTML preview temporary `localStorage` and `sessionStorage` limits to 64 MiB each; they clear when the preview closes. Locally deployed pages use the browser's native storage quota, so keep using the page's export/backup feature for important data. - **Oct 19** HTML previews now use the full side-panel viewport, so interactive canvases and page scrolling are no longer shrunk to fit. After dimensions are reported, you can switch to fit-to-page; SVG and other static previews keep their existing fit behavior - **Oct 17** Give the CLI a terminal-specific interaction: one progress line and final results by default, `/details` and `--verbose` for tool output and diagnostics, `/usage` for usage; ignore blank interjections, execute unconsumed follow-ups in the same session, list only current changes, and open recent results with a local application while preserving approvals, errors and pipe protocols - **Oct 16** Fix unresponsive games and buttons in HTML previews: install isolated temporary storage before page scripts initialize; workspace and library previews retain their separation from application credentials and data - **Oct 16** Release checks: isolate packaging fixtures in temporary directories, include SHA-256 checksums, and summarize changes since the last published release rather than an unpublished tag; UI regression tests explicitly select their language and cover switching - **Oct 16** HTTP connector OAuth adds provider authorization, token refresh and local credential removal - **Oct 16** Preview region annotations and line comments enter editable revision drafts; individual change blocks can be reverted and restored - **Oct 16** Team tasks use organization models and connectors with separate usage records; browser recordings generate reviewable skill drafts - **Oct 16** English UI tests catch untranslated text while preserving user content; the website includes bilingual user and developer guides - **Oct 16** Organization-only read-only task sharing: owners create or revoke links; signed-in members see conversation text and delivered files. Tool arguments, configured credentials and out-of-root files are excluded. Sharing settings stay in the page and are hidden in single-user mode - **Oct 16** Bilingual READMEs put first success before feature details: install, configure, copy a report task, open and verify its file. Added troubleshooting, source-labelled links, trial timing definitions and an aggregate funnel template with missing metrics stated explicitly - **Oct 15** Execution plans are collapsible lists inside the conversation, with no overlay. Progress updates preserve expansion, keyboard focus and selected text; pause reasons and the resume button remain visible - **Oct 15** Added an inbox for approvals, questions, errors, running work and unread tasks. Results from standalone scheduled tasks appear there and open their execution history. A schedule created in a conversation writes its result back to that conversation; standalone schedules go to the inbox. The source session link stays on the server, and the API only reports whether one exists - **Oct 15** Task history is grouped by date and marks tasks that produced files. Row menus can rename, move, mark unread, pin, delete or open in a new window; drag a task into Pinned to keep it there. Workspace and library files can also be pinned in the sidebar - **Oct 15** Approval cards support Esc to reject and the primary modifier + Enter to allow once. Added shortcuts for new tasks and task search, plus a reminder below the composer to review important results - **Oct 15** Pin tasks in the history: hover a task and click the pin to move it into a Pinned group at the top, newest pin first; click again to put it back. Pins are saved on the server, so they survive another computer or a cleared cache, old pinned tasks never drop off the sidebar, and pinning doesn't change a task's order. If saving fails, the task moves back and you're told why - **Oct 15** Pages feel faster: Accept / Reject in Self-evolve no longer reloads the whole page — the button says it's working and the list updates in place. Self-evolve and Agents open without a multi-second wait: local CLIs are probed at startup, all at once, and a stale result is shown while a fresh one loads in the background. Skills, plugins, connectors and Account no longer flash "Loading" when switching, and Settings keeps your scroll position after a redraw - **Oct 14** On a shared server every account can bind its own messaging channels: in Assistant settings, scan your own WeChat, or add your own Feishu, QQ, WeCom app, Official Account, group bots, generic webhook and email. Messages run as you, with your own workspace, preferences and assistant model, and only you see the log; scheduled results and reminders go to the channels you bound, falling back to the server ones only if you bound none. Secrets come back masked, each callback URL carries a random token instead of your username, and a member's generic webhook rejects tasks until a secret is set. Disabled or deleted accounts are disconnected within a minute. The platform admin's channels are unchanged - **Oct 14** Members on a shared server get their own setup: the platform admin picks, per provider, which models members can use; members can add their own API keys, which only they can see and use, billed to them rather than the org's credits (the admin can turn this off for the whole server) - **Oct 14** Members can create their own experts that only they can delegate to; the platform admin can hide any of theirs from members one by one, and expert teams only list the experts a member can actually use - **Oct 14** Everyone can set their own assistant name, avatar and "My preferences": sent with every task after the org-wide preferences, without affecting anyone else - **Oct 14** WeChat QR pairing, disconnecting and the "Test" button on IM channels are now platform-admin only - **Oct 14** Attachments whose names contain the Chinese enumeration comma (、) are no longer split in two: a file like "这是一个测、试文件.txt" used to show up under the message as two files that don't exist, and local Codex / Claude Code never got its full path - **Oct 13** You can sign up with an email: the part before @ becomes your username, and signing in with the email works too. If that part is taken or not allowed, the page says so right away. Failed sign-ups no longer count toward the limit; one address can create 20 accounts before a 1-minute pause, then 3 minutes (it used to lock for 15 minutes after 5). - **Oct 13** New "Sandbox safety" switch in Settings → Security: turn it off and the AI can touch files outside the workspace, reach local and private-network addresses, and commands skip the system sandbox. The file blacklist and OpenWorkBuddy's own ports stay blocked. Can't be turned off with several accounts - **Oct 13** "Allowed local/private addresses" accepts wildcards: 192.168.1.*:* for a whole subnet, *.local:8080 for a family of hostnames, *:5432 for one port on any machine. OpenWorkBuddy's own ports stay blocked - **Oct 12** Your personal WeChat (connected by QR scan) now gets scheduled-task results and reminders: finished tasks and "message me on WeChat" requests are pushed there and survive restarts (send the bot one message in WeChat first). It used to push only to WeCom / DingTalk group bots, so the AI found no way to send and said it couldn't. Tasks that came from WeChat are no longer pushed back a second time - **Oct 12** Snappier WeChat connection: messages keep arriving while a task runs, so "stop" works right away instead of after the task ends; WeChat shows "typing…" while it works; a brief network blip reconnects after 1 second (10 seconds at most) instead of waiting up to a minute - **Oct 12** On the Automations page, "+ Add automation", "Add from template" and Edit now open in a dialog instead of expanding the page; Esc or Cancel closes it, clicking outside does not. A task reply that merely mentions "fetch failed" is no longer judged as an upstream connection failure - **Oct 12** Feishu "scan to set up an app" now accepts picking an existing app: choosing one counts as set up, and the QR-code noise from the terminal no longer ends up in error messages - **Oct 12** Local Codex / Claude Code can now read files sent with a message: pasted text, dropped images and documents are passed along with each file's full path. It used to get only the file name and search for it, and when the working folder sat inside a git repo that ignored the file, it found nothing and replied that it only got a file name - **Oct 10** Members, auditors and branch-org accounts can create multiple projects (workspaces) too: with more than one account, anyone on their own working folder can create, switch, rename and remove their own projects from Projects in the sidebar, and project notes, the attached library section, experts, skills and connectors are kept per project as before. Each project gets its own folder under projects/ in their own working folder; earlier outputs are all in the default project, which is the original working folder. You can't pick another folder; if a project folder is swapped for a link pointing elsewhere, it isn't followed and that run falls back to the working folder itself. Each person's projects are visible only to them, and the shared project list default-org admins use doesn't change. The default project is the working folder itself and can't be removed. Removing another project only takes it off the list, the folder and outputs stay; renaming keeps the folder, so old chats still open. After a project is renamed or removed, its old chats no longer vanish from the sidebar: renamed ones follow the new name, removed ones go to the first project, and the same goes for the admins' project list - **Oct 10** Canvas cards have a port on their right edge (shown on hover or when selected, always on touch screens). Drag it onto another card to link them; it's the same kind of link as before, so you can remove or undo it. A card that can't take the link turns red and says why (it can't use that card's content, they're already linked, or it would make a loop), and letting go there links nothing. Dropping on empty space or on the same card, or pressing Esc, leaves no stray line, and moving cards, box-select and panning work as before. Click the port for "Add linked card": it lists only the kinds that can link, and the new card lands in free space to the right, already linked; one undo removes both. On a selected card you can edit the title, name or prompt in place: Enter saves a single-line field and starts a new line in a multi-line one, clicking elsewhere saves, Esc cancels, and Enter or Esc while an input method is still composing is ignored. The inspector updates at once, and the edit is the same undo step as editing there. For member accounts, the canvas folder menu no longer offers "Choose another folder" - **Oct 10** Channels can pick which API images, video and voice use: the channel form gains Image API / Video API / Voice API selects, "auto" by default with the API it will use shown in brackets, and relays can choose too; a choice overrides inference from the model, channel type and address. Test on Qwen image models no longer shows a false 400: it used to send the image model as a chat request, to a chat path appended to the native Model Studio address, which could only return 400; now image, video and voice models get a free model-list check marked "Partly checked", the chat check uses the compatible-mode address, and errors show the status code plus the upstream text. When Model Studio explicitly refuses an image call as unsupported sync/async and no task was accepted, it resubmits once the other way and remembers that per channel and model; 5xx, timeouts, accepted jobs and channels with a chosen API never switch - **Oct 10** Turn outputs are ranked by importance: the turn's files are ranked first and then trimmed to the top 12 cards, with finished deliverables (web pages, PDFs, Word / PowerPoint / Excel) first, then video, audio and images; when there's a finished deliverable, process files such as drafts and debug scripts fold their cards away, and their names stay in the list below. When a task finishes, the final deliverable opens in the preview by itself, so you no longer click through one by one. Deleted or moved files are marked "Deleted" and sink to the bottom only after the server has checked they're gone, and a file written back under the same name is picked up again; opening a file first asks whether it's still there, instead of saying "file not found" after the click. When a thumbnail or image copy fails, the message says only what was seen ("Thumbnail didn't load", "Couldn't fetch the image (HTTP 404)"), without guessing whether it was renamed, moved or deleted - **Oct 10** The input box grows with your text: two lines at least, up to about a third of the window (6–12 lines), and only scrolls inside after that, without the bottom line being cut in half; when there's more text above or below, that edge fades out by a line so you can see which way to scroll. The two scroll buttons at the bottom right of the chat move up with the box instead of covering what you're typing - **Oct 10** Working folders, outputs and the library are kept per account when there's more than one: default-org admins (the server's original owner included) stay on the shared working folder, projects and library, so the workspaces they already had are all still there; everyone else (members, auditors, and everyone in branch orgs, their admins included) gets their own working folder (accounts// under the data root for the default org, one level below the org root for branch orgs) and their own library, one workspace each for now. File lists, the outputs page, search, downloads, and the AI's reads, writes and file search only see your own copy; admins open a member's files from that member's chat. Files and folders in Local output can now be deleted: only inside your own working folder, not while a task is running, and every delete goes into the security audit. Scheduled tasks run in their owner's folder; tasks from the command line or IM stay on the shared one. With a single account nothing changes; old files aren't moved, and outputs from before the upgrade still open from "By task" and old chats - **Oct 10** Local Claude Code can ask you before deleting files and the like: the Claude Code card's "Let the Security Center decide actions that need approval" box is now "Ask me before deleting files and similar" (Settings → Agent → Engine, owner only, off by default). With it ticked, rm, sudo and other actions that need a person's yes pop up an approval card instead of being refused — the same card the built-in engine uses, on the web, in the terminal and on your phone, with a diff for file edits — and run only once approved. List-allowed actions still run; unknown tools and errors while deciding are still refused. In the "edit files automatically" mode, rm and rmdir that claude would allow by itself become ask rules from your security settings (refused outright as before if this claude version doesn't take the flag). "Run the tools it borrows (image generation, library…) in this app" doesn't need to be on as well; if the card can't be shown you're told first, and such actions are skipped for that run. Allowed commands are run by Claude Code itself, outside the system sandbox; Q&A and plan runs and Codex aren't covered - **Oct 10** Settings descriptions rewritten in plain words: each item says in one sentence what it does, its default and whether it costs money. The Execution limits card now reads "Max steps per task", "Max minutes per task" and "Max seconds to wait for a tool"; the six switches that ask a small model are grouped under "Let a small model check first", with the cost and the missing-small-model note said once at the top; the two command lists on the Security tab say which one runs directly and which asks you first, and the option for commands on neither list is now "Check unlisted commands with a small model". Settings named when a task stops, in skill instructions and in error messages match the Settings page word for word, and "change it in Settings" is a link you can click; the English UI follows - **Oct 9** Switching the working folder no longer brings along another chat's files: folders you pick yourself also get one subfolder per chat for outputs, while folders named with -C or /cd on the command line and folders that look like code repositories are still edited in place. Both placements are offered in Settings and in the folder menu next to the input box, saved the moment you pick the folder and remembered per folder; after splitting, the AI is told your existing files are one level up. Attachments uploaded before sending remember which folder they went to and who uploaded them, so switching folders or chats before sending moves only your own copy - **Oct 9** Attachments sent with a new chat's first message no longer say "file not found" when you open the preview: they sit in the workspace root until the first message creates the chat's output folder and are then moved in, and the file chips and thumbnails on the message now follow them. Two chats that each upload a file with the same name no longer point at each other's copy, and switching to another chat mid-upload keeps the attachment with the chat it was uploaded in - **Oct 9** Answered options stay pinned: the options you picked in a session and anything you added mid-run go to the model at every step, so compacting a long task's history can't drop them, and the main agent sees answers given to expert sub-agents. Asking the exact same question again is blocked (this check used to sit behind a switch that was off by default, so it rarely ran). Assumptions written into the question itself don't count as your decision; if something really can't be done, the agent says where it's stuck before asking, instead of quietly switching approach - **Oct 9** Local Codex tells the owner when a newer version is out: "Rescan this machine" also checks for one (only for npm, bun and Homebrew installs; just opening Settings never goes online) and shows a line with an upgrade command you can copy in one click; after an in-place upgrade the model list is re-read, so models only the new version has show up right away. When Codex rejects a model during a task, the message says which version you have, whether the name only differs in spelling, which models work now and how to upgrade, with Codex's own words at the end, instead of blaming a subscription limit - **Oct 9** The local engine's model dropdown lists specific models, grouped into specific models, aliases, other models and the ones you've used; the owner also gets "Type another model…", members pick only from what's allowed. With more than one claude / codex installed, the newest copy that runs is used and the card lists the others with path and version; models that need a newer CLI stay out of the dropdown and are listed for the owner with an upgrade command. The engine test reports the model that actually ran, including what an alias resolved to - **Oct 9** Platform admins can switch the engine to local Claude Code / Codex and use it right away, with no box to tick first; on shared servers only other accounts are held back until an admin ticks "Let other accounts use it", and the card says whose subscription is used and where commands run. Settings, onboarding, thinking levels, chats and goal breakdown all go by the engine the account actually uses - **Oct 9** DeepSeek V4 on the official endpoint gets up to 64K per reply, and the thinking switch uses its new parameters (thinking.type turns it on or off, effort is only low / high / max); when a reply is cut off for length, the message says so plainly and replying "continue" carries on - **Oct 9** Map settings: AMap errors in plain words, the key hint says to pick the Web Service (Web服务) key type, and a new setting picks AMap or Google for opening routes in China - **Oct 9** Attachments no longer stop at 30MB: the input box and the canvas upload the original file and the server writes it to disk as it arrives, instead of turning it into base64 held in memory, so screen recordings of a few hundred MB go through; the old JSON upload used by older pages and scripts still works - **Oct 8** Tools lent to local engines can run in the main app instead: in Settings → Agent → Engine, tick "Run the tools it borrows (image generation, library…) in this app" on the Claude Code or Codex card (owner only, off by default). Each call to a lent tool (image generation, voiceover, the library and so on) then goes back to the main app and runs in that run's context: usage is billed to whoever started the run, approval cards show up on the web, in the terminal and on your phone, and org rules and the permission level are that run's. Each run gets its own credential, revoked when the run ends (readable only by you on macOS and Linux); tools not on the list and calls arriving after a stop are refused. If a call can't reach the main app it says that step didn't happen instead of quietly running on the engine's side; if the channel can't be opened you're told and the run works the old way. Codex's command sandbox can't connect to local sockets, so the owb command inside Codex still runs on its own and only its MCP calls come back - **Oct 8** Local Claude Code / Codex can pull material from your library: PDFs, images and Word files there aren't text, so engines couldn't reach them and had to ask you to drag them into the chat; now library_import is lent to them and copies the file into the run's working folder. Only your own library is reachable, only the mounted part when a project mounts one, and names or ../ can't reach anyone else's; Ask and Plan runs don't get it because it writes to the working folder - **Oct 7** Local Claude Code can hand actions that need approval to the Security Center: tick "Let the Security Center decide actions that need approval" on the Claude Code card in Settings → Agent → Engine (owner only, off by default). Commands, file edits and web fetches are judged by the Security Center's rules and run straight away if a list allows them; deletes, sudo and anything else that needs a person's yes, unknown tools, and anything that errors while being judged are refused. Settings are reread on every call, so list changes apply to the next one. Allowed commands are run by Claude Code itself, outside the system sandbox, just as without the box ticked; Ask and Plan runs and Codex don't get it - **Oct 7** Local engines making videos: after you press stop, a tool hits its time limit or the CLI exits first, jobs the provider already accepted still render, and the main app keeps collecting them in the background by job ID, never ordering again; the clips land in the original conversation folder and are billed as usual, and the run ends with a note saying how many and which job IDs. When you pressed stop, Bailian and Volcengine jobs are cancelled upstream first (queued ones cancel, ones already rendering still finish and bill). After a restart the server picks up whatever it hadn't collected; anything still missing after a day is no longer checked - **Oct 7** Stopping a local engine now also stops descendants that went off on their own: a browser a script launched, orphans left when a middle process exited first. Killing by process group couldn't reach them; now they're stopped one by one from a process-tree snapshot taken while the run was going, with SIGKILL 3 seconds later for anything ignoring SIGTERM. Reused pids and processes whose parent changed are left alone, and nothing is killed by name - **Oct 7** Local engines check the delivery at the end: if a file the reply names is missing or a 0-byte shell, or the reply says "as shown" but no new image arrived this run, the run page says so (a note only, the run isn't sent back). The bridge's network, command and block records are appended to the audit one line at a time, so a command-line call that exits right away doesn't lose them, and the Audit Center merges them in by time instead of the bridge overwriting the main app's copy - **Oct 7** Local engine run pages make sense now: lent tools show under their own names with the same icons and titles as the built-in engine, not mcp__openworkbuddy__x; images from Codex's built-in image generation land in the conversation folder mid-run with an image card; after a long silence it says every 30 seconds that it's still running and how long since the last output; notes like a tool not being attached or which settings a read-only run turned off are saved with the conversation and still there when you reopen it. Local engine failures are counted on their own instead of against your cloud model. All of this is in English in the English UI too - **Oct 7** Local engines also get connectors that come with plugins; ones turned off or paused on the Connectors page aren't attached; URL-based connectors are written differently by the two CLIs and aren't passed on yet, and the run page names them. Names with dots or double underscores are rewritten so both CLIs accept them, so Claude no longer asks for permission on every call; the connector config written for Claude is readable only by you - **Oct 7** Codex started by OWB no longer loads skills installed on the machine for other tools or its own system skills (only imagegen stays), so their descriptions stop crowding out the project's instructions and the lent tools; skills that ship with the project still load, and using Codex directly in your terminal is unaffected. Codex errors are passed on verbatim: it only says you're signed out when Codex says so, a bare 401 is reported as rejected upstream, and a "no longer supported" setting comes with Codex's own advice on what to change - **Oct 7** Local engine usage is counted correctly: a resumed Codex run no longer adds the whole thread's running total again (the ledger used to be several times too high), and records 0 rather than a guess when the starting point is missing; Claude's usage is still recorded when it's stopped or times out, and a call reported in several pieces counts once. Runs on your local subscription are recorded without spending credits, and the UI says so - **Oct 7** Ask and Plan modes on local Claude Code / Codex always run read-only (claude plan, codex read-only); the owner's looser settings and extra arguments don't apply to those runs, and the run page says which were skipped; only the read tools are lent. Tools that can't work inside Codex's command sandbox (paid ones that need the ledger, ones that write the data folder, ones that open Chrome) stop before starting, so no money is wasted, and the prompt names the MCP tool to use instead; at the "Read only" level neither engine gets the owb command - **Oct 7** Local engine prompts tell the truth: every lent tool is listed, every skill is listed with the path to its full text; Claude reads images with Read and Codex with view_image; node scripts can require the bundled pptxgenjs / docx / exceljs directly. owb takes --help anywhere on the line and prints the full usage and every argument, and a mistyped tool name gets the closest match - **Oct 7** Local engines borrow OWB's tools more reliably: one lending list drives the prompt, MCP and owb list, and lending nothing really lends nothing; output lands in the run's own workspace and the library shows only your own, or only the mounted part of it; PATH puts the owb folder first and the completed search path after it, so CLIs installed with nvm or Homebrew find node again; image, video and voiceover receipts give paths relative to the engine's current folder instead of claiming "shown above"; when the settings file can't be read, lent tools refuse to run instead of running on defaults as if it were empty - **Oct 7** Trip card "Open route": China days with four or more stops now open AMap's web route planner with names and coordinates for every stop, up to 15 a day, and the QR code opens the same page on a phone (three stops or fewer still use the AMap navigation link, and three stops always drive, or the middle stop gets dropped). Google Maps gets "name, city" for each stop instead of bare coordinates — with coordinates alone it swaps in a nearby shop's or statue's name, and with the name alone it can land in another city. China routes get an "AMap / Google" switch next to "Open route" that changes the day route and the between-stop links together and is remembered on this machine; Google takes at most 10 stops at once, and a longer day says how many stops it has. QR codes for long links are drawn bigger so they scan - **Oct 7** Trip cards: clicking a pin on the map opens a small card for that stop beside it (photo, rating, category, time, "Navigate here"); "Previous" and "Next" page through the day, Esc closes it and puts focus back on the pin; in a narrow window the card docks to the bottom of the map. "Navigate ▾" between two stops opens walking, transit, cycling and driving, each as an Amap link; "Send to phone" shows a QR code right there (made locally) to pick up navigation on your phone, and the one between stops can switch modes. Amap quota is counted per month; a bad Key is paused for the day and only hit once per batch; a leg whose route lookup failed shows the error as returned and can be retried, on the timeline too; each stop shows where its place info came from; when the trip JSON can't be parsed, it says which line is wrong - **Oct 7** Safer canvas spending: the Agent in canvas chat no longer generates directly; it hands over a to-generate list pinned to the top of the canvas, priced first, and nothing is charged until you click "Run"; if only part of it runs, the done items are crossed off and the rest stay with a fresh quote. Every order carries an ID and the server runs each ID once, so a dropped connection no longer charges twice; cells that never got a result are marked "Status unknown" with "See result", "Generate again (charges again)" and "Ignore". A failed generation is resent on a backup model only when the upstream clearly didn't accept it or it was never sent; timeouts and broken downloads that may already be paid for report the error as is. The shot inspector can "Generate voiceover" and "Try another voiceover", also quoted before charging. Local engines running canvas tasks can edit nodes and hand over lists too - **Oct 7** Canvases you can get back: deleting a canvas moves it to the project's trash, restorable from the canvas dropdown with its backups and snapshots; before the Agent clears a canvas or deletes a run of nodes it takes a snapshot, with "Undo" at the top to restore it. Undo only undoes your own changes, and the "Undo" on a notice only undoes the step it names; links can be cut on their own, and deleting a node no longer asks for confirmation. When a canvas fails to save to the project, a red bar at the top quotes the error and offers a retry. Drag shot cells on the timeline (press and hold on touch) or press Alt+←/→ to reorder shots, and one-click compose follows that order; shots with no voiceover but sound in the video keep that sound; if compose progress is lost after a server restart, it says so and offers to recompose. Uploading a file with a taken name saves it as name_2 and leaves the original alone, and re-dropping the same attachment replaces it in place; the asset ledger no longer marks in-use assets as unused - **Oct 7** Lighter when idle: with no task running and nobody using the app, the server's stall timer stops after a quiet minute, cutting its wakeups from about 40 to about 2 per second; it resumes the moment a task or request comes in, and the UI's background polling doesn't count as use. While the window isn't focused, the main process no longer keeps watching for stalls - **Oct 7** Trip cards redesigned: the map fills the whole card, the day's stops float in a panel on the right, day pills float at the top left of the map, and zoom plus "Back to overview" moved to the bottom left. Pins are white-bordered photo squares with a number and the place name; names that would collide are left out, and framing keeps clear of the panel — fold the panel and the map re-frames to the full width. Click a place card and the map glides to it; click a pin and the panel scrolls to its card. Cards have bigger photos with rating and category, and a specific time gets its own line; the row between two stops shows how to get there, how far and how long, and doubles as the divider. "Open route" says which map it opens: AMap for up to three stops in China, Google Maps for more or for days mixing China and abroad (up to ten stops), walking when every leg of the day is under two kilometres - **Oct 6** System sandbox: commands the AI runs (run_shell, background commands) and run_node scripts run under the built-in sandbox-exec on macOS and at low integrity level on Windows. However a command is written, it can't read your Keys or ledger or modify the app itself; on macOS it also can't reach OpenWorkBuddy's own ports, and on Windows it can only write to the workspace and temp folders. Switch it in Settings → Security → System sandbox, with four levels: default, auto (runs anyway if it can't start), required (doesn't run if it can't start) and off; the settings page shows whether it's active. Cost per command: about 7 ms on macOS and 13–16 ms on Windows (measured on real Windows). When something is blocked, the output says where the switch is. The Windows layer doesn't block local ports and doesn't separate several orgs' workspaces on one machine; that's stated next to the switch and in [安全](docs/安全.md#系统沙箱macoswindows). Linux doesn't have this layer yet - **Oct 6** Only the models you configured: the relay no longer forwards unregistered models unless the owner explicitly allows any model; the judge model accepts only configured models; channel tests only test registered models, and saved Keys aren't sent to an edited address; memory embeddings use only the embedding model chosen in settings, falling back to keyword search. With a quota set, unpriced models are refused; the price wizard includes default models, and free local models are recognized by channel address - **Oct 6** Quota gate gaps closed: before switching to a fallback channel the price gate runs too — if the fallback has no price it isn't used, and you're told where it's stuck and to fill in the price list; tasks that switched channels are billed per channel. Vision goes through the quota gate, and image and video tools resolve the actual model before estimating cost. Pricier tiers matched only by a prefix family (such as -pro, -realtime, -thinking) are refused as unpriced when a limit is set, while dated snapshots and cheaper small tiers pass. When memory embeddings are blocked by the quota, the memory page says why and falls back to keyword recall - **Oct 6** Keys stay out of the AI's reach: child processes started by the AI get a minimal environment with no Key variables by default, and the owner can set a pass-through list in Settings → Security. Network tools can't reach localhost, the LAN or cloud metadata by default: addresses are resolved and checked one by one, the connection is pinned to the checked address, and every redirect is re-checked; OpenWorkBuddy's own ports are always blocked, and the owner can allow host:port pairs. In multi-user setups, commands and code that touch the file blocklist are refused outright instead of showing an approval card - **Oct 6** External engines (local Codex, Claude) must name a model: it has to be pinned or on the owner's allow list, and extra arguments can't switch it; they don't start when the org has the command line turned off; in multi-user setups they're off by default and the owner enables them one by one, with no Key variables passed in. Codex runs offline and writes only to the workspace by default. Errors point to settings that actually exist, and old configs get a notice about these changes on upgrade - **Oct 6** Trip cards: itineraries in answers become a map you can drag and click, with per-day tabs, place cards, a timeline and photos. The list on the right collapses and resizes, the map height can be dragged, and "Back to overview" at the top right fits all of the day's places again (or press 0 with the map focused). Works without a Key; with an AMap Key in Settings → Map, places in China are more accurate and come with ratings. When the map service has no photo, one is looked up on Wikidata by name and location. Navigate opens Google Maps abroad and AMap in China, walking for stops within 2 km and driving beyond; abroad, a whole day's route opens with its stops as waypoints - **Oct 6** The README's "What it looks like" now has two trip-card screenshots from a three-day Beijing plan: the map view with each day's stops pinned, photo place cards and navigation links on the right, and the timeline running from morning to evening - **Oct 6** Local Codex receives the working folder and output instructions; images from its built-in image generation land in the conversation folder and preview in the output panel - **Oct 6** The commercial license now spells out the three cases where a license code is revoked (refund, leak, impersonation) and how to appeal: you're notified first, leaks and impersonation don't penalize the licensee, and revocation only shows a notice and locks nothing. It also states the app has no analytics or telemetry and only goes online to do its work and check for updates - **Oct 6** Source code moved into layered folders under src/: platform, core, domains, agent and engines, server / cli / im / desktop, and util, each depending only downward, with the layer guard enforced in tests; the packaging allowlist only accepts src. Read [代码架构](docs/代码架构.md) before extending it - **Oct 5** New project: connectors, experts and skills can be selected all at once, cleared, filtered and Shift-selected, or copied from an existing project - **Oct 5** Evals: judges see the process, prompts are editable, plus human review and a multi-dimension scorecard - **Oct 3** The README now has an AI short-drama episode made from one sentence, "Bu Shao Xin" EP01: script, character looks, storyboard, first frames, shot videos and the final cut all done in one go — 75 seconds, click the poster - **Oct 2** Using a prompt template now opens a fill-in form first: `__name=default__` gives a default, `__name=A/B/C__` gives choices, and text after a comma or bracket becomes a hint; a live preview sits on the right and your last answers are remembered. You can drop it into the input half-filled and press Tab / Shift+Tab to jump between the remaining blanks. When writing a template, select text and click "Make a blank" (Mod+E); click again to undo it, and the list underneath lets you try filling them. Deleting a template no longer asks first — you get an undo instead. All 14 built-in templates now have named blanks - **Oct 2** The Windows installer is now a wizard: pick the drive and folder, and watch a progress bar; installing into the data folder is blocked. The package went from over ten thousand files to about four thousand, so it installs faster; taskbar icon and notifications share one app name. The portable build shows an image while unpacking, and double-clicking again no longer deletes the copy that's running - **Oct 2** Windows tasks no longer flash a black console window: child processes stay hidden, .cmd shims and GBK output are read correctly, and exit cleanup and clipboard no longer block the UI. Chinese, space-containing and long paths are handled in one place; python3 uses an ASCII shim, and the WindowsApps placeholder no longer wins. Tray icon, output-panel buttons, canvas and timeline are sharp and aligned on high-DPI screens. The delete guard now catches over twenty more cmd spellings (`@`, `,`, `;`, `=` prefixes, redirections, `cmd.exe/c`, `%ComSpec%`, `^` continuations, `forfiles /c"…"` and more), with PowerShell, start and net judged the same way - **Oct 2** PPT preview draws each slide from its real layout: positions, backgrounds, font sizes, images, tables and charts, and zip64 files open too. The infinite canvas keeps images at their real aspect ratio instead of stretching them; the timeline preview is now a film strip. The output panel's close button no longer gets pushed off narrow windows - **Oct 2** Model channels got a round of fixes: GLM on Volcano Ark is no longer rejected as "a Zhipu model"; a model on the wrong channel is no longer guessed or moved silently — you're told when it's re-attached. In media channels, Tongyi international and dedicated endpoints, the vision protocol, picking by model name, and async Wanxiang image jobs all work; reference images, first frames and videos placed from chat onto the canvas no longer all show "not found" - **Oct 1** Saying "install this skill" in chat installs it into the skill library, the same way as "Install from GitHub" on the Skills page — security scan, source recorded, and you're asked before it overwrites a skill or when the scan warns. Built-in skills update with the app while keeping any you've edited. Asking to connect an MCP server takes you straight to the Connectors page - **Oct 1** Replies to Feishu, WeChat and other chat channels that exceed one message's limit are now split at paragraph breaks into several messages, with nothing lost — previously the overflow was cut off. SVG charts in a reply used to be sent as raw source; they're now converted to PNG and sent as images, and channels that can't send images say plainly that the chart wasn't sent - **Oct 1** Conversations from before per-conversation folders: files already in the workspace root are edited in place instead of getting a same-name copy in a new folder, and they show up under "This chat" in the outputs panel. After switching workspace folders, image and document attachments in older conversations still show thumbnails and open - **Oct 1** The CLI (owb) also gives each conversation its own folder instead of piling everything into the workspace root. Clicking "New task" while another task is running no longer drops the new task into the previous task's temporary folder. The Copy buttons on messages and code blocks work again in the desktop app, and say so honestly when copying fails - **Sep 30** Each conversation gets its own output folder beyond the default workspace too: projects created without a folder (the app makes one under ~/OpenWorkBuddy/projects) and orgs without a set folder now split outputs per conversation, so two conversations writing a file with the same name no longer overwrite each other. Feishu, WeChat and other chat channels split per conversation (`IM_对话/MMDD_first-line`), schedules split per task name (`定时任务/task-name`), and folders left empty after a run are removed. Folders you picked yourself are still read and written in place. Switching folders mid-conversation no longer leaves the outputs panel at "This chat 0", and opening a schedule run's replay now shows that run's outputs - **Sep 30** Templates can now be added, edited and deleted. Anyone can add to "Mine", which only they can see; only admins can add, edit or delete "Company" templates, which the whole org sees, and every change goes to the org audit log. Editing a built-in template saves a copy and hides the original; anything hidden can be put back from "Hidden". The personal desktop app only has "Mine" - **Sep 30** Many open conversations no longer slow things down: they share one live channel and each uses its own browser tab; the server moved off the UI thread, and saving, thumbnails and screenshots no longer block the main thread; each sidebar row can be stopped on its own - **Sep 30** First-run setup accepts any OpenAI-compatible endpoint (relays, self-hosted gateways, local vLLM), and retries with /v1 when a bare domain returns 404; Enter no longer sends half-typed pinyin while an input method is composing; long conversations scroll up continuously - **Sep 30** One server can now host several companies: the platform owner creates an org under Organizations and sends its first invite from that row; whoever signs up with it becomes that org's owner. An expired org can't start new tasks (web, IM, schedules and relay keys all stop) but can still sign in, read history and download results; to suspend one right away, set its expiry to a past date - **Sep 30** Multi-user permission fixes: ordinary members no longer see connector URLs and launch arguments; tenant admins can't turn the shell on, change the billing discount, raise the org cap or change paid-API quotas themselves; re-enabling a member checks seats; the 200-invite cap is per org; conversations left by deleted members stay with their org and the username can't be reused; offboarding handover must go to an active member of the same org; member rows get a Reset 2FA button, so a lost phone no longer means running a command on the server; usage from the CLI and IM (no signed-in user) can no longer land on a tenant org - **Sep 30** Scheduled tasks run as their owner: billed to them, in their org's folder and under its rules; if the owner is disabled or deleted, or the org expires, the task stops on the spot with the reason. Every sign-in success and failure goes to the runtime log; lockouts and attempts on disabled accounts go to the org audit - **Sep 30** New guide [账号与部署运维](docs/账号与部署运维.md) (Chinese): picking a deployment, the first account, adding people, offboarding, recovering passwords and 2FA (including inside Docker), where sign-in records live, backup/restore/upgrade, hosting several orgs, and a pre-launch checklist. Outdated bits in the deployment and multi-user docs are fixed - **Sep 30** License files go from three to two: the MIT list and text from `LICENSE-ECOSYSTEM.md` moved into [COMMERCIAL-LICENSE.md](COMMERCIAL-LICENSE.md#按-mit-授权的部分), terms unchanged - **Sep 28** The license terms are now spelled out: free for personal non-commercial use; company use (even internal only) and individual commercial use (client work, paid products) need a commercial license from the author, and companies can try it free for 30 days first. The sign-in page, the first onboarding step, the About page and the startup log all say so. The admin console has a new Commercial license page: paste a license code and it's verified locally, with no network call and no features locked. Details in [COMMERCIAL-LICENSE.md](COMMERCIAL-LICENSE.md) - **Sep 28** Rewrote the Word document skill: it builds files with the docx library, with a ready template for CJK fonts, a table of contents, headers with page numbers and tables, plus how to edit an existing .docx and the usual traps - **Sep 28** Memory embeddings on a Qwen (DashScope) channel are no longer marked "unavailable": 16 texts went out per request against a limit of 10, the whole batch was rejected, and it fell back to local Ollama. Requests are now split to each provider's limit - **Sep 28** The Excel and PPT skill templates run as written: the final `await` was a syntax error under run_node, and the Excel template used a variable it never defined - **Sep 28** Fixed replies that stopped mid-sentence while the task still said "done": when the upstream halts partway (DeepSeek does this under load) or the stream ends without an end marker, the reply now counts as unfinished and the model is asked to pick up where it left off, and you get the joined-up answer. After 3 cuts in a row it stops and tells you the upstream didn't finish. Replies cut by the upstream's content filter now say so on screen - **Sep 28** Channels now have an API format setting: OpenAI Chat Completions, OpenAI Responses, Anthropic, Google Gemini or Ollama native. Pick whichever protocol your local deployment or gateway speaks; "Test" hits the same address as real runs, and tool calls work in every format. The Ollama native API gets a 32k context by default, so it no longer silently truncates the system prompt at a few thousand tokens; when Gemini's safety filter blocks a request, the error says so - **Sep 28** When a chat is still running elsewhere (another tab, your phone, or a run the page lost track of after a dropped connection), sending "continue" no longer fails with "this session already has a task running": the page attaches to that live run and delivers your message as an interjection or a queued message, per your setting. If the run was already going when you opened the chat, the running turn shows once instead of also leaving an "interrupted" copy - **Sep 27** Changing the default model under Settings → Models now carries over to new chats. With "new chats reuse the last model you picked" on, the model once picked in the chat box used to win every time, and picking "Follow the default" there was pulled back too. If the open chat has its own model, you're told it stays; if the assistant page or IM messages (Feishu etc.) use a different model, the Chat card says so with a one-click switch back to the default - **Sep 27** Auto backup: pick Daily or Weekly under Settings → Data backup and a backup is made on schedule, keeping the latest 7 automatic ones; manual and imported backups are never deleted. Off by default, since backups contain your API keys. It never runs during a restore, restores wait while it packs, and failures show up in Settings and the audit log - **Sep 27** Video creation: segment-by-segment voice-over (TTS), motion rendering (`render_motion`) and timeline compositing (`compose_video`). Four ready-made recipes — product promo, product demo, Xiaohongshu carousel, multi-platform posting — open with a form for length, aspect ratio, sound and cover before any work starts, and a delivery page lists everything at the end. **Paid steps (AI images, paid voices) default to "skip this time"**, marked "costs money, not chosen for you", and run only if you tick them; a voice-over stopped after it was billed is counted as spent. Files with the same name are never overwritten - **Sep 27** Web demo recording (`record_web_demo`): it opens the page, clicks through the steps and records a video, masking local details such as your user name and paths before anything is made public; if the page navigates away mid-recording, nothing is delivered rather than half a video. A brand kit keeps one profile per product — name, selling points with sources, colours, fonts, voice and tone — and images and videos use it directly - **Sep 27** The library gains a Workspace tab you can click through folder by folder, so files at any depth can be found and opened; the Folders view says it shows only the latest batch, with an "All N →" link; Unassigned and search now cover the whole workspace instead of the newest 500 files within three levels - **Sep 27** "This turn's output" lists files four or more levels deep (before, files the AI wrote deep down never showed up), and Feishu / WeCom replies attach them too; deleted files are left out - **Sep 27** Feishu: mp4s are sent as video messages, compressed first when over the limit, falling back to a file if upload fails, and portrait phone videos are no longer squashed. The task card shows what the run cost ("spent ¥0.84"); anything without an official price reads "price unknown" instead of counting as 0, and engines like Claude Code or Codex whose spend cannot be fully recorded show no amount rather than an understated one - **Sep 27** When WeCom, QQ, WeChat Official Accounts or iLink fail to send a file, the error no longer includes the full local path - **Sep 27** Tests no longer touch real data: a guard fails any test that tries to write the real data directory, and suites that did so when run alone now use their own temp dirs. A full run's new temp files drop from about 1.5G to 6.3M, and e2e leaves no temp dirs behind once green - **Sep 27** The recipe fallbacks "none chosen / skip this time / not filled" are translated in the English UI - **Sep 25** The project site has comparison pages: Claude Cowork, Codex, Doubao, Qwen and DeepSeek, each in English and Chinese, each saying plainly when the other product is the better fit; the README gains a "Coming from another tool" section, and the site now has a sitemap.xml and robots.txt - **Sep 25** Multi-line input in the CLI REPL: a pasted block lands in the input box and is sent only when you press Enter (before, the first newline inside it sent half the text); type your own line breaks with `\` then Enter, `Ctrl+J` or `Option+Enter`; `Ctrl+G` opens `$EDITOR` for a long message, and if the editor exits without saving your text is still there; `Ctrl+R` searches what you asked before, and a multi-line message comes back as multiple lines; with lines held, `Ctrl+C` discards them without quitting - **Sep 25** While the CLI is working, the "thinking…" line and the running tool line show elapsed time, tokens so far and how to stop (`· 12s · 8.4k tokens · Esc 停`), so you can tell busy from stuck; `Esc` stops this run without quitting, and the line settles to `· 12s`. Esc is left alone while you are typing or answering its question - **Sep 25** When Plan mode in the CLI finishes a plan, you get two choices: "Go with this plan" (switches to Craft and carries it out, then goes through the plan step by step saying what was done and what was not, and why) or "Keep editing the plan"; Esc skips the choice so you can just type. If you had already started typing while the plan was running, no menu pops up - **Sep 25** With nobody at the terminal (a pipe, `--json`, a scheduled run), a step that needs approval is denied immediately with a note on how to allow it, instead of waiting forever. New `--allow` flag approves a kind of command, file writes or a risky operation ahead of time. The `--allow "…"` shown in that note can be pasted back as is; a mistyped rule exits with code 2 rather than running on without the permission you meant to give. The model is told the truth too: it used to hear that an approval request had been shown, so it would tell you that you had said no when nobody had been asked - **Sep 25** In the CLI REPL, a line starting with `!` runs a shell command yourself (`!git status`); its output is attached to your next message so the agent sees it, and `/drop` discards it. Ctrl+C stops only that command. To send a message that really starts with `!`, type `!!` - **Sep 25** Project rules (AGENTS.md, or CLAUDE.md when there is none) are read from the working directory all the way up to the git root; rules in a closer folder take precedence, and they share a 6000-character budget. CLI tasks now include them too, and `/status` lists which files were read and how many characters each contributed - **Sep 25** A file name mentioned in an answer is clickable whenever that file exists in the workspace. Before, only files created in that very turn were linked, so images made a turn earlier sat as dead text in the summary table. When you open an older task and the file list arrives a moment later, the links are filled in then. A bare name that matches files in two different subfolders stays unlinked rather than guessing - **Sep 24** CLI approvals are now picked with ↑↓ and Enter (number keys and y/n still pick directly, Esc means deny); an Enter within 0.4 s of the menu appearing is ignored, so a stray keypress typed earlier cannot approve a delete. No line wraps on a narrow terminal, so redraws erase cleanly - **Sep 24** Tool calls in the CLI now look like Claude Code / Codex: `● Shell(npm test)` shows which command ran or which file was edited, with `└` showing the first 4 lines of output and how many more; a nonzero exit code comes first; when calls run in parallel or an approval prompt comes in between, the call line is printed again before its result so it never hangs under the wrong one - **Sep 24** Canvas video: if a job fails after the provider already accepted it (result timed out, download dropped), it is no longer retried automatically, so you are not charged twice; the message gives the job ID - **Sep 24** Canvas video now sends the length, aspect ratio and resolution shown on the card; a shot with no length uses the seconds per shot set for the drama; placeholder text on new shot cards no longer leaks into prompts - **Sep 24** Leaving the context cap empty in Settings now sizes it from the model window, instead of every save locking it at 120k - **Sep 24** Chatting on the canvas now shows up in task history right away: marked running, moved to the top, replies streaming in; history shows only what you typed, not the long instruction block; a dropped stream keeps following until the task really ends - **Sep 24** The input box hints `@` to reference files and `/` for skills and commands - **Sep 24** The Jev decision model is now off by default: an OpenRouter chat channel no longer turns it on; add a "TypeSafe Jev" channel to use it - **Sep 23** CLI `/cost` (same as `/status`): tokens, calls and an approximate cost for this session; if any round's model has no price, only tokens are shown; local engines count as free - **Sep 23** Commands that wait for input (`read`, `input()`, `npm init`) no longer sit until the timeout: `run_shell` / `run_node` get no stdin and hit end-of-input at once — 120 s before, 0.07 s now. `read_file` accepts `offset`/`limit` (they used to be dropped silently and the file read from the top) and says so on binary files instead of returning garbage; `search_files` accepts `pattern`/`path` and searches just that file when given one; an empty command is an error, not a fake exit 0 - **Sep 23** Editing a Windows (CRLF) file no longer leaves half `\n`, half `\r\n`: `edit_file` / `multi_edit` / `write_file` (append too) keep the file's line endings; when the file uses tabs and the edit uses spaces (or the reverse), the edit is written back in the file's style instead of leaving mixed indentation Python rejects. Files already mixed are left as they are - **Sep 23** `openworkbuddy workflow flow.json`: write "plan → build → review" as a file and run it step by step; `{{name}}` pastes an earlier step's answer; a failed step stops the rest, and a broken file runs nothing - **Sep 23** Hooks: commands in `agent.hooks` of `config.json` — `before_shell` can block a command, `after_edit` runs after each file edit (format/lint), `done` must pass before the task wraps up (sent back at most twice) - **Sep 23** CLI: `/review` (and `openworkbuddy review [base]`) hands your changes over for a read-only review; write your own slash commands in `.openworkbuddy/commands/*.md` with `$ARGUMENTS` / `$1`; new one-off flags `--model` `--max-steps` `--append-system` - **Sep 23** Better at code: `find_files` finds files by name, `multi_edit` makes several edits to one file (all or nothing), edits are blocked if the file changed since it was read, `run_shell` can run in the background (`shell_output` / `shell_kill`), and a `todo_write` checklist — finishing with unticked items is sent back - **Sep 23** **Skill pick before starting: naming a skill loads it for free, an unnamed job asks the decisions model one multiple-choice question (off by default, Settings → Agent).** You install a skill so that kind of job gets done its way, but in the prompt a skill is just a list of names; if the model doesn't remember at step one the whole piece is done its own way. `/wechat-article` or "write it with wechat-article" now loads mechanically, switch or no switch (whole-word match, so `html-page` isn't fooled by `html-pages-v2`); an unnamed job with the gate on asks "which skill should this follow", options being the skill names plus "none fits", and only a pick at 70%+ confidence gets loaded. Asked only at the top level and only while nothing is loaded; it can only load one more, never one fewer; "none fits", unsure or unreachable leave things as they were and log one warning. 67 checks; swapping the old agent.js back turns 18 red - **Sep 23** **A loaded skill was forgotten the moment history got compacted, and the next turn never knew it had existed.** The full text from `use_skill` was just a tool result lying in the history, the first thing compaction and trimming sacrifice, so the third article in the same conversation ignored the skill again. Loaded skills now sit in a "loaded skills" section of the system prompt (up to three in full, older ones by name), visible at every step; the tool result is a one-line receipt; the compaction summary carries a loaded-skills line, and the next turn rehydrates from the use_skill calls in history and that line. The prompt's "use_skill before you start" went from a suggestion to a rule - **Sep 23** **Compaction used the global default model, not the one this conversation picked — with a decisions model set as the default it failed with an opaque 400.** The summary now follows the conversation's model; a decisions model (Jev) used as a chat model gets a plain error: it only picks options, never writes, change it under Settings → Models, decisions models are configured separately under Settings → Agent - **Sep 23** **When memory ran over budget, a rule like "don't open files for me on delivery" got squeezed out — exactly the kind every job has to keep.** Over budget, entries are picked by relevance to the task, which is right for facts (an article doesn't need "expenses go through Feishu") and wrong for rules: that one shares no word with "write a WeChat article", scores zero, and every newer bit of trivia outranks it. Rules now go in first (by creation time, stable), the remaining budget picks facts by relevance; too many rules to fit is said out loud ("N more rules didn't fit") rather than dropped silently; hits are recorded only for entries that actually got in. 21 checks; reverting rules-first turns 5 red - **Sep 23** **The agent kept running `open` to pop the file up for you once it was done — unless asked, a delivery just reports the path.** `open` / `xdg-open` / `start` / `explorer` are now on the ask list: even full-auto raises one confirmation, and only an explicit "open it" goes through; the prompt also says the chat shows a preview card, so there is no need to open anything for the user. cmd-risk suite at 141 checks, 11 new (incl. `&&` chains; `echo open the door` and `node open.js` are not caught) - **Sep 23** **The preview card in the chat and the right-hand preview panel were opening different images.** The agent rewrote the same path three times, the card grabbed the moment it finished, the panel served the browser's cached previous version, and it looked like the agent was lying. Every card and every preview now carries that version's timestamp (`?v=`), the server only long-caches requests that name a version and never caches bare ones; card and panel each point at their own version, and whichever card you click is the one that opens. Front-end and e2e suites each gained assertions - **Sep 23** **The second v0.9.0 tag went red again, this time on "clone takes 50.1M, under a quarter of a plain copy" — the clone measured *larger* than the plain copy, which is physically impossible.** That assertion measures the before/after delta of `df` system-wide free space; on the CI runner, e2e re-runs the prefs suite as a child process right after the Electron tests, other processes were still writing, and someone else's 2M leaked into the 48M delta. The same commit was green on the push-triggered workflow and green locally — **measurement noise, not clone falling back to a plain copy**. Now it measures up to three times, stops the first time clone comes in under a quarter of plain, fails only if all three look wrong, and `sync`s plus waits 300ms between tries. The failure message carries every measurement so noise and a real fallback are told apart at a glance. Mutation control: forcing `canClone` to false in the mirror gives ≈48M on all three tries and the gate stays red — the retry rescues noise, not a real fallback. Locally 393 pass / 0 fail. - **Sep 23** **Judge before interrupting: from the second question on, the agent spends one judgement call asking whether it really needs to ask you.** `ask_user` is the only tool exempt from the loop guards — it can ask four times in a row without being stopped, and the right reason to stop it was never "how many times" but "is this worth interrupting the user". With the new gate on (Settings → "Judge before interrupting", off by default), the first question always goes through; from the second on, a question already asked verbatim this run gets the previous answer instead of a second popup, and a new one is classified by the judge model first — "key information / fork in the road / your call" pop up as before; only "progress report / technical route / could look it up / asking again" are held back, and only at ≥ 80% confidence. A held question returns a receipt to the agent: proceed with the most reasonable default, note it in the final report, and if the judgement was wrong ask again — the gate never blocks a real fork. No classification, no judge response, or the two answers disagreeing all fall through to a popup — **the gate can only ask less, never pick an answer for you** (forks always pop up). 85 assertions including five real agent runs as controls: gate on with a "progress report" holds 1 and shows 2; gate off shows all 3; "must ask" shows all 3; judge throwing shows all 3 and leaves a warning; classified as a fork shows all 3. - **Sep 23** **Green locally, red in CI — the suite quietly depended on the developer machine's system language.** The v0.9.0 tag went up, the test job failed, and packaging and release were skipped entirely, so the Release page had no build at all. The difference was the machine, not the code: the UI language follows the system (`navigator.language`), the dev machine is Chinese and the CI runner is English, and two assertions compared Chinese copy verbatim — “clicking + opens the new-project editor” compared the dialog title, which on an English UI reads `New project`. **What went red was how the assertion was written, not the product**: the editor was open, all four fields present. Both now measure structure — that one counts whether the four fields (name / working directory / project instructions / which library to attach) are there, which is exactly what its own wording promises, and the `/` menu's header check now looks for `↑↓` and `Esc` in the hint, key names no language translates. The real-page harness also pins the language and reloads the whole page before anything else, then verifies the pin took and throws if it did not, rather than letting dozens of assertions rest on an unstated premise — the other four real-page suites had done this for ages; this one had missed it. Measured three ways: pinned to Chinese, 68 sections / 2296 assertions, all green; pinned to English, the same 68 / 2296, all green — the same ruler on either machine; and with the pre-fix assertions against an English UI, section 66 goes red with the exact failure text CI printed, which is what makes it a reproduction. - **Sep 23** **The previous entry fixed the wrong numbers; this one stops them depending on someone remembering.** Those badge numbers are only recomputed when a human runs `npm run stats`, and remembering is not a mechanism — that is exactly how the last release shipped with them stale. The machine now counts on every test run and compares field by field against `docs/stats.json`, version number included, so bumping a release and forgetting to recompute goes red on the spot. **The `updated` field is deliberately excluded**: it changes daily, so comparing it would demand a rerun every day and greet everyone with a red suite each morning — a gate that cries wolf daily is one people learn to ignore within a fortnight. 9 new assertions, 4 of them reverse controls: one skill off must be caught, a stale version must be caught, a differing date alone must not go red, and an unreadable file must count as every field mismatching rather than passing quietly. Measured by actually injecting them: drop the stored skill count by one and the gate reds on that line; roll the version back to 0.8.1 and the failure text spells out “the file says 0.8.1, the count says 0.9.0” along with the command to run next. The release checklist gained the step too. - **Sep 23** **The badges at the top of the README were wrong — they said 35 skills when there are 36.** They read `docs/stats.json`, which is only recomputed when someone runs `npm run stats` by hand, and nobody had since the last release. The same script's date was off too: `updated` came from `toISOString()`, which is UTC — run it after 8pm in UTC+8 and the badge says yesterday, while the changelog date in the very same commit is local, **so one day's work carries two different dates in two places**. It now uses the local timezone. Measured on the spot: local clock 09-23 00:42, the old expression yields 09-22 and the new one 09-23; two controls — under `TZ=UTC` both expressions must agree exactly, and under `TZ=America/Los_Angeles` the local date must be the earlier one. Without those, “fixed” could just mean a day was hardcoded onto the offset, still wrong in another timezone. - **Sep 22** **The arrow-key navigation just added to the `/` menu had nowhere telling anyone it existed.** The header now also says that Enter puts the skill in the box rather than sending the message — that rule just changed, and anyone who learned the old behaviour will not find out by guessing. The line disappears when nothing is selectable (you typed something that matches no skill), so it never points down a road that goes nowhere. **The same pass fixed a defect the previous version introduced: the list is fetched asynchronously, a fetch repaints the menu, and the repaint pinned the selection back to the first row** — press ↓ twice, and the moment the list lands you are back at the top. The repaint now keeps the selection where it was, and only while the menu is actually open: where you had got to in a menu you already closed should not carry into the next `/`, because that is stale state the user cannot see. When the list comes back identical, nothing repaints at all. 3 new assertions (hint shown / hidden, selection survives a repaint, no repaint when unchanged), 6 mutations injected into the real code plus 2 equivalent rewrites as controls, none of which misfired. The “selection leaks across close-and-reopen” case was found because the existing reverse control went red first. - **Sep 22** **Type `/`, pick a skill, press Enter — and the message flew off before you had written what you wanted done.** The listener that handles `/` runs in the capture phase and stopped at `preventDefault()`; but the listener that sends on Enter is on the same textarea, both are at the target level, and they run in registration order — the first one only suppressed the browser’s default (a newline), not the second JavaScript listener. **What needed stopping was propagation, not the default action.** Arrow keys went in at the same spot: “selected” used to be nothing but a `.sel` pinned on the first row at render time, so Enter could only ever take the first item and picking the second meant reaching for the mouse — “choose a skill” was not actually possible from the keyboard. ↑↓ now move, wrap at the ends and scroll along. **Second half: a newly installed skill only showed up in `/` after a page reload.** That list was fetched once at page open and never touched again. Patching only the “installed from this machine” path is not enough — skills arrive by several routes: the skill hub, a plugin bundle, a file dropped into the directory from the command line, someone else installing on the same server. So it now refreshes when it is about to be used: typing `/` re-checks the list (throttled to 3s, or a single word would hit the endpoint a dozen times), whichever route it came in by; the + menu’s skill list shares the same copy, and the plugin install / update / uninstall paths, which do not refetch skills, ask for it explicitly. The 7 new assertions run against the real page, with the throwaway server also answering `/api/skills` — pushing an entry into that array *is* “a skill got installed on the server”, and the test then checks whether the page notices without reloading. Two **reverse controls**: flatten `stopImmediatePropagation` (that one line and nothing else) and the same Enter sends the message; replace the refresh with an empty function and the new skill stays invisible forever. 11 mutations injected into the real code one at a time (swap in `stopPropagation`, make Enter ignore the selected row, invert the arrow directions, drop the wrap-around, stretch the throttle to half an hour, fetch the list but never repaint), plus 2 equivalent rewrites as controls, none of which misfired. - **Sep 22** **Clicking the + on the sidebar’s “Projects” row flashed once and did nothing.** That + sits inside the whole “Projects” row, and the row’s own action is “open the projects page”. Sidebar clicks are delegated on `.side-nav` and resolved with `closest(".item")`, so hitting the + and hitting the row are the same event to it: switch the main area to the projects page, refetch projects, repaint all of `#proj-list` — wiping out the input that had just been inserted. **The “flash” was not an animation; it was two actions fighting, and the second one painting over the first.** The create entry point changed too: it used to prepend an input that only took a name, so the working directory, the project instructions and which library to mount could not be set at all and you had to go back in afterwards; the + now opens the same editor as “New project” on the projects page. A sweep of `public/index.html` confirms this is the only sidebar row with a clickable control nested inside it, so the bug is a one-off. All 6 new assertions run against the real page: a throwaway static server serves the real `index.html`, the real + gets clicked, and the test checks whether the dialog opened and whether the main area was switched out from under it. One of them is a **reverse control** — drop the propagation stop, put the old inline input back, and the “flash and gone” reproduces on the spot. Without it the others would only prove “it works now”, not “this is the line it broke on”. 2 mutations injected into the real code one at a time, plus 1 equivalent rewrite as a control, none of which misfired. - **Sep 22** **On a phone you had to jab at that row of buttons two or three times to hit one.** Measured at 390 wide: of 50 clickable things, 9 had a side under 36px — the send button was 32 square, and attach, sidebar, model picker and the topbar controls were all 30 tall. A fingertip contact patch is roughly 8–10mm across, which on a 390-wide screen is about 40px, so a 30px button makes every tap a gamble. **That is not “a bit small” — it is the same action working sometimes and not others**, and the person cannot tell whether they missed or the app ignored them; it just feels sluggish. Sizes are now chosen by `pointer`: on touch both control-height tokens go to 44, the send button becomes 44 square, the topbar’s “files” and “interject” and both rows on the empty screen (scene tabs and the suggestions under them) get 44 of height. The ones wedged inside list rows (the trailing “more”, the + on a group header) cannot have 44 and get 40. The × on a chip was 18 square, and a chip is only 26 tall, so its hit area spills out of the chip with negative margins — it still looks 18, it is 32 to your finger. **Gated on pointer rather than window width**: a desktop browser dragged narrow is still a mouse and there is no reason to spend density on it; touch is what needs the room, and only `pointer` can say which one you are. The geometry already hung off those two tokens, so two lines here grow the whole family at once. Measured again afterwards: on touch, 0 under 44 and 0px of horizontal overflow (growing buttons is exactly how you push a layout into sideways scrolling); on mouse, nothing moved and nothing is below 24×24, the WCAG 2.5.8 floor. The 15 new assertions do not read CSS text — they start a throwaway static server over `public/`, load the real `index.html` in a 390×844 window and measure real geometry; the touch pass turns on touch emulation over CDP, because `Emulation.setEmulatedMedia` does not support the `pointer` feature at all. The first version reported identical numbers for both passes for exactly that reason, so **“is this pass really coarse?” is now a premise assertion of its own** — a false premise makes every line below it green and meaningless. Controls that cannot be measured because the sidebar is collapsed and no chip exists yet are controls that were never tested (two mutations slipped through the first sweep right there), so the test now builds the shell the real selector needs and measures it off-screen. 12 mutations injected one at a time into the real stylesheet (including “the whole block does nothing”, “gated on window width”, “send button grows wide but not tall” and “the regular token was left behind”), plus 3 equivalent rewrites as controls, none of which misfired. - **Sep 22** **The desktop pet was ticked on by default — with nothing on the desktop.** Three places in the backend agree that an unconfigured pet is off (`server.js`'s `enabled === true`, `pet.js`'s initial value, the fallback in `electron-main` when no config can be read); only the settings card rendered `enabled !== false`. So someone who had never touched the switch opened settings, saw “show desktop pet” neatly ticked, and found the corner of their screen empty — they would go and debug why the pet never appeared, not why the checkbox was ticked. **A UI that lies about state is harder to chase than the wrong state itself.** The switch moved out while we were in there: the only one used to sit in a card you had to scroll down to under settings → assistant, so letting the pet out for a while, or making it disappear before a meeting, took three levels of digging — **a switch that exists but cannot be found is no switch at all**. It now sits in the avatar menu at the top right, right below “language”, as the same pair of On / Off pills: one click, the menu stays open, the pill flips in place. The click flips the UI first and saves after; a failed save flips it back and says so in red — leaving the screen showing a state the server does not agree with is worse: you think you turned it off, and the thing is back on the desktop next boot. Pure server mode (`npm start`) has no desktop window, so the row is not drawn at all — a switch that does nothing when clicked hurts more than no switch, and the settings card is still there to explain why. Not one new dictionary entry: the label was already in there, and the pills ride `data-i18n-skip`, so English reads On / Off and neither side of the 90% i18n coverage gate moved. The avatar-menu assertions went from 29 to 45 (negative controls included: `available:false` draws no row even when `enabled` is true, clicking the pill already selected does not hit the server again, and a genuinely-on pet does tick the settings box). Two of them hold the save mid-flight to measure **the frame where the request has gone out and not come back**: dropping `p.enabled = next`, or not repainting until the save returns, stays green if you only look at the final frame (a successful save swaps the whole `settingsCache` for the server’s copy, so the last frame looks identical) — which is exactly how those two mutations survived the first sweep. 23 mutations injected one at a time into the real source, plus 3 equivalent rewrites as controls, none of which misfired. - **Sep 22** **The release page didn't say what changed in this version.** All it carried was an install guide and a `Full Changelog` compare link — and the one thing someone standing in front of a download wants to know is whether it is worth updating right now, which meant clicking through and reading twenty commits. That sentence already existed: it is the plain-language list in the README's "what's new", it had simply never been carried over to the release body. `scripts/release-notes.js` now mines it **by tag range** — the lines *added* to the README between the previous tag and this one — and puts them at the top. By tag rather than by date, because two patches cut on the same day would make a date-based rule print the previous version's changes all over again (the e2e assertion compares two real tags for duplicate entries — and while being written it immediately caught a bug in the script itself: `main()` accepted a tag argument but still read `process.argv[2]`, so both calls mined the same version). When there is nothing to mine it writes nothing at all: the first tag has no predecessor, and a shallow CI clone cannot reach the previous tag — falling back to "install guide only" beats printing something of unknown provenance in a release body. The release-pipeline gate went from 22 reverts to 26, the four new ones all covering paths where this section would **silently go blank** (script gone, computed but not placed in the body, the step not calling it at all, the release job's checkout going shallow); the "checkout must be full-depth" rule is now checked per job, since release.yml has two checkouts and regexing the whole file lets one go shallow while the other keeps the rule green. - **Sep 22** **A long session has to be compacted first, and for those ten-odd seconds the screen said nothing.** Compaction is itself a call to the model, and it sits between **pressing send** and **the first token** — with nothing on screen, all you see is a spinner that won't say what it is doing, and you are left guessing whether the model is stuck or the network is down. (The context meter promising "it will compact once before the next turn" made the silence harder to read, not easier: it said something would happen, and then nothing happened.) It now announces itself: a line appears in the process area reading "compacting the earlier 20 messages into one summary… 8s so far (this turn starts after it finishes; the originals are archived, not deleted)". There is no real progress to report, and drawing a bar that crawls at a constant rate would be a lie, so it reports only the two things it can state accurately — what is being compacted, and how long you have waited. The seconds are driven by the per-turn 1-second timer that was already running, not a second one that could be left behind. **Announcing a start obliges an ending**: succeeded, the model call threw, the model returned an empty summary — all three emit a result that replaces the text of that same line (rather than stacking a second line, which would leave a permanent "compacting…" frozen in the history, looking like a hang). A failure says which error, says that nothing earlier was touched, and points at where to raise the budget; it never pretends to have succeeded. And if the backend ever fails to close the loop, the end of the turn sweeps up any line still marked running. `compact_start` is transient and deliberately stays out of the recorded-event list — recorded, replaying an old session would re-enact a "compacting…" line at a moment when nothing is being compacted. 10 end-to-end assertions (including a reverse control: below the threshold it must say nothing at all) plus 25 frontend ones, 11 of them measured in a real window — the ticking is verified by the text actually changing, not by the function having been called. - **Sep 22** **The "vision model" slot in settings was pushing aside a main model that could already see.** The old rule had exactly one condition — is that slot filled? — and a filled slot always bypassed the main model. So a real configuration ended up like this: the main model and the vision slot held **the same model id**, just reached through two different channels, which means one model split in two, a second API key and a second rate-limit budget; the dedicated channel kept returning 429 and timing out while the main model answered first try. What settings has always said is now what the code does: **the dedicated vision model is there for people whose main model cannot see.** If the main model can, it gets the image, and the answer ends with a line saying "didn't route to your `X`" — silently bypassing something someone configured by hand makes that configuration vanish from their point of view. Same model id across two channels is recognised by the id, not the channel; `look_at_image(model: "…")` always gets exactly what it names. "Can it see" is read off the capability checkbox they ticked themselves, and only falls back to guessing from the model name (sharing `capOfModel` with the settings dropdown) when an older config has no capabilities recorded — a name is a thin disguise, and there was something better to ask. A wrong tick doesn't cost you the image either: when the main model answers 400 "doesn't support images", the dedicated channel takes over and the answer says where to untick it. The settings tile says who is actually looking, too — "your main model can see, it gets these · N spares standing by" — otherwise someone who put a model in that slot goes on believing it handles every image, when it has never received a single request. - **Sep 22** **The image you dragged in for it to look at came back out as "produced this turn".** Attachments upload **before** the message is sent, and the upload endpoint needs the session id to claim this conversation's output folder — but on a brand-new conversation that id was not minted until you **pressed send**. So the upload carried `null`, the server had nowhere to put the file and no note to move it later, and the image landed one directory **above** the task folder. The agent couldn't find it in its own working directory, went looking, and copied it in — and that copy is a file written this turn, so the user's **input** was faithfully recorded as output. The id is now minted the moment an attachment goes up, and "is this a new conversation" is decided by whether the sidebar has a row for it — having an id is not the same as being listed. While in there: the task folder name treated the media anchors appended at send time as words the user had typed, which is why the data holds a folder called `任务_0921_图片1IMG8037JP` — an ordinal and half an extension filling all 12 slots, with not one character of the question he actually asked. Anchors are stripped first now, and dragging in an image with no text falls back to the file name. - **Sep 22** **Once you hit send, which image you just dragged in is reduced to a file name.** The row of 64px thumbnails above the composer only exists before sending; afterwards the bubble carried a single paperclip chip reading `IMG_8037.JPG` that didn't respond to clicks, and checking what you actually sent meant hunting for the name in the file library. Images now render as real thumbnails **above** the bubble (aligned to its right edge, 220px on the long side, always `?thumb=320` — one original here is 2800×7032, about 75MB decoded), while non-image attachments stay as named chips inside the bubble: you recognise a picture by its picture and a file by its name. Both open the preview panel on the right, which carries "download" and "reveal in folder" — that is what **finding** the file actually means. Paths resolve in three tiers: what the server said at upload time is the most accurate, then the session folder, then the workspace root — images uploaded before the session-id bug above was fixed physically sit in the root, so a tier-2 miss retries tier 3 once before degrading to a grey chip with the name on it and a tooltip saying the file can't be read any more (renamed, moved or deleted). Nobody gets shown an empty grey box. 2223 frontend assertions across 63 groups (the bubble group went from 28 to 50, 7 of them measuring `getBoundingClientRect()` in a real window: thumbnail above the bubble, right edges aligned, 220px cap, aspect ratio intact, the grey chip actually visible); 17 mutations, one at a time, into the real source. - **Sep 21** **Hit "quote" and 400 characters of someone else's words landed in your input box; drop in an image and a `【图片 1:xxx.png】` line landed there too.** Both are protocol the model reads, and both were being poured into the box a human types in: after quoting you had to scroll past your own quote to keep writing, and deleting one character of that image line broke the anchor on the spot. Feishu, ChatGPT and Claude don't do it that way, so neither do we any more. **A quote is pinned above the composer as a card** (who said it · two lines max · × to drop it · click to jump back to the original, and it says so out loud when the original is no longer in this conversation), and selecting a passage inside a reply pops a "quote this" button right where you selected — it quotes only that passage, and only if the selection really sits inside that reply (text selected elsewhere must not get filed under it: the screen looks perfectly normal while you quote someone else's words). **Images become a row of 64px thumbnails above the composer** (the name moves into the tooltip and the accessible label, with an ordinal along the bottom edge), while files stay wide named chips — you recognise an image by its picture and a file by its name. The protocol is assembled at the moment you press send — a leading `> ` block, then one `【图片 N:…】` per line, then the compatibility list at the end — so **the bytes the model receives are unchanged** and old sessions replay exactly as before; an anchor the user typed into the sentence himself keeps his position and is not repeated at the top (repeating it makes one image look like two, and the model dutifully builds a four-panel collage). The bubble folds both away too: a leading `> ` becomes an expandable quote card (three lines max) and the anchors become the row along the bottom (image / video / audio / excerpt / file each with their own icon — a row of identical paperclips is the same as no icon at all), while **an anchor written mid-sentence is left alone** — folding it would turn "put 【图片 1:a.png】 on the left" into "put on the left" when the text he actually sent was fine. Quoted text is model output, so both places use `textContent`, never `innerHTML`. 2176 frontend assertions across 63 groups (46 for quoting, 28 for the bubble): the bubble renderer lives inside the `createTurnUI` closure, so the block is cut out of the real source and run through `new Function`; folding and the two-line cap are measured as real clipping via `scrollHeight > clientHeight`, because `-webkit-box` computes to `flow-root` on a flex item and asserting the keyword would look right while the clamp was gone. 12 mutations, one at a time, into the real source: 12 red, each on the assertion meant to catch it. - **Sep 21** **To find out whether the Claude Code / Codex on your machine actually works, you first had to switch to it and use it.** One control on that settings page told the truth — "test connection" — and it lived inside the expanded area, which is only rendered for the engine that is *already selected*. Now "give it a try" sits on every installed card, one button per card, no switching first. The badges were drawing conclusions too: a successful `--version` earned a green "installed ✓", but that only proves a file exists — not logged in, subscription lapsed, rate limited all wear the same green. Until a real run happens the badge is neutral, "found 2.1.278 on this machine"; green is reserved for a call that actually came back, and a failure turns it red and says which command to run next. Version strings are normalised to one shape while we're here (`2.1.278 (Claude Code)` and `codex-cli 0.154.0` both show just the number on the badge — the heading already says the product name), with the raw string and the launch command moved into the expanded area: `claude -p --output-format stream-json` on a collapsed card tells someone who just wants to use their subscription nothing at all. Verdicts survive a re-render, so saving settings doesn't cost another round of tokens; clicking the button or the verdict never switches engines — trying and using are two different things. 29 new frontend assertions (4 of them reverse controls: the button is not inside the expanded area / the command line was moved, not deleted / clicking the card body really does switch / red is a measured colour, not a counted class name), and 6 mutations that put each old behaviour back were all caught by the matching assertion. - **Sep 21** **"This turn's output" filled up with file names you'd seen somewhere else.** Nothing was crossing between conversations — the cards had unpacked a bundle. That turn ran an export script that copied the whole repository into the task folder: 124 files written, 122 of them in a single directory. Cards show a file name and not its directory, so what you got was a row of familiar names with byte counts identical to the ones in the repo root (they were byte-for-byte copies), and the plan the task had actually produced was squeezed down to one card slot. Two conditions together decide it: this directory took 8 or more files this turn, *and* the ones worth showing are a minority. Without the second one, an image task that produces 12 storyboard frames in one directory would collapse into a folder icon — and those 12 are the real output. A qualifying bundle becomes a single folder card with one way in; the change list below is untouched, and all 122 files still resolve to full paths — what got folded is the card strip, not the facts. Also: a folder is alive as long as a file under it is alive (its own name never appears in a file list, so checking `alive` retracted it on every event), and language-suffixed files like `README.en.md` no longer pass as deliverables. 15 frontend assertions, 3 of them reverse controls. - **Sep 21** **The address baked into the phone-pairing QR code was a guess, and a wrong guess scans into nothing.** The old rule was "take the first private IPv4 you find", and `os.networkInterfaces()` guarantees no order — measured on this machine: the real interface is 192.168.1.84, the proxy tunnel 198.18.0.1 comes ahead of it, and even `route -n get default` points at the tunnel, so "follow the default route" is wrong too. On a machine with Docker or a corporate VPN, 172.17.x / 10.x can easily come first, and into the QR code goes an address the phone can never reach. Interfaces are now ranked: physical first, tunnels, Docker bridges and VM adapters last, ties broken by name so a refresh doesn't hand out a different address each time. Ranking well is still guessing, so the endpoint returns up to 3 candidates, each with its own QR code, and the screen offers "doesn't open? try another address". That screen also says three things it never said: what the code is for, where the address came from, and when it won't work (different network, mobile data). Caught along the way: 6 calls of `toast(…, "err")` — the second argument is an icon name, `#i-err` matches none of the 169 sprite symbols, the browser neither errors nor warns, and you get a 15×15 box with nothing in it, so "copy failed" and "couldn't remove them" had no icon and weren't red either. `test/icons.js` gained a gate for icon names that flow in through a parameter (102 literals checked against the sprite). - **Sep 21** **One row in the shortcut table only ever popped up "not supported yet".** All 19 were walked through in the real app: 2 are handled by the input box itself, 16 really do something, 1 was dead — ⌘D "voice recording", whose entire body was `toast("voice recording isn't supported yet")`, with no recording UI anywhere in the frontend. It sat in settings among the other 18 looking exactly like them, so there was no way to tell which rows were real. Removed in all three places (table row, action, English string); the "N shortcuts" line reads the table length and went from 19 to 18 on its own. The gate doesn't grep the source — it parses both real tables and reconciles them, with a reverse control for each of five ways to break it: placeholder actions, empty actions, a row with no action, an action no row can reach, and a row marked system-level that the main process never registered. - **Sep 21** **Search knew three vendors — Jina, Tavily, Brave — so users in China had to tunnel out first.** Eight now, domestic ones first by default: Bocha, Zhipu, Qiniu, then Tavily, Serper, Jina, Brave, plus a **custom** slot — point it at a URL and it talks to your own SearXNG, or to the vendors I could not verify line by line (Aliyun IQS, Metaso, Volcengine). However many you configure, it falls through them in order when one is down, and only then to the keyless free channel. Quota is held up front and refunded when the free channel is what answered, and the bill records the vendor that actually returned results, not the one picked in settings. Responses are recognized by **shape** (a handful of known array positions, tried in turn), so a vendor wrapping its payload one layer deeper degrades to "nothing found this time" instead of taking the agent down with it. The free fallback's timeout went from 30s to 8s — it is a fallback; making someone wait the full 30 seconds is worse than saying "no results" early. The numbers in the price table come from public sources and were not checked against each vendor's page one by one, and the UI says so. - **Sep 21** **A turn whose process died mid-run spins "running…" in the history forever, with nothing anywhere to stop it.** Replay ends a turn when it reaches the matching "answer" record; a question with no answer after it (service restarted, process killed) never gets an end drawn, so the spinner in the header keeps going — and at that point the session is not in runningSessions at all, so there is no stop button anywhere on screen. Replay now notices that the last question was never answered and closes it as interrupted: "Interrupted · 1 step", with "no end event — this turn was cut off" on the header. 3 new frontend assertions, one of them a reverse control: a turn that ended normally still says "Done". - **Sep 21** **A fork you already answered asks again every time you open that conversation.** Replaying history adds `done` to the question card — that class means "not clickable any more"; but "the verdict is already painted" was checked with the same class, so the step that paints the verdict was blocked by its own side. A fork settled days ago still read "There's a fork here, I'd like you to decide / this question comes from the history", which looks like a conversation stuck waiting for someone. With the two meanings split into two marks, the same conversation replays as "You settled this fork · you chose 投资决策参考". While in there, the original hypothesis got measured (replay animation too slow, hence the illusion): the whole replay takes 21ms — not it. 4 new frontend assertions, one of them a reverse control: a fork that was never answered must not invent an answer. - **Sep 21** **A brand-new account opens the sidebar and finds the previous owner's entire history.** Sessions carried over from before accounts existed have no owner recorded, and “no owner” used to mean “everyone's” — meant to keep history from disappearing, it handed every new demo account somebody else's work the moment it opened. They now belong to the first person who registered on this machine (back when there were no accounts, that was whoever sat at it): his history is intact, a new account's sidebar is empty. Empty shells still pass — the frontend sends the session id before the first message, so blocking them blocks every new task. Two ★ assertions in e2e, one each way. - **Sep 21** **The library was one shelf for the whole machine: upload anything and everyone saw it.** The only line you could draw there was “whose thing is this”, and it only held for writes (non-admins got a “read-only” badge) — reads leaked anyway, because every user's agent carries library_list / library_read, so “have a look in the library” read out somebody else's contract. Each account gets its own root now: the old library stays where it is and belongs to the first registered user (not one byte is moved), everyone else gets their own, created when first used. The write gate came out with it — separate roots plus a write gate means a member stares at his own empty folder unable to put anything in it, which is worse than before; the “Shared library · read-only” face on the page is gone too. tenant: 498 pass / 0 fail, all 8 mutants killed. - **Sep 21** **Switch accounts and the pet and notification channels are still the last person's.** The engine, the pet, the shortcuts — all of it is per-person by nature, and all of it lived in one config.json. It is stored per account now, and mounted alongside the workspace, quota and policy when a request comes in. The “personal desktop” shortcut (read config as before) went from two conditions to three: Electron shell + listening on localhost only + **exactly one account on this machine** — two accounts on the same Mac and it stops being true, and taking the shortcut then means one employee can edit the owner's keys and MCP servers. - **Sep 21** **You could not change your own password.** The avatar menu has “Change password” now: it checks the old one and signs the other devices out. The password rules shown are the sentence the server returns for the org's policy — the frontend does not keep a second copy, or the admin changes the policy and this page still says “at least 6 characters”. - **Sep 21** **“I fixed that channel” and the AI still says it's down.** The local breaker had three ways back: the config changed, the user hit Save on the settings page, or the 30-minute cooldown expired. But the user fixed the far end — topped up, renewed, got the network through — and never touched the settings page, so the fingerprint never changed and all three missed. There is a fourth now: **a new message from a human reopens it**. Reopening alone isn't enough — last round's “don't call this tool again” is still sitting in the conversation history, the model reads it and keeps refusing, and the user still sees “it's down”. So the channels just reopened are written into this round's prompt, saying that sentence has expired, call the tool normally if you need it, and don't conclude anything without sending a single request. The cost is at most one extra request. - **Sep 21** **Links came out with a stray `<strong>` after them, pointing somewhere wrong.** Bare-URL linkification ran before bold, so in `**https://…/abc**` the two trailing asterisks were swallowed into the href, and the bold pass then inserted a `` in the middle of that href. Moving linkification last cuts the path — by then `**…**` is already `…` and the URL has clean edges. Two more while in there: the whole inside of a tag is skipped (a URL in an **attribute**, like `see https://…`, used to blow the attribute open), and a trailing right parenthesis is kept when it pairs and trimmed when it doesn't (on Wikipedia or Lark, `.../Foo_(bar)` goes to a different page if you trim one character too many). - **Sep 21** **The people who can't open the app get a full page of Chinese.** Same root cause as the delete confirmations below, one layer further out: the native context menu, the boot-failure page and the two system error boxes are all main-process JS strings that never enter the renderer's DOM for a moment, so the web translation cannot reach them. All 8 context-menu items came up in Chinese under English. The boot-failure page matters more — the person reading it is the person who can't open the app (the issue #1 crowd), the one who most needs to understand how to fix it, and what he got was a page of Chinese. The shell carries its own dictionary now (29 entries each): it asks the renderer for the preference the web layer already stores (`owb-lang`, the same key), and falls back to the OS locale only when there is no answer — or no window yet, which is exactly when the boot-failure page is drawn; that page's `` follows it too. The gate compares the two dictionaries by **shape** (same keys, no Chinese left in the English one, no entry identical in both), runs the context menu and all six failure paths in both languages, and scans the string literals in those four regions so the next hardcoded sentence fails. New e2e section; all 5 mutants killed. - **Sep 21** **The storyboard card on the canvas is called “New note” in the dropdown.** Of the 12 node kinds, `canvasDefaultPayload` was missing the storyboard branch, so it fell through to default and got the note payload. The card face hides it — its header string is hardcoded — but in the “Connect to a downstream node…” dropdown it reads “New note”, and the text handed to the Agent is the note placeholder, “Jot down an idea, a task, or something to fill in later…”. The card the starter template lays down is exactly that one. It has its own default now, plus a gate that compares by **shape**: the kind table and the case list are two separate lists, and a mismatch fails (note excluded — it *is* the default branch). Delete that line and e2e goes red on this assertion. - **Sep 21** **In English, every “Delete” confirmation still comes up in Chinese.** All 31 delete confirmations used the browser's native `confirm()`, and translation walks the DOM — the sentence inside a native dialog is only ever a JS string and never enters the DOM for a moment, so even the gate that counts leftover Chinese characters on screen couldn't see it. The same root cause left two other debts: once a user ticks “prevent this page from creating additional dialogs”, `confirm()` silently returns false and every delete button goes mute (the same shape as the old `prompt()` bug where clicking did nothing), and a native modal can't be clicked offscreen, so none of those 31 delete paths had ever been exercised. They all use the app's own dialog now: 59 dictionary entries + 15 pattern groups (one of them expanded over 8 nouns), a verb on the red button (Delete / Uninstall / Dissolve / Kick out / Retire / Reset), focus on “Never mind”, and the things about to be deleted listed one per line — the list is height-capped and scrolls, because a dialog pushed off screen takes “Never mind” with it, which is worse than no confirmation at all. The gate matches the **shape of the options object** rather than “written inside askConfirm's parentheses”, and that alone caught two more misses: the type-a-workspace-path dialog, and the bulk automation delete whose options are passed through a helper. The gate scans all of public/js, the enterprise admin included — it draws its own dialogs today, but nothing stopped it from reaching for confirm() tomorrow. test/confirm-dialogs.js: 25 pass / 0 fail, all 14 mutants killed. Also deleted a conclusion in library-mkdir that had been falsified — “confirm doesn't throw, so the site-wide dialogs don't need changing”: the measurement still holds, the conclusion stopped holding a while ago. Swapping the dialogs then exposed two older test files: their stubs still replaced `window.confirm`, so the `askConfirm` call in the source became a ReferenceError — and since it throws inside an async handler nobody catches, “cancelling sends no request” stayed green for the wrong reason. A positive control now sits next to it: clicking OK on the same button has to actually send one. - **Sep 21** **Create a folder, then go looking for a way to delete it — there isn't one.** Delete only existed in the list layout: icon and gallery set `display: none` on it, and search results never drew the button at all. All three layouts have it now (pinned to the tile's top-right corner in icon and gallery), search results included, and standing inside a folder puts "Delete this folder" right next to "New folder" — you create and delete from the same bar, and deleting the folder you're in walks you back up a level. Workspace output still gets no delete button: that's what a task wrote in the working directory. The dead end on a non-empty folder is gone too — that button used to read "Got it" (got it, and then what?) and now reads "Open and empty it", which takes you into that folder. test/library-mkdir.js: 85 pass / 0 fail (was 65), all 9 mutants killed. - **Sep 21** **Switch the workbench to English, open the admin console, and it's still all Chinese.** The console never loaded i18n.js at all — 19 panels, 538 untranslated UI strings. Added 494 dictionary entries + 28 patterns for the ones with numbers and dates in them, plus a 中文 / English switch at the bottom of the sidebar (sharing one preference with the workbench, so switching in either place moves both). **Names the user chose are not translated**: organizations, departments, channels and models are data, not UI — translating them would effectively rename someone's channel. Numbers follow the language too (1235 万 / 12M), and switching repaints the current page, because the 万 bucket and the thousands separators are computed at render time and no DOM walk can reach them. New gate: switch to English, walk all 19 panels, and a single Chinese character left in the DOM counts as a miss; 11 remain and every one of them is a name the user typed. admin-ui: 145 pass / 0 fail (was 134), all 7 mutants killed. - **Sep 21** **Hold a card mid-drag, let a sync land, and the card snaps back to where it started.** Same root cause as the entry below: applying a snapshot is a full `graph.clear()` rebuild, so the card being dragged is torn out from under the pointer. "Sync waits while you type" is now "sync waits while your hands are busy" — typing and dragging both count, and the timer runs from your last movement (not from when you pressed down; lining a card up can take half a minute). A drag released outside the window can't stall sync either. test/canvas.js: 58 pass / 0 fail (was 52), all 11 mutants killed. - **Sep 21** **Type on the canvas, let a sync land, and the half-sentence you just typed is gone.** The inspector is rebuilt wholesale, so the input becomes a new one: focus falls back to the body, the rest of what you type goes nowhere, and the node's text is overwritten by the incoming copy. Sync now waits while you type (resuming 10s after your hands stop, so a caret left in a field can't stall the tab forever), and a rebuild puts the caret back on the same character. test/canvas.js: 52 pass / 0 fail (was 47), all 7 mutants killed. Also fixed a test that failed at random: a hidden window's confirm dialog is sometimes dismissed by Electron outright, so the check is now "does it throw" (prompt throws, confirm doesn't — both measured in the same window) rather than "does it block". - **Sep 21** **Deleted edges came back — on a sync, on a reload, and on undo.** Two rules meant to protect older boards: "keep my edges if the incoming snapshot has none", and "one shot plus one named location, so draw the link back in" — but "no edges" can also mean the user just deleted them. Boards now save as version 2, meaning edges are recorded as they are and empty means empty; version 1 files are still protected and still get inferred links, and one delete upgrades them. Also fixed the flaky canvas test that failed v0.7.4's release job (a hard-coded 2.3s wait against a 1.8s sync loop that skips a whole round while a save is in flight), which is why v0.7.4 shipped no installers. test/canvas.js: 47 pass / 0 fail (was 40), all 7 mutants killed. - **Sep 21** **Deleted boards came back from the dead; emptied boards grew cards on their own.** Deleting a board only removed the server's copy — the local one, keyed by board name, stayed — so creating a board with the same name brought the deleted cards right back, and saved them to the server again. And the two starter cards were seeded whenever the board "is empty right now", so a board the user had deliberately cleared regrew them on every open, on any machine. Deleting or creating a board now clears the local copy too, and starter cards are only for a board nobody has ever touched: a newly created board keeps `updatedAt: 0`, and every save after that (clearing included) stamps the time. test/canvas.js: 40 pass / 0 fail (was 30), all 18 mutants killed, plus 2 more on the server side. - **Sep 20** **Open another project and the canvas showed the previous project's cards.** The local copy was keyed by board name only, so both projects' `main` shared one slot — and once those cards were laid out, the next save overwrote the new project's own canvas. Same for switching boards: the save waits 240ms and only then reads which board is current, so the old content landed under the new board's name. The key now carries the project (an older unscoped copy is claimed once), a pending save is flushed before any switch, and every write re-checks who it was meant for. test/canvas.js: 30 pass / 0 fail; the same tests fail 8 assertions on v0.7.2, and all 11 mutants are killed. - **Sep 20** **Three silent data losses on the infinite canvas.** Nodes titled 「开始工作」 or 「开始创作」 were dropped on every load and sync, their edges with them — but nothing in the code has ever created a card with those titles, so the only ones it can hit are names a user typed; the pruned result was then saved, shrinking the local copy and the server's alike (3 nodes + 2 edges in, 1 node and 0 edges out). A snapshot pulled from the server was applied and immediately written straight back, and the server re-stamps its timestamp on every write — so two open tabs shove each other every 1.8s, each write also copying a .bak, with nobody touching the canvas. And one sync collapsed a multi-selection down to one: select 3, sync once, and 1 is still selected — the next Delete then removes something else. These 2462 lines had no tests at all; the new test/canvas.js goes 6 pass / 9 fail before the fixes to 16 / 0 after, with all 6 mutants killed. - **Sep 20** **In English the library page was a wall of Chinese — and it's why v0.7.0 shipped no installers at all.** The release stopped at the test job: the CI machine runs in English, the UI rendered in English, and the assertions were written against Chinese copy. Chasing that turned up the real defect — the page had no dictionary entries at all, so English users saw 22 Chinese text runs + 15 Chinese attributes, including the paragraph that explains what "new folder" is even for. 59 entries + 11 patterns for the ones with counts and file names in the middle; the empty state now puts one whole sentence per text node (a `` mid-sentence makes English word order impossible). The site-wide coverage gate is a percentage, so one fully-untranslated page averages away — hence a second gate: render the page, switch to English, count the Chinese left on screen with the user's own file names stripped out first. A test that opens a real page and doesn't pin the language now fails. Library suite 65 pass / 0 fail (was 57); pull the pin and 8 assertions fail on an English machine. - **Sep 20** **The library now says what "new folder" is for, and you can delete from it.** The page used to mix the reference material you upload with the outputs a task writes, while "New folder / Upload" floated above both — so they read as acting on everything below. The sections are now separate and those buttons live in the reference one. The empty state says it outright: a project can mount just one folder, so its agent sees only that slice and never wanders into another client's material — folders here are the agent's field of view, not a filing cabinet. Folders and files each get a trash icon (the outputs section deliberately has none), the confirmation focuses Cancel rather than Delete, and a non-empty folder gets no Delete button at all — it says how many things are still inside and sends zero requests. All three Electron dialogs were actually measured: `prompt()` throws on call (that's where v0.6.12's "nothing happens" died), `confirm()` / `alert()` hang on a native modal — fine for users, unclickable from an offscreen test, so new irreversible actions use a hand-drawn one. The assertion that proved `confirm` works via `typeof` now really calls it. 57 pass / 0 fail (was 36). - **Sep 20** **Three more admin-console responses were still growing with headcount: 678 / 491 / 631 KB → 11 / 12 / 9 KB.** The usage-detail page shipped a per-person rollup every request (195 KB of a 205 KB response at 3,000 seats, on a page that draws transactions); the relay-key page shipped the per-person API cap table (620 KB of 631 KB was one line repeated three thousand times); the stats page computed every member's role, quota and balance plus a ledger pass to read one integer. Member usage and per-person caps each got an endpoint, 50 per page, filtered/sorted/paged server-side; grouped tables truncate to 20 / 50 and carry the real group count, or twenty reads as *all*. Those two tables sort **deliberately not in roster order** — they answer "where did the money go" and "whose limit needs changing". "Spent this month" is pinned across three call sites: top-ups aren't spend. Tenant 485 pass / 0 fail (was 435), admin UI 134 checks, relay 218 pass, **all 15 mutants killed** — the first round left 4 survivors, i.e. four assertions were lying (no top-up in the fixture, ownership check only asserted a status code, the out-of-quota count was only compared unfiltered, and ten people fit one page so the boundary was never crossed). - **Sep 20** **The members page shipped the entire roster to the browser and let it filter locally.** A screen shows a dozen rows; the response carried every single person in the company — role, department, status, balance, last active, all of it. Measured on a 3,000-person org, opening the members page was **1041 KB**, painted **3,002 rows / 78,098 DOM nodes**, and took **878ms** from click to table. And that was just the members page: the same full roster was bundled into two other places — the roles page shipped **1042 KB** every time (to draw a table of a dozen admins), and the usage detail page bundled one on every request, which is why `?limit=` could not shrink the response at all: `limit=20` was **682 KB**, `limit=1` was still **678 KB**. Filtering, sorting and paging now happen on the server, 50 people per page: at 3,000 people the response goes **1041 → 17 KB**, DOM nodes **78,098 → 1,403**, render **878 → 38ms**, backend 18.8 → **12.2ms**; at 1,000 people 346 → 17 KB and 259 → 28ms; at 300, 104 → 17 KB and 90 → 22ms. Typing one character (including the full round trip) returns 52 rows in **57ms** at 3,000 people. The roles page goes 1042 → **48 KB** (just the admin roster, plus a candidate list carrying **only the five name-ish fields**, capped at 500 and *saying so* when capped — otherwise "that person isn't in the dropdown" reads as "that person doesn't exist"); the usage detail page now bundles the roster only on request, 682 → **200 KB**. Two orderings in here are invisible and silently undo the whole thing if broken: **sorting must happen before slicing** (otherwise "page 1" depends on the physical order of the ledger, and who comes first today depends on who edited their profile yesterday), and **computing must happen after slicing** (every person costs a role, a quota, a remaining allowance, a balance, and a scan of the usage ledger for "last active" — computing six hundred to display fifty means five hundred and fifty were computed for nothing). The same applies to the rank floor: filter the **raw accounts** first, don't compute and then throw away. A hand-edited `?limit=99999` still returns 500; `limit=0` / `limit=-5` now fall back to one default page (they used to clamp to 1, turning the page into "one person per page, six hundred pages"); paging past the end lands on the last page instead of an empty table (an empty table looks exactly like "this company has no people"). **One more failure mode, fixed — the kind only real typing can measure**: on all three of these pages the search box lost the caret as soon as the data came back. `route()` repaints the whole body, the search input's DOM node is thrown away, focus falls back to ``, and the second character you type goes nowhere. Measured: after the re-render `document.activeElement` = BODY, and the next keystroke landed on BODY. After the fix: caret still in the box, text intact, next keystroke lands in the search box — verified on members, usage detail and audit. The skeleton flash was measured too (1 frame / 19ms on usage detail, 0 frames on audit); it bought nothing, so nothing was changed. Multi-tenant suite: **435 assertions pass / 0 fail** (was 367). Admin UI suite: **107 assertions pass** (was 88). **19 mutants, all caught** — including "move the sort after the slice", "look up last-active for the whole roster again", "stop clamping limit", "return an empty table past the end", "bundle the full roster on the home page again", and "don't put the caret back after a re-render". **Not done yet**: the per-member usage table still renders one row per person (that request is still 678 KB at 3,000 people), and the "whose usage" dropdown on the detail page is still 3,000 `