CHANGELOG.en.md
Rama main · réplica de lectura del repositorio original.
# 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/<account>/ 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 `</strong>` in the middle of that href. Moving linkification last cuts the path — by then `**…**` is already `<strong>…</strong>` 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 `<img alt="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 `<html lang>` 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 `<b>` 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 `<body>`, 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 `<option>`s (~197 KB) — both of those want a search box, not a dropdown.
- **Sep 20** **The "offboard" button in the admin console did nothing when you clicked it.** Offboarding is the most enterprise-shaped feature in this console — revoke every permission at once, hand running schedules over to a colleague, leave an audit record. The button sat there on the members page, the backend route was fine, the `lifecycle` suite was green, and **clicking it did absolutely nothing**: the click handler read a variable that only exists inside the render function (`shown`), so every click threw an `Uncaught ReferenceError` into the console — nothing moved on screen, so a person just assumes the network is slow, clicks a third time, and goes to find another way. Rendering all 19 panels would never catch this: the rendering is correct, the click is what is broken. Backend tests would never catch it either: they call the API, and nobody had ever clicked that button. The fix is one line, and it fixes the meaning too — handover candidates should come from the **entire active roster**, not from whichever rows the current filter happens to show. So that this failure mode no longer depends on a human happening to click, the admin UI suite gained a gate: walk into all 19 panels, click one button of each kind for real, with an `error` / `unhandledrejection` collector installed in the page, and compare afterwards. **42 buttons clicked, 1 dead.** Copy buttons are not clicked — they write to the **system clipboard**, and a test run must never wipe out what someone is holding there; export buttons have their download action stubbed, or Electron pops a save dialog that hangs forever with nobody to click it. Offboarding then gets one more check of its own: not just "no error", but that the dialog actually opens and the handover dropdown lists **other active colleagues** (never the person being offboarded — handing the work to them is not a handover). Admin UI suite: 88 assertions pass / 0 fail (was 84). Revert that one line and the gate goes red immediately, naming `members/data-off: Uncaught ReferenceError: shown is not defined`.
- **Sep 20** **The platform's org list was assembled by querying each company's member list one at a time.** Each row shows two numbers — how many people, how many active — but behind them sat the function built for the members page: it computes every person's role, quota, remaining allowance and balance, and scans the usage ledger for "last active". Measured: 18.7ms at 11 companies, 59.0ms at 31, **159.0ms and 35.6 MB of disk reads** at 61 (for a 38 KB table), and **388.4ms / 98.3 MB** at 121 — disk reads growing with the square of the company count. Guarding it was a comment that is no longer true: "there are only single digits of orgs, not worth indexing" — while the product is now sold as multi-tenant, and this page is the first thing a platform admin sees. The ledger is now counted once and the rest is addition in memory: 61 companies 159.0 → **2.8ms**, 35.6 → **0.5 MB**; 121 companies 388.4 → **5.4ms**, 98.3 → **0.9 MB**; and the usage ledger is **not opened at all** (it was opened 61 times). Counting people has two easy mistakes, each pinned by an assertion: disabled accounts **still count as headcount** (seats are sold per head, so dropping them undercharges), and legacy accounts with no `status` field **count as active** — the same default the members page uses, because otherwise one company shows two different active counts on two pages and nobody can say which is right. The last assertion simply compares the two. 367 assertions pass / 0 fail; 29 mutants all caught.
- **Sep 20** **The entry fee every request pays before doing anything.** The login gate plus tenant scope runs on every request: identify the user, check session expiry, enforce 2FA, check the remote-device switch, resolve the tenant. It read `users.json` **3 times** and `orgs.json` **4 times** — files holding the **entire platform's** accounts, every live login token and every company, none of which has anything to do with what the request is for. Measured on a do-nothing endpoint: 0.52ms on a 5-user install, 4.16ms at 300 users / 900 tokens / 20 companies, **13.18ms and 2.36 MB of disk reads** at 1,000 users / 3,000 tokens / 60 companies, and **39.12ms / 7.1 MB** at 3,000 users. The chat page polls every few seconds, so "the whole product got slower once the company grew" had nothing to do with who was using it. Each book is now read once per request, and the resolved org and its settings ride along on the request for everything downstream: 13.18ms → **4.84ms**, 2,359 → **782 KB**; at 3,000 users 39.12ms → **13.93ms**, 7,115 → **2,359 KB**. The 5-minute throttle on "last seen" used to read all of `users.json` unconditionally *and then* decide — it saved the write but never the read, and on this path the read is the expensive half. It now decides from the copy already in hand and re-reads only when it is really about to write (writing back the copy taken at the start of the request would erase whatever else was written in between). The assertion counts reads rather than milliseconds: milliseconds drift on a slow machine, counts do not. **One behavior change**: the remote-device switch used to carry its own `try/catch → false`, so with a corrupt `orgs.json` *and* 2FA already enabled for that user, the request was **silently let through**; it now fails with a 500. If neither book can be read, block rather than pass. Session expiry is unchanged: an unreadable org table still falls back to the default 90 days instead of turning a failed disk read into a 500. 357 assertions pass / 0 fail; 25 mutants all caught, 11 of them aimed at this change — the three "go read it again yourself" sites, "the settings become empty", "expiry ignores the org's settings", "throttle removed", "write back the stale copy", "tenant scope drops the tenant when nothing is attached", plus whether the 2FA gate and the remote switch still work now that both read from the same settings object (judged by the `auth-2fa` and `remote` suites respectively).
- **Sep 20** **The members page in the admin console got slower as other companies signed up.** `publicUser` computes three fields per person (monthly quota / remaining this month / balance), and each one called `org.getOrg()` to read `orgs.json` — three reads per member, from a file holding **every** company on the platform. Measured: a 500-member page read `orgs.json` **1,500 times**, pulled 1,302 KB into memory, and took 28.69ms. The uncomfortable part is the case where nothing about your own org changed: the same 50-member page took 5.6ms with 2 companies on the platform, 16.8ms with 21, 76.1ms with 101, and **365.6ms with 501**. What slowed down was not your data — it was your neighbours. The org settings cannot change during a single listing, so the list now reads them once: 500 members 28.69ms → **1.36ms** (1,500 reads → 1), and the 50-member page with 501 other companies 365.6ms → **2.7ms**. Only the read is saved, not the decision: the settings passed down must be **that person's own org's** (the list is already filtered by org), a per-person monthly quota still overrides the org default, and `publicUser` still reads for itself when used on a single user. The assertion counts reads of `orgs.json` rather than milliseconds — milliseconds drift on a slow machine, counts do not. 346 assertions pass / 0 fail; 14 mutants all caught, two of them aimed squarely at the ways this change could go wrong — "it reads once, but it reads someone else's" and "the decision got skipped along with the read, so a per-person quota is overwritten by the org default."
- **Sep 20** **One tenant's ordinary activity could wipe another tenant's audit trail — and it looked exactly like "nothing ever happened there."** Every org's audit entries lived in the *same array* inside `orgs.json`, capped on save with `slice(0, 1000)`. Measured: tenant B records 21 entries, tenant A then operates normally and records 1,200, and B's 21 entries are **gone without a trace** — the admin console reads "0 entries," which is indistinguishable from an org where nothing was ever done, with no warning of any kind. B did nothing wrong. The second bug is the same bug: the audit trail sat on the hot path. `getOrg()` has 37 call sites and each one parsed the entire audit history along with the org data — measured, a single `getOrg()` costs 0.02ms with 0 audit entries, 0.91ms with 1,000, and **45.57ms with 50,000**. That is *why* the cap had to be 1,000, which translates to "we delete compliance records so requests stay fast." Now each org gets its own book: `data/audit/<org id>.jsonl`, one entry per line, appended (O(1) per write, no more read-whole-file/modify/write-whole-file), capped at **5,000 per org**. Isolation is structural — separate files, not a filter someone can forget — so no one can evict anyone. `getOrg()` is back to 0.02ms and no longer scales with audit size; `orgs.json` is down to 1,730 bytes. Existing installs migrate on first start, and the migration **overwrites** rather than appends, so a half-finished migration that crashed can simply run again without duplicating entries. Files are 0600 (this book records who approved which command, same treatment as `users.json`), and a power cut mid-line corrupts that one line instead of making the whole book unreadable. Two lies in the UI went with it: the subtitle used to say "every management action since this org was created" when audit has a retention cap — it now says "most recent N (retention limit reached; older entries have been evicted)" and shows a banner naming the cap, the oldest entry still on file, and what to do next. The export button only ever exported the 50 rows on screen; someone filtering a month with 5,000 hits had to page through 100 times. It now pulls the full filtered range in batches (measured: 5,012 rows in one export, button disabled while running and restored after). 339 backend assertions + 84 real-browser assertions pass, 0 fail; 10 backend mutants and 4 UI mutants all caught (the atomic-write mutant is deliberately marked "known untestable" — atomicity only shows up on a power cut, and it stays in the list as a reminder not to mistake the suite for complete).
- **Sep 20** **Nine silent failures when reading Office documents: whole trailing rows dropped, formulas coming back blank, native charts reading as empty slides.** The old tests only ever fed a dense rectangular table — no blank rows, no formulas, no merged cells, no sparse columns — so all of this was green and would break on any real report. On the Excel side these are exceljs counting traps: `actualRowCount` / `actualColumnCount` are *counts of non-empty rows and columns*, not the index of the last row or column, so one blank spacer row means six rows written and five read back — and the one lost is the totals row. Worse, `truncated` still reported false: data went missing while the caller was told nothing was cut. Formula cells read back as empty strings (exceljs neither evaluates formulas nor writes cached values); they now return the formula itself, `=SUM(B4:C4)`, which is at least true. Merged cells used to copy the anchor's value into every covered cell, so an A1:D1 title repeated four times across and read as four columns of data. On the PowerPoint side, slides built with `addChart` read back completely empty — while `skills/ppt-design` actively recommends addChart. They are now read through graphicFrame → slide rels → `charts/chartN.xml`, aligned by `c:pt` idx (idx skips numbers, so walking document order misaligns categories against values); picture slides were equally blank and are now marked as images with their alt text. On the Word side, ordered lists and bullets both read back as "-", which makes "what does item three say" unanswerable; numbering is now resolved through numPr → num → abstractNum → lvl numFmt into real 1. 2. 3. Hyperlink URLs were dropped entirely, leaving only anchor text, and headers and footers were never read at all — which is exactly where "Internal — do not distribute" tends to live. One hole closed along the way: `.docx` files usually arrive from outside, a `javascript:` hyperlink inside one is perfectly legal, and the preview panel rendering it verbatim handed it a clickable entry point; only http/https/mailto are linked now. On the writing side, `skills/excel-report` now requires formulas to carry a `result` cache value — without it, generated reports show a blank totals column in the web preview, in `read_document`, and on anyone's phone. 368 pass / 0 fail (office-tools) and 45 pass / 0 fail (preview-layout), both with negative controls; 13 mutants, 13 caught.
- **Sep 20** **Green three runs in a row locally, red once in CI: the attachment suite was waiting on time, not on a condition.** Right after v0.6.11 was tagged, CI failed on one assertion: "a dropped file uploads under its own name: 粘贴文本_0920_070258.txt" — a name carrying the runner's own timestamp, as if drag-and-drop had renamed `笔记.md`. It hadn't touched a byte. The first seven cases in that suite waited a fixed 40ms, and `uploads` is a cumulative array: reading a file goes through a FileReader, GitHub's machine is slower than mine, the read hadn't finished inside 40ms, and `uploads.at(-1)` was still the .txt pasted by the *previous* case — so the assertion compared the previous case's attachment against this case's filename. The suite's own comment already said "always wait for a condition, never for time"; the rule was written down and the first seven cases never followed it. There are no fixed waits left in that suite now: each case waits for its own upload to land, and the negative assertions ("short pasted text must not become an attachment") get a real window in which to catch the request that shouldn't come — waiting a single tick only ever proves "it hasn't happened yet". 50 assertions pass, 0 fail. Control: with a 200ms delay injected into `readAsDataURL`, the pre-fix file **reproduces the CI line verbatim** (only the timestamp differs, being local time), and the fixed file passes; delaying only that one `.md` read reproduces it on the exact same assertion.
- **Sep 20** **The boot-failure page said "paste this line into an issue" — and it was the one page where right-click couldn't copy.** The context menu and the "hand external links to the system browser" wiring both sat *after* `require("./server.js")`, and on a failed boot that require *is* the throw site: nothing below it runs. So the page told you to "paste the line below into a GitHub issue" and to "include the startup log", and then selecting that path and right-clicking produced nothing at all — you were expected to copy it off the screen by hand. The "file an issue" link at the bottom didn't go to your own browser either; it opened another Electron window inside the app, with no address bar and no login. This is the third time this file has been caught by the same thing — the 3-second show-the-window fallback and the 20-second watchdog were both moved above that require for exactly this reason, and the comments say so; these two lines were simply missed. They now get wired as soon as the window exists. The GPU advice on that page also now names the **real path** to `config.json`: it used to say "in OpenWorkBuddy/config.json in your home directory", leaving someone already locked out to work out where that is on Windows versus macOS. If the path can't be computed it falls back to a sentence — not `undefined`, and certainly not taking down the last notice board there is. 377 assertions pass, 0 fail, 6 mutants all caught: the two lines moved back below the require (the pre-fix file used verbatim as the mutant), the copy item removed, the "open in browser" item renamed, an empty menu popping anyway, the path reverting to "in your home directory", and the fallback returning nothing.
- **Sep 20** **The boot-failure screen told you to file an issue — even when the gate already knew the fix**. The startup gate checks your Node version and your dependencies and produces a plain-language verdict ("run `npm install`", "re-download from Releases", "get the LTS build from nodejs.org"); but the window only received an error string, and then guessed what to say by matching **English** keywords like `Cannot find module`, `EACCES`, `EADDRINUSE` against it. The verdicts are written in Chinese, so nothing ever matched, and all three fell through to the catch-all: "the server crashed on startup. Paste the line below into a GitHub issue." Measured on all three failure modes (source checkout missing deps, installed build missing files, Node too old) the headline came out word-for-word identical — that one sentence. Somebody who downloaded the app and can't open it was being told, as the only instruction on screen, to open an issue and wait. That is exactly the situation the people in issue #1 were in. The verdict now travels with the error: the gate attaches it as `err.bootProblem`, and the window uses its `fix` directly. Guessing from error text is left for the crashes the gate cannot know about — port already in use, permission denied. The red box now carries only the verdict's headline, not the fix as well: the fix is already spelled out in the body above, and repeating it inside the red box makes it look like part of the error you are supposed to paste somewhere. The `dialog.showErrorBox` path uses the same pair. 370 assertions pass, 0 fail, and 5 mutants are all caught: the advice going back to guessing, the gate no longer attaching the verdict, the red box printing the whole string again, the system dialog missing the wiring, and the window headline going back to guessing.
- **Sep 20** **The startup gate checked dependencies against a hand-written list of three packages** — `express`, `@anthropic-ai/sdk`, `exceljs` — while `package.json` declares 13. I measured what that list actually buys: erasing packages from module resolution one at a time and booting, **only `express` genuinely stops startup**; the other 12 are required after the server is already up. So a miss doesn't crash at the door, it waits until the user reaches for one particular feature, and by then what the error looks like is nobody's problem. Worse, a hand-written list **only ever gets staler**: add a dependency and you have to remember to come back and add it here, and forgetting is completely silent. It now reads `dependencies` from `package.json`, falling back to those three names only if that can't be read (corrupt file, stripped build) — falling back to an empty array would disable the gate outright, which is far worse than a stale list. The wording changed too: printing only the first three made "the whole install failed, all 13 are gone" look identical to "one package is genuinely missing", and those call for different actions. Over three names, it now appends the total. Verified across four cases: a real install passes (exit 0), only `qrcode` missing reads "can't find qrcode", a half install reads "and 12 in total", nothing installed reads "and 13 in total", and the packaged build is told to re-download from Releases rather than to run `npm install`. 133 assertions pass, 0 fail, 4 mutants all caught.
- **Sep 20** **Fetching a web page made the model answer a pointless multiple-choice question every single time**: the tool list carried both `fetch_url` and `render_page`, while in code they are literally the same case — `render_page` *is* `fetch_url` with `render:"force"`. Worse, the prompt had a line teaching it "if you get an empty shell, use render_page", contradicting the `render` parameter's own description: two conflicting instructions for one job. `git log -S` turned up the root cause — a comment written on 2026-09-14 already claimed "merged into fetch_url", but only the words were written; the definition was never deleted, and it sat in the list for another six days. Now it really is gone: 28 → 27 tools, tool table 18294 → 18014 characters. The merged `fetch_url`/`render_page` case in `executeTool` **stays** — live sessions, external MCP clients and historical scheduled tasks may still be holding that name, and falling through to default would get them nothing but "unknown tool". **For external CLI engines (Claude Code / Codex) the trade-off runs the other way**: they don't have this project's `fetch_url`, their own fetch tools don't run JS, and dynamic sites come back as empty shells — there is no choice to be made on their side, so dropping it is a real loss of capability. `render_page` therefore lives on in a bridge-only list (16 tools lent with a browser, including it; 14 without, excluding it). Five mutants all caught: the definition sneaking back into the list, `fetch_url`'s `render` parameter renamed, the name reappearing in the prompt, the bridge copy deleted, and the old-name alias in `executeTool` deleted.
- **Sep 20** **When the settings page asked a provider for its models and got none back, the line of text simply vanished and said nothing.** One moment it read "asking the provider which models it has…", the next it was blank — whatever the answer was, the screen never showed a word of it. Local Ollama suffers most: it answers 200 with an empty list (the service is up, you just haven't `pull`ed anything), which is not an error, so there isn't even a reason to display — and nobody tells you about the one thing you actually need to do, which is `ollama pull` in a terminal. It now says one of three things: a local service with an empty list → which command to run, roughly how big, and that reopening the dropdown after the pull will ask again; a cloud provider with an empty list → plenty of providers simply don't expose this endpoint, the curated list and "type it in" still work; **the request never going out at all** (server down, network gone) → that gets a sentence too, where before it looked exactly like "asked, and there's nothing". The empty result is also **no longer cached client-side** — cache it and you come back from a successful `pull`, reopen the dropdown, and see the same empty list: the hint told you to do something, and doing it changed nothing on screen. Both the chat-model and media-model pickers are fixed (`public/js/app-05.js`), with 5 assertions against the real source and 5 mutants all caught: blanking back to an empty string, caching the empty result again, forgetting to thread `local` through, shouting `ollama pull` at cloud providers too, and the `catch` branch going back to wiping the line.
- **Sep 20** **Connecting a local Ollama gave you no say in which model to use** — the wizard had `qwen3:14b` hard-coded, a ~9GB model a 16GB Mac can't even pull, while the machine already had others installed. The wizard was reading the one model name baked into the provider template, and **what's installed is something only your machine knows**. It now asks: picking "Ollama local" fires a `/v1/models` call and lists what is actually there. The template's default is selected **only if you really have it** — defaulting to a model you never pulled means the next step 404s, and the error reads like Ollama itself is broken. If Ollama isn't running you aren't left empty-handed: the hint spells out `ollama serve` then `ollama pull`, and leaves an "ask again" link (you go pull a model in a terminal and come back — without it the list stays cached and looks just as empty as before). Nothing you want in the list? Pick "type it in" and enter the name; an empty one won't let you continue. The model you pick is **actually the one probed**, and it is written to the config only after the probe passes — the old order mutated the config first and probed second, so a failed probe left that change sitting in memory, to be written out by the next save of anything: one failed attempt quietly breaking a setup that worked. The offline fallback also dropped from `qwen3:14b` to `qwen3:8b` (~5GB); 14B is still in the catalog for anyone who wants it. 13 front-end assertions run the real source in `public/js/app-03.js`, and the server half is verified end to end against a fake Ollama: the list comes back, a second call inside 10 minutes is cached (upstream hit exactly once), neither anonymous callers nor ordinary members can reach upstream (this endpoint goes out with the platform's key), "not running" and "malformed address" report differently, the named model really is the one pinged, a failed probe leaves both the on-disk and the in-memory row untouched, and the template path honours the choice too. Four mutants — probe after mutating, template swallowing the choice, fallback back to 14b, owner gate removed — all caught.
- **Sep 20** **macOS users on an installed build who clicked "update available" were sent back to download a fresh dmg — and hit "Apple could not verify" all over again.** A trap of our own making: the copy in their hands had already been cleared, and upgrading made them walk through System Settings a second time. The macOS update hint now leads with the zero-dialog `curl -fsSL .../install-mac.sh | bash`, and that command **does not live inside the prose** — prose is a paragraph, and a command buried in one can be neither selected nor clicked. It travels as its own field and is drawn as a monospace line with a "copy this command" link; copying says "paste it into Terminal and press Return". The command appears only when there really is a newer version (offering an upgrade command to someone already up to date just wastes their time), and Windows and from-source users never see a character of it. 6 front-end assertions run the real source in `public/js/app-06.js`, plus a server-side set: the prose must point at the command, the command must be that curl, and both Windows and source must come back empty.
- **Sep 20** **Drop a file into the composer and nothing happened for a moment** — no chip, no text, not even a spinner. The chip used to be attached only after the upload came back: a 4MB clip measured 124ms of blank, longer on a phone network, long enough that people assume the drop missed and do it again. The chip and its text anchor now appear the instant you let go, with the upload spinning in the background. Seven more things went with it: chip numbering was `count(same kind) + 1`, so adding two images, deleting the first and adding another produced a second "Image 2" — two identical labels on screen and two in what the model receives; numbers now only ever go up. Dropping the same file twice was completely silent (it re-uploaded, and nothing on screen moved); it now says the file is already in this message and refreshes the contents. Dropping a folder used to upload it as a 0-byte file, leaving junk in the working directory under a chip that claimed success; it is now refused by name with a note on what to do instead. A failed upload used to leave the anchor in the box — telling the model a file exists that it will fail to read; the anchor is now withdrawn and the chip turns red with a retry button (the file is still in your hands, no need to go back to Finder). Hitting send mid-upload used to send a message with a dead link; it is now held, with nothing cleared from the box and a word on what is being waited for. Chips showed only a filename, so a 1KB draft and a 25MB clip looked identical; they now carry the size, and delete/open are real buttons (delete used to be a `<b>` with an onclick — clickable by mouse, invisible to Tab, unreadable by a screen reader). Thumbnails used to be the whole file base64'd into `img.src`: a 300KB image measured 409,622 characters, decoded at full resolution and then drawn at 28 pixels, so a handful of phone photos cost hundreds of megabytes. Regression assertions for this section went from 21 to **49**, all running the real source out of `public/js/app-02.js`.
- **Sep 20** **Anyone downloading the dmg by hand hits "Apple could not verify…" on first launch, and that dialog offers only Done and Move to Trash** — it reads like a dead end, and it had already cost people on GitHub. First, which block is it: the build is ad-hoc signed (`TeamIdentifier=not set`), so `spctl -a -t exec` rejects it regardless of the quarantine flag — but `codesign --verify --deep --strict` exits 0, the signature is intact. That means it is the kind you clear with one click in System Settings, not the "is damaged" kind (which has no Open Anyway row at all and needs a different fix — mixing the two sends people looking for a button that isn't there, so all four documents now keep them apart). The Apple enterprise account is still in review, so until the certificate lands this step cannot be removed, and four layers now cover it. The dmg window explains itself: a new 540×420 background plus @2x, drag-across on top, and below the divider the exact route — Done → System Settings → Privacy & Security → scroll to the bottom → Open Anyway — in both languages, with a footnote that the step disappears once the certificate arrives. Verified inside the real build: `.background.tiff` carries both frames, the `.DS_Store` window is `{{400,510},{540,420}}`, icon centres land exactly on (140,178) and (400,178), zero ink pixels fall inside either icon box, and every line is centred to within 1px. Both READMEs hoist the warning above the fold and un-collapse the section (someone who can't open the app won't open a collapsed block), with the no-terminal route listed first — most people won't open a terminal to install something, so a lone `xattr` line is no help at all. `docs/安装与启动.md` and the release-notes template carry the exact dialog text, button names and verification commands, and **all 7 published releases (v0.6.2–v0.6.8) were rewritten in place** (2169→2971 characters; 1970→2792 for the earlier ones), keeping the existing double-click and boot.log sections. New release gates watch all of it, including one that fails if the dmg icons would overlap the text below the divider (negative control: scaling the icons to 200px goes red immediately).
- **Sep 20** The **"why won't this connect" line on connector cards only ever showed a slice from the middle**. A failure reason carries the process's last few lines of stderr — two or three hundred characters is normal — and it was being rendered in the row that holds tool-name pills: fixed 20px tall, no wrapping, contents centred. So one sentence came out as a middle fragment with no start, no end and not even an ellipsis. It gets its own block now: tinted red background, normal wrapping, capped at three lines, with an expand control that appears only when the text is actually clipped (asked of the browser, not guessed from a character count), and the full text in `title`. Three more went with it. The command line was clamped to three lines but its `max-height` said 3.7em against a 1.55 line-height — 2.39 lines — so **the third line was sliced through the glyphs**, which is exactly what "covered up" looks like. Clamping has its own trap: `-webkit-line-clamp` only governs the content box, so padding on the same element lets a sliver of the fourth line show underneath; the clamp now lives on an inner element with no padding. And the absolutely-positioned plugin badge in a card's top-right corner sat right on top of "Connected · N tools" — the header row now steps down when a badge is present. Status text and avatar also moved from the fill tokens to the matching text tokens (fill colours are for backgrounds and go muddy as text in dark mode). Finally, the reason itself got shorter: servers like filesystem take the directories to expose as arguments, and on a bad path they emit a string of `Warning: Cannot access directory ...` plus an English summary — the failing directories are now named directly. The layout fixes are verified by measuring boxes in a real browser (box height must be a whole multiple of the line height); both old versions were put back as negative controls and both failed. That ruler was then run over the rest of the app and caught three more boxes cutting their last line in half: the command text on a collapsed approval card (capped at 4.8em, which leaves 1.94 lines once padding and border come off), the body of a canvas audio node, and the running message in the canvas chat. The badge rule generalised from connector cards to every card that carries one — “official” on expert cards, “plugin” on skill cards, “connected” on directory cards, all of which were sitting on the subtitle. A new front-end check watches this whole family: measure the box in a real browser and call it sliced if it isn't a whole multiple of the line height (clientHeight is rounded — 57.6 comes back as 58, which flattens exactly the error being measured, so it uses getBoundingClientRect); putting the four old caps back turns all four red on the spot.
- **Sep 20** After using the real browser, **it never left**. The Chrome that `chrome_cdp` launches is detached, its pid recorded and then forgotten — this dev machine ended up with one that had been running for ten and a half hours, its GPU process pinned at 160% CPU and the load average in three digits. Three things now cover it: it closes itself after ten idle minutes (`OWB_CDP_IDLE_MS` to tune), it goes down with the process that started it, and you can send an explicit `close`. The kill takes the whole process group — killing only the parent leaves the GPU and renderer children, which are the ones actually burning CPU.
- **Sep 20** Built-in skills went from 25 to **35**: competitor watch, market research and sizing, spreadsheet data analysis, one-draft-many-platforms, customer feedback analysis, SEO content briefs, financial modelling, podcast scripts, infographics, sales outreach. Each is written against tools this project actually has.
- **Sep 20** Connector presets went from 39 to **52**: Linear, Jira / Confluence, Sentry, Vercel, Figma, Airtable, Asana, Canva, Apify, YouTube Data, AntV charts, EdgeOne one-click deploy, Bilibili. The OAuth-only ones all go through `mcp-remote`, so the first connection is a browser consent screen instead of a key hunt.
- **Sep 20** Four new recommended skills: interactive web apps, algorithmic art, internal comms, Claude API integration (all Apache-2.0).
- **Sep 20** `gen_diagram` **often delivered the SVG but no PNG** (Feishu docs and Word only accept PNG, so the picture simply didn't go in). Outside the desktop app the conversion shells out to headless Chrome — and the screenshot lands on disk in about two seconds, but the Chrome process can then hang around for another minute without exiting (especially on a Mac with a display attached). The old code waited for the exit, hit the 30-second timeout, killed it — and then threw away the perfectly good PNG sitting on disk, because the process had been killed. It now watches the file instead of the process, reaps Chrome as soon as the image is written, and is done in two seconds. It also gets its own throwaway browser profile: without one it opens the Chrome profile you have running, fighting for the lock and touching your live browser.
- **Sep 20** On macOS the **downloaded app wouldn't open on a double-click**, and at least one person was stuck on 0.6.2 because of it. Nothing is wrong with the build: this project has no $99/year Apple developer certificate, so releases are ad-hoc signed, and anything a browser downloads gets tagged `com.apple.quarantine`. Since macOS 15 the dialog for such an app offers only "Move to Trash" and "Done" — **there is no "Open"** — and the old right-click-to-open trick no longer works either, so most people conclude the software is broken. There's now `curl -fsSL .../install-mac.sh | bash`: a file fetched with curl never carries that tag, so the whole path is dialog-free — detect the chip and pick arm64/x64, download the zip, unpack, move it into Applications, strip quarantine anyway with `xattr -dr`, and re-sign in place if the signature is broken (copying, sync folders and unzip tools all break it, and on Apple Silicon a broken signature reports as "damaged", which tells you nothing about the real cause). It also closes a mutual-clobber: the dev shell from `scripts/make-mac-app.sh` always lands in `~/Applications`, and the installer falls back to that same directory when `/Applications` isn't writable — same name, same place. Overwriting blindly replaces a working install with a shell pointed at a local checkout: your files are still sitting safely in `~/OpenWorkBuddy`, but the app shows zero tasks, which reads as "my data is gone". Each script now recognises the other and stops with instructions instead.
- **Sep 20** The infinite canvas can now **select a group at once**: Shift- or ⌘-click nodes to add and remove, a new marquee toggle in the toolbar lets you drag a box over everything you want, ⌘A selects all, Delete removes them together, Escape backs out. Marquee used to be reachable only by Shift-dragging, but dragging on empty space pans by default — you try it once, conclude it isn't supported, and never try again, so it got a visible switch. The real defect was behind all of that: those keys are bound to the canvas page, but after any mouse click focus stayed on `<body>` — an *ancestor* of the canvas — so keystrokes never passed through it. You could draw the box, you could see the highlight, and then **Delete did nothing, ⌘A did nothing, Escape wouldn't leave marquee mode**, which looks exactly like multi-select was never built. Clicking anywhere on the canvas now takes focus, while clicks on inputs and buttons leave it alone. Two smaller changes: a marquee now selects anything it *touches* rather than only fully-enclosed nodes (dragging a box over half the canvas and selecting nothing is not something anyone will diagnose as a missing half-edge), and a Shift-click on empty space with no drag means "clear the selection". 25 assertions pin it down, including "Delete only deletes characters while you're typing in a field".
- **Sep 20** The drama progress bar **covered the canvas once expanded**. Viewport height was hard-coded as `calc(100vh - 278px)`, and that 278 was measured before the progress bar existed; once it appears — and expanding it grows a blocker list and a shot table on top of that — it pushes the canvas and the chat column below the fold, while the toolbar floating over the canvas at `top: 42px` was measured in that same era and stayed put, landing right on the bar's face so the script and character-shot buttons were permanently hidden. The bar's real height is now measured into `--cp-h` and both the viewport and the toolbar give way; the bar itself is capped (40vh, scrolls past that), because thirty shots make a table taller than the screen.
- **Sep 20** "**N characters have a look-book shot on the canvas that no longer exists on disk**" was often **a false alarm**. Whether a file exists was decided from one workspace listing, and that listing is truncated — 3 levels deep, 500 entries max. Let the workspace fill up, or put assets one level deeper, and every node on the canvas raises that blocker while the files are sitting there perfectly fine. It's two steps now: in the listing means present; not in the listing means ask the disk, one path at a time (capped at 400 per board), because **only the disk gets to pronounce a file dead**. A path that won't resolve counts as "can't say" — better silent than wrongly accusing a file that's still there. One duplicate on the same bar is gone too: the "what's next" line shown when collapsed is the same sentence as the first blocker shown when expanded, and having both up at once reads like two separate failures, so the expanded view keeps only one.
- **Sep 20** "Tidy up · free disk space" is **no longer offered on the empty state**. The first thing someone sees before they've done anything shouldn't be a button urging them to delete things — that's worrying on their behalf about a problem they don't have yet. The feature itself is unchanged; you're still asked about intermediate files when a task finishes.
- **Sep 19** Eval runs **no longer touch your real memory, preferences or quota ledger**, and the task set grew three shapes a single-turn task can't test. The harmful part first: eval used to share one data directory with normal use, so an agent that called `remember` mid-task wrote "the user prefers Firefox" straight into your long-term memory — and the next run was then influenced by what the last one wrote, which makes scores irreproducible and leaves you with memories from nowhere. The data directory is now pointed at `eval/runs/<timestamp>/data/` before anything loads, so a run's entire footprint stays in that one folder. The task set went 15 → 18: a **long-haul task** (12 order files, the spec hidden in one of them, 40+ steps) asks whether a rule read at step 3 still holds at step 30 — the spec has to live in a *file*, because the prompt sits at the head of the history and survives trimming while the tool output in the middle is what gets dropped; a **multi-turn task** runs three turns with the format rule stated once, in turn one, and never repeated; a **memory-recall task** seeds the rules into long-term memory and mentions none of them in the prompt, plus two deliberately irrelevant seeds, to see whether it forces unrelated memories into the deliverable. Alongside it, a **free offline gate** (`node test/eval.js`, 100 assertions, zero model calls): feed every task's checks an empty directory and require that **not one of them goes green** — an agent that did nothing scores zero, so any check still passing there isn't grading, it's giving marks away. The gate caught two such freebies in existing tasks on day one: a pure negative assertion that passed when the model crashed without output, and a "local copies cleaned up" check that passed when the model simply deleted both source files. Finally, "changed a prompt or a tool description? run the eval" is now written into CONTRIBUTING, with the triggers spelled out (the agent.js system prompt, tool `description` text, skill prompts, trimming and memory-injection logic).
- **Sep 19** When two tasks change the same repo, **the later one gets its own git worktree** (`worktree.js`). The old isolation was one output folder per conversation — fine for document work, useless the moment the working directory is your own code repo: both tasks read the same `src/`, A changes a function to three arguments, B is still holding the two-argument text, and B's write erases A's change. Nothing errors, the tests may even stay green; what's broken is the version in between. The approach is Codex's: same `.git`, separate working copy, changes land on `owb/<session>`. Three deliberate limits: **only a collision triggers it** (one person with one task is the overwhelming majority, and dropping someone into an unfamiliar directory for no reason just means they can't find their files); **the first task is left alone** (it edits the tree in front of you, which is what you expect — the later one is the extra); and it **never merges back**. A few more things that would make the feature a lie if skipped: your uncommitted changes are **carried over** (not the `.gitignore`d ones, or an 800MB `build/` would ride along), because without them the second task sees a repo rolled back to the last commit and redoes work you already did; the work is **auto-committed** to that branch at the end, since uncommitted changes can't be merged and you'd follow the hint, run `git merge`, and find the branch identical to your own; a run that produced nothing is **removed on the spot**, directory and branch, or every collision would leave behind a branch nobody dares delete; and anything with work in it is **never deleted** — after 14 days only the directory goes, the branch stays. The collision list includes terminal runs (one web task plus one terminal task is the common case, and they're separate processes that see each other only through the `data/cli-live` ledger). The prompt states outright that the agent must not merge, rebase or push. Every failure mode (no git, no first commit yet, bare repo, patch won't apply…) falls back to the old behaviour in the original working directory with one log line, so a task never fails to start because of this. `openworkbuddy worktree` lists them; `openworkbuddy worktree 清理` sweeps the empty ones
- **Sep 19** A **super-admin** tier, and permissions rebuilt as RBAC. **any admin could demote, disable, delete or reset the password of any other admin**, and mint new admins at will. Two people disagree and whoever clicks first wins; that isn't a permission model, it's first-mover advantage. Worse in branch orgs: the protected `owner` flag was only ever set for the **first account on the whole install**, so orgs created later had nobody protected at all. There are now four tiers — super admin / admin / auditor (the tier already existed, it just wasn't ranked: can open the console and see the whole org's usage and billing, changes nothing) / member — and only two rules: **you can only manage a strictly lower tier** (equals can't touch each other), and **granting a role requires outranking both the person and the role** (so an admin can promote someone to auditor but can never produce a second admin). There are four routes to minting an admin — change role, create account, issue an invite code, department template — and all four now go through the same check; leaving one open makes the rule one step away from meaningless. Each org has exactly one super admin and the seat can only be **transferred, never issued**: two super admins would reopen rule 1 at the top tier — either they can depose each other (first-mover advantage is back) or neither can (one mis-click is permanent). Two escape hatches so nobody gets locked out: the platform super admin (the default org's) can reassign across orgs, and if they're the one who's gone, `openworkbuddy owner <username>` on the server itself — same reasoning as `passwd`: anyone who can read `users.json` already owns the machine. **Neither is ever exposed over HTTP.** Upgrades migrate once: the legacy `owner` boolean becomes a role, and orgs that never had one get the earliest active admin. The whole thing lives in one `rbac.js` you can read in a single screen — nobody trusts a permission model they can't finish reading. The role dropdowns in the console are drawn from what you can actually grant — an admin no longer sees "admin" in the list, because a visible button shouldn't be one that only tells you no after you press it; and when editing someone, their current tier is shown first even if you can't change it, so the dropdown never defaults to "member" and makes a demotion look like the status quo. 103 new assertions, each "must refuse" paired with a "must allow".
- **Sep 19** Storyboard shots can now be **rolled back to an earlier version**. Change one line of dialogue, re-run, and the previous first frame was overwritten by a file of the same name — gone. Recording the path doesn't help: the path is unchanged, it's the bytes underneath that were replaced. So snapshots store **the bytes**: first frame, last frame, video and voice go into a sha256-addressed object store, identical content stored once (re-run ten times, roll back ten times, and most of the intermediate states are duplicates). "Versions" on any shot lists what it has looked like, thumbnails read straight from the snapshots, and restoring swaps **the whole shot** back to that version rather than merging — merging leaves you last version's video against this version's first frame, which looks exactly like success on screen. The current version is snapshotted before a restore, so a wrong rollback is itself reversible. A version that has been swept is genuinely gone: the button is simply disabled and says which kind of gone, rather than being a button that does nothing. The ledger lives in the workspace where the model can write to it, so no path from it is ever trusted — everything resolves back under the root and is re-checked; anything outside is refused.
- **Sep 19** A channel in arrears is **cut off after one hit**, not retried daily. Alibaba's Model Studio reports arrears as **HTTP 400** with no "balance", "quota" or "overdue" anywhere in the body — just `Access denied, please make sure your account is in good standing` and `code: Arrearage`. 400 isn't in the hard-error set and the breaker's wording list caught none of those, so arrears read as "unlucky this time": the next call went out, waited out another timeout, and did the same every day until someone topped up. The factory **default video model** hangs off that channel, so video generation was walking into a wall that was never going to move. `Arrearage` and that `in good standing` sentence are now recognised, one hit trips it, and the message says the channel is out of credit and points at Settings → Models instead of "retrying in a few minutes" — no amount of waiting fixes a wrong direction. Two counter-checks hold the line: a 400 about a bad parameter, and a 429 rate limit, must never trip it (rate limits pass, and tripping one closes a channel that still works).
- **Sep 19** Models **resold on Volcengine Ark are no longer treated as sitting on the wrong channel**. Ark stamps every model it lists with a `-YYMMDD` date suffix, and it resells other vendors' models — `deepseek-v3-250324` and `kimi-k2-250711` are both filed under Ark in the curated catalog. The old rule read only the brand prefix, so `glm-5-3-flash-260828`, filed by the user on their Ark channel, was judged to be Zhipu's and moved to a Zhipu channel on the next save — while that model answers fine on Ark (verified, HTTP 200) and is guaranteed to 404 anywhere else. This kind of failure looks exactly like success: the settings page still shows the name they typed, so it reads as a broken model. The date suffix now outranks the prefix, and it has to be a real date — month 99, day 0 and four digits don't count, and neither Anthropic's eight-digit `claude-sonnet-4-20250514` nor OpenAI's `gpt-4o-2024-08-06` may be swallowed by it. 46 assertions cover this, each "should be flagged" paired with a "must be left alone".
- **Sep 19** A stuck task **stops itself** now instead of spinning until morning. There used to be two tiers, both of them nagging: three identical results in a row pushed a "try another route" note into the history, five in a row blocked one call. If the model ignored both, it spun on to the step or runtime cap — and what the user saw was "maximum runtime reached, wrapping up", which reads like the job was too big, so they raised the cap and the next run burned longer. The third tier is a **hard stop**, and it knows five shapes of stuck: the same call returning the same result 6 times, the same error text word for word 6 times (changing arguments doesn't help, so the arguments were never the variable), 12 consecutive failures, 4 calls into a channel that is already tripped, and `A→B→A→B` — a two- or three-step loop repeated 4 times with identical arguments and results, the one shape where no single tool looks repetitive and only the sequence gives it away. **This kind of stop never auto-continues** (the first thing a continuation would do is run the same loop again), and the closing line no longer suggests raising the cap — a bigger cap just buys more laps. It names the road to fix instead: the channel, the file or the command. Each of the five tiers ships with a **one-short-of-the-line counter-check** among 36 assertions: loosen a threshold by one and a task that is actually working gets cut off mid-stride, and a false stop hurts more than a late one.
- **Sep 19** Three things in the library got fixed together. **Audio and video play when you click them**: a 1006 KB mp3 came back as "too large to preview, download it and open it locally", because it went down the read-as-text path with its 512 KB ceiling. The real cause was on the server: the file endpoint **always sent an attachment**, `Content-Disposition` and all, so `<audio>`, `<video>` and `<iframe>` rendered nothing. It now serves inline with the right MIME type, and only `?dl=1` adds the attachment header. **Binary files are no longer pulled down whole**: `.psd`, `.heic`, `.sqlite`, `.zip` get a single HEAD to check they still exist and a plain "binary file, download it to open it"; extensionless files are judged by their first bytes. **"From which task" no longer names the wrong one**: `任务_0915_对话_2/BGM_纯配乐.mp3` was credited to a task from two days later, because the old rule picked the first task that listed the file and that later task had indeed touched it. It now goes by the task folder the file actually sits in, falls back to the scan only when that fails, and returns nothing rather than guessing when neither works. **Two new buttons, "Reveal" and "Copy file"**: the first selects the file in Finder, the second puts the file itself on the clipboard, so pasting into WeChat or PowerPoint gives you the file and not a local path. Both are drawn only for the person on the machine.
- **Sep 19** "Open this page in a real browser and look at it" **works now**. Only one action used to work (list the tabs); navigate, screenshot and run-JS all failed, and you had to start a Chrome with `--remote-debugging-port` yourself first. Four separate bugs were stacked on top of each other: **port 9222 was held by a Chrome that serves no debugging endpoint** (a listening port is not an open debugger, and the connection was refused on the spot); the handshake carried an `Origin` header, which **Chrome has rejected since 111** with a 403; **the successful handshake was read as a failure**, because the check matched the reason phrase "Switching Protocols" while Chrome 153 answers "WebSocket Protocol Handshake" with the same 101 status; and `evaluate` **unwrapped one level too many**, so every call came back `undefined`. It now starts its own Chrome: separate user-data directory, port picked by the OS (`--remote-debugging-port=0`, then read `DevToolsActivePort`), so it never touches the window and the logins you have open; if a live one is already there it adopts it instead of starting another. WebGL really renders in headless mode too (`--use-angle=swiftshader` — the old `--disable-gpu` was exactly what killed it). When it can't connect, run `action: "status"` first: it probes without touching anything and tells you whether Chrome isn't installed, the port belongs to another process, or `OWB_CDP_NO_LAUNCH` is holding it back. 20 new assertions; the fake Chrome in the tests answers 403 to any handshake carrying an `Origin`, so that regression can't come back.
- **Sep 19** When it asks you a question, **the file the question names opens on the right by itself**. Three cover drafts sat in one HTML, the question even gave the filename, and you still had to dig it out of the files panel. Filenames in the question and in the option notes are both picked up (in guillemets or with a path), and **only files this run actually wrote count**, so it can never pop a "file not found"; the files are also buttons under the question, one click away at any time. Nothing changed for outputs that arrive at the end of a run, which still never steal the layout; only a question you cannot answer without looking touches the right-hand pane. Not during replay, not on someone else's session, not on a narrow window.
- **Sep 19** The files panel got a **search box**. Grouping solves browsing, not "I want that one": a task leaves dozens of files, and you had to expand the right folder and scan line by line. It matches the whole relative path, so folder names are clues too (typing "0918" pulls up everything from that day's task folder); words separated by spaces must all match ("cover html" means the name has "cover" in it and it is an HTML file); results are flat rather than grouped, each row labelled with the folder it lives in (one task can hold several files called `index.html`). Under 8 files the box is not shown, because when you can take it all in at a glance it is just in the way.
- **Sep 19** WeChat article covers are **actually generated images** now, not a screenshot of an HTML layout. The root cause was in the skill: `wechat-article` only specified sizes (900x383, 1:1) and said nothing about what the picture should look like, so every run fell back to typesetting one. There is a dedicated cover step now, with the prompt built from four lines (subject, style, light and colour, composition) and three hard rules: **do not let the model write the title into the image** (Chinese glyphs always come out mangled), deliver both sizes, and generate three directions into one comparison page to pick from, naming that comparison file in the question so the web UI opens it by itself.
- **Sep 19** File edits **can be rolled back now**: before `write_file` / `edit_file` touch disk they keep the previous version (content-addressed under `.history/` in the workspace — identical content is stored once). Open a step card and you get that step's diff (green adds, red deletes) plus a "rewind to before this step" button — it rolls back that step and everything touched after it: new files removed, edited ones restored. The rewind itself is checkpointed, so a wrong rewind is one "undo rewind" away. On the command line it's `/rewind` (no number lists the steps). **In "ask every step" mode the approval bar shows those diff lines** — you approve the change itself, not a filename — the same on terminal, web and phone. A rewind only honours this session's checkpoints, a hand-edited ledger with `../../` in it can't write outside the workspace, and checkpoints older than 30 days are pruned.
- **Sep 19** Fixed a red that was **blocking releases entirely**: v0.5.1 was tagged and produced no installers at all. What failed on CI was the front-end check that jumping back to a turn really draws the highlight — the runner measured `animation-name: "none"`. Not a code bug: GitHub's macOS runners **have "Reduce motion" on by default**, and under that setting the highlight animation is supposed to be none. Same files, same Chromium, green on the dev machine and red on the runner — it looked like CI itself was broken. Every test window now states which setting it wants (no-preference by default; the two screens that test reduced motion ask for reduce), so results no longer drift with the accessibility settings of whatever machine runs them, and each pass gained a fixture self-check so that removing this layer reports "this pass is not the reduced-motion one" instead of a baffling `"none"`. The runner's state was reproduced locally with `--force-prefers-reduced-motion`: red before the change, all 54 screens green after.
- **Sep 19** The line you get when a task is cut short now **says what to do next**. The old wording was a single clause — "raise the limit in settings to continue" — which never said which page, never said the default is 30 minutes, and never mentioned auto-continue rounds (0 by default, so nothing picks itself back up after hitting the wall). It now names Settings → Execution limits, names the auto-continue setting, and gives the location of the progress file (`PROGRESS.md` in the working directory), so nobody has to come back and ask. **A manual stop is no longer treated as a limit**: telling someone who pressed stop to raise their limits answers a question they did not ask, so that one only says how to pick the work back up — and the process note in the web UI and the `▲ …, forced to wrap up` line in the CLI no longer trail "stopped by hand" either. The wording also went from two copies to one: the built-in loop and the native CLI engine each assembled their own, so editing one and not the other meant two different sentences on the web and in IM; both call `stopNotice()` now, and a third hand-written copy turns the test red.
- **Sep 19** Every run of a scheduled task now leaves **a full conversation you can open and replay**. Each task in Automation has a "run history", and each run has a "see what it did" button — click it and you get the tools it called, what each step returned and why it ended where it did, structurally identical to a hand-typed conversation and opened through the same `openSession`, not a second replay path. It used to be a bare `runTask({ history })`: no emit meant no events, no session id meant no session, and an entire run left one result string **truncated at 500 characters** — nothing to look at when you wanted to know where it stalled. Three details follow from the fact that these run with nobody watching: **scheduled sessions stay out of the sidebar** (a few dozen cron runs a day would push the real conversations off the list; their home is the run history), **the desktop pet doesn't react to cron** (a pet dancing for a background task covers whatever you were looking at), and **deleting a run deletes its session** (runs cap at 300 and go away with the task, but the session files on disk were never touched again — growing forever, all of them orphans with no way in). Recording must never affect execution: if the recorder fails to start the task runs anyway, and the session id is written to disk before the run begins, so a crash halfway still leaves that half findable.
- **Sep 19** Scheduled tasks can now be **one-shot**. When the user said "remind me in five minutes to prep for the interview", what got scheduled was a cron for **14:00 every day**. The cause wasn't the model: cron's five fields have no concept of "once", and with only that one tool the model can only approximate "at this time" with "at this time every day", which turns a reminder into an alarm that fires forever. Three spellings are accepted now: `+5m` / `+2h` / `+1d` (**this is the one that matters** — the current time in the system prompt is only accurate to "around this hour", so the model can't compute the absolute instant of "five minutes from now"; forcing it to try just gets it wrong, better to have it copy the amount the user said), `14:05` (that clock time today, rolling to tomorrow if it's past), and `2026-09-19 14:05` or full ISO. **Anything else throws rather than guessing an instant**: the schedule is shown to the user for approval, and a wrong guess is something they click "agree" on. After it fires the state is "done", not "paused" (paused implies "it will run again when you re-enable it"), and the record stays — you still want to check whether that reminder actually fired. A missed one from a shut-down machine is still caught up, which is exactly when a one-shot reminder should be (you closed the laptop for a meeting and should see it fired when you're back), but "remind me in five minutes" firing a day later is just noise, so past the catch-up window it only records "missed, N minutes late". Disabling itself happens **synchronously** before the run is released: fire is async, and disabling after the run finishes means every tick in between sees it still enabled with a due time in the past — a five-minute reminder would fire over and over until you turned it off by hand.
- **Sep 19** In the library, **clicking a file actually shows it** now. The content really was rendering into the right pane — the pane was just still `display:none` (it only appears on demand, `data-prev=off` when nothing is selected), so nothing happened on screen. Why nobody caught it: touching any filter re-renders the whole page, and that path recomputes the attribute from the selection, so it worked again — it looked like a flake when in fact **the first click after entering the page always failed**. Preview formats were filled in on the way: Word / Excel / PPT / CSV / TSV / zip now open here too, drawn with the same code and styles as the conversation view rather than a second copy; sheet tabs in an `.xlsx` are clickable (a spreadsheet is usually several sheets, and without tabs you only ever see the first); CSV honours RFC4180 quoting and detects whether the separator is a comma, a semicolon or a tab (European exports use semicolons, and splitting those on commas gives you one tall column). The three Office 97-2003 formats are OLE binaries rather than zip archives, and when it can't unpack one it says it's the format and gives a way through (save as the modern format) instead of letting it fall into "this file isn't text".
- **Sep 19** The **+ next to the composer is now one entrance to the six things you actually do**: add files, switch mode, pick an expert, pick a skill, see **which tools the model has in hand right now**, and turn configured connectors on and off. Before this, those six lived in six places: upload was a paperclip, mode was at the far right of the toolbar, experts and skills meant leaving for another page (and the conversation had scrolled by the time you came back), and "which tools are there" **wasn't in the UI at all** — you had to ask and wait for it to answer "I have no messaging channel configured". The first level is six lines of text; the second flies out to the right as a 300px panel, so the first level never gets stretched by the second and the width doesn't jump as you move between them. Connectors can be **switched off individually**: off ≠ deleted — config, keys and arguments all stay, it just isn't connected this round and isn't in the tool list. Leaving the definition in is what costs you: the model plans around it, calls it once, eats a "can't connect", and re-plans — a wasted round. A connector you switched off reads differently from one that failed (without that one field, switching one off gives you a grey card identical to a connection failure), and "authorization is gone" reads differently from "can't connect" (the first needs a new key from you now, the second usually clears up on its own). Connectors are one set per machine, so the switch belongs to the platform owner — the same test as the permission tier.
- **Sep 19** **Models that can't be reached are no longer hit over and over.** . All five media paths (look / image / video / voice / transcribe) now go through one dispatch layer: 401 / 402 / 403 / 404 — the errors that will never clear on their own — pause that channel for 30 minutes on the first hit; timeouts and connection failures pause it for 5 minutes after 3; 400 / 422 are argument problems and aren't charged to the channel. Why the dispatch layer rather than the top of each of the five functions: those functions contain thirty-odd separate `return {isError:true}` sites, and bookkeeping added one at a time will eventually miss one — and the one it misses is the one being hit hardest. The fingerprint is "path + base url + model + first 8 of the key hash", so swapping a key or a model works immediately without waiting out the cooldown. That gate alone isn't enough: the model still goes round its loop, and what the user sees is forty identical "look at image · failed" lines in the trace — so the **second** time a tool is stopped by the breaker, it isn't run again this round (the first time, the model hadn't read the message yet and is entitled to try once more, e.g. naming a different channel with the model argument; hitting it again after reading it isn't a judgment call). The breaker can work silently in the background, but "why did it suddenly stop looking at images" has to be visible somewhere: a yellow bar at the top of Settings → Models spells out which path, how long is left and why. **Touching any channel config clears the breaker immediately** — otherwise you fix it and still wait half an hour, then come asking "I fixed it, why is it still dead".
- **Sep 19** The vision-model dropdown now **groups by the modalities the provider reports**, not by guessing from names. Measured against OpenRouter's 446 models that day: 263 really accept images, and names alone find 134 of them — the user's own `z-ai/glm-5.3-flash` has no `vl` or `vision` in its name and got dumped into "other models", while `gpt-5-image` and `gemini-3-pro-image`, which **produce** images, have `image` in the name and sat in the vision list waiting to be picked. Pick a generator for a vision job and upstream either returns an argument error nobody can read or actually draws a picture and then times out — on screen it's just "stuck". Happily, more and more providers report `architecture.input_modalities` / `output_modalities` at the `/models` layer (OpenRouter, new-api and one-api all pass it through). That's the provider's own word, which beats guessing and beats a model table we could never keep current: if it's reported we trust it and mark it "sure"; if not we fall back to the name and mark it "unsure". The UI uses that flag to decide **whether it dares keep a model out of the vision group** — a guess doesn't get to exclude anything, the provider's own word does. Text-in-text-out is a **definite** "none of these paths", and saying so is the only thing that keeps `gpt-5.x-codex` out of the vision group; anything that both accepts and produces images is filed under what it produces (putting `gemini-3-pro-image` on vision duty is pure waste and latency — drawing is its day job).
- **Sep 19** Providers with the same name **number themselves**. Two entries for one vendor is normal (your key and the company's, or a primary and a backup), but two identical lines in a dropdown mean picking by guess — and editing the wrong one while believing you edited the other. Duplicates now carry ①②③ in config order, and only duplicates do: a lone entry shouldn't sprout a symbol for no reason.
- **Sep 19** The **"add a model" form is no longer wiped by an async repaint**. The curated catalog is fetched live, and its arrival repaints the pane: you hit "add a vision model", you're scrolling the model list, the catalog lands — the whole card repaints, the form goes with it along with the row you had just picked, the button still looks fine and clicking it gives you an empty form again. The pane now doesn't repaint while a form is open: the repaint exists to fill in the catalog, which isn't worth what someone half-typed. The same complaint had a second half: on a failed save the message **wore success's clothes** — green, 13px, at the very bottom of a full screen of forms with five collapsed cards above it, so what the user saw was a small green line indistinguishable from "saved". It had been reporting the error every time; the error was just off-screen in a success costume. A failure now also speaks up separately rather than trusting that line.
- **Sep 19** The **desktop pet you picked survives a restart**. The config handed over at startup was five fields copied by hand, and **`sprite` was the one missing**: anyone on a sprite pet got `character: "sprite"` with an empty `sprite`, the lookup by empty id found nothing, and the last-resort line quietly fell back to `"cat"`. The same hand-copied list also dropped "notify when done" and "wander", and the scale fallback disagreed with the pet's own default (2 there, hard-coded 1 here) — three instances of one disease: a hand-copied field list that nobody remembers to update when a field is added. The whole object is passed through now, with only "enabled" pinned to explicitly-true (never configured means no pet), everything else left to the pet's own defaults. Adding a field no longer means touching this file.
- **Sep 19** **An image pasted into the composer is no longer counted as the previous round's output.** The root cause is that upload drops the file straight into this session's output folder — which is right: material and output belong together, or the workspace root piles up (22 of them in real data). But "this round's output" is decided by "in my folder + modified after the run started", and this image satisfied both: it really is a new file that just appeared, the writer just happened to be the user rather than the agent. **Who wrote it is knowable only at the moment it lands on disk** — you cannot tell from the file afterwards — so the note has to be taken then. It records "name + the mtime at that moment", not just the name: if the agent later actually rewrites that image (cut out, compressed, re-encoded) the changed mtime stops it counting as input — at that point it really has become output and belongs on the card.
- **Sep 19** The security tier **now reaches the command line of the local CLIs**. Both of those CLIs bring their own tools and their own loop, and their file writes and commands **do not pass through** this project's security center (only the ones that come back over the MCP bridge do). The claude path used to hard-code `acceptEdits` and the codex path `workspace-write`, so a user who set the tier to "look, don't touch" and switched to a local engine still had their files edited freely — the toggle in the UI was decoration, and it's the last one that should be. The four tiers now translate all the way down: look-don't-touch → claude `plan` / codex `read-only`, ask-me → `default` / `read-only`, automatic → `acceptEdits` / `workspace-write`, unrestricted → `bypassPermissions` / `workspace-write`. The "ask me about commands like these" list **cannot be asked about** on this path (`-p` is non-interactive; there is no approval channel), so all that's left is refusal: each entry is reduced to its first word and becomes `--disallowed-tools Bash(sudo:*)` (the list holds prefixes like `diskutil erase`, while the CLI matches on the executable name), and delete-protection, which is a separate toggle, adds `Bash(rm:*)`. Look-don't-touch additionally **withholds the borrowed command-line entry from the allowlist** — that's a back door the model can write files through. A hand-written `engine_options` still wins, and with no guard passed the old behaviour is byte-for-byte unchanged. When a tier tightens things, the run page says so out loud: without that, what the user sees is "why won't it do anything", and the actual reason is a toggle on another page, two steps away from anything they can connect it to.
- **Sep 19** In the `@`-a-file menu, **you can finally tell which row is which file** across twelve of them. The name used to be the full relative path, but a task folder name is twenty-odd characters on its own, so all twelve rows read "task_0918_make me an 『openwor…"; meanwhile the description on the right was identical on all twelve ("workspace file"), wasting a line. The name is now just the last segment, the directory moves to the description line, and what gets inserted into your message is still the full relative path. The name needs its own element to get an ellipsis (a bare text node is an anonymous box in a grid and long names can only hard-wrap), and every row gets an icon (a generic one if nothing matches — otherwise some rows have icons and some don't, and the left edge of the names zig-zags).
- **Sep 18** Clicking a file in the library now **lands on the exact exchange that produced it**, instead of reopening the whole conversation. Each file row grew a small speech-bubble button (it lights up when you hover the row), and the "from task" line is tagged with the round number. It used to just open the session and scroll to the bottom — on a task that ran a dozen rounds you still had to scroll back yourself, so the jump helped with nothing. The round number is counted from the **user entries in the transcript**, not from the array index: replay starts one round per user entry and the array has assistant entries interleaved, so an index is off by nearly 2× and would drop you on an exchange that has nothing to do with the file you clicked. A file edited across several rounds anchors to the **first** one — you clicked it to see where it came from, and that sentence is in the round that first wrote it. **When it can't jump, it says so**: old sessions never recorded rounds and transcripts can be trimmed, so the server invents nothing, the UI simply doesn't draw the button, and the old scroll-to-bottom behaviour stands. The landing round highlights briefly (with "reduce motion" on there's no fade — the tint is just there) — without a marker you only know "the page moved", not where to look.
- **Sep 18** **You can quote one reply and ask about it.** The action bar under a reply grew a "quote" button: it drops that reply's text into the composer as a `>` blockquote and you write underneath it. **Select part of it first and only that part is quoted** — a reply is often several screens, and quoting the whole thing points at nothing. Why the composer rather than an attached chip: a quote **has to be editable**. The real use is almost always "one sentence in here is wrong" — you keep that sentence and delete the rest. It takes the rendered body only: no progress cards, no button bar, no token counts. A half-written message isn't clobbered (the quote is appended after a blank line); pressing quote twice on the same passage doesn't insert it twice (it says it's already there); anything over 400 characters is truncated with an ellipsis — past that it's restating, not quoting, and the bulk drags the model off course.
- **Sep 18** Fixed three icons that were **rendering as empty boxes**: "last run" in the automation list, "skill and connector scan" in security settings, and "last generated" on the canvas. `<use href="#i-a-typo">` throws no error and logs no warning — the browser quietly draws a blank the same size as a real icon, and only someone staring at that one cell notices. Added a gate on the way out: all 379 hard-coded icon names in the UI are now checked against the sprite (each page has its own, checked separately), and the 78 built from variables are reported as a count rather than pretended to be covered.
- **Sep 18** Added the **Jev decision model** (TypeSafe System One). It doesn't write prose — it returns a chosen option, a score, or a probability, plus how sure it is. It is deliberately *not* in the model dropdown: a chat model takes prose and returns prose, while this one takes a state plus a **typed** question, so putting it there would 400 on every call. It gets its own path instead: `openworkbuddy jev` on the CLI, `/api/decide` over HTTP, and the "test it" button on a channel in settings (which knows better than to ping a decisions model at the chat endpoint). **If you already use OpenRouter, nothing to configure** — the key you have works. The first real use inside the product is **Goal-mode verification**: it used to ask a chat model to emit `{"results":[…]}` against the acceptance criteria, which is asking something that writes prose to imitate a table — it occasionally answers off-format and a whole round of checkboxes stays frozen. Now each criterion is one yes/no question whose shape cannot drift. More importantly there is now a confidence: **a box is ticked only above 70%, and anything below stays unticked with the card saying "not sure" rather than "not done"** — the way this card misleads people is that once a box is ticked, nobody looks again. On a machine without this model configured it falls back to the old path silently, byte for byte. Quota is metered **per question**, not per request (one request carries up to 32 questions, so a per-request gate would gate nothing); output tokens are free and a three-question round costs about $0.00002.
- **Sep 18** The sidebar's **task history now searches the conversation itself**, and by meaning. The magnifier used to filter titles only — but titles are auto-written when a task finishes, and nobody ever reads them. What people remember is the sentence they typed ("pull the duplicate rows out of this csv") or the file they got back ("cleaned.xlsx"); a title filter finds neither, so you open sessions one by one and give up around the fifth. It now matches three ways and **labels every hit with which way found it**: title / produced file / in the conversation / close wording / similar meaning. Without the label, the semantic hits look like unrelated tasks appearing out of nowhere and people conclude the search is broken. Typing filters titles locally at once rather than waiting on the network (waiting leaves the list empty, and an empty list looks exactly like "no matches"); the server pass lands 300ms later and takes over. If that request fails, it falls back to the local title filter **and says it failed** instead of passing it off as "nothing found" — those are two different things. With no embedding channel configured, the result says plainly that the "similar meaning" pass did not run; no silent downgrade. The CLI's `/resume` gets the same: `/resume duplicate rows` reattaches a session whose title contains none of those words, and a row matched only through the body is tagged "in the conversation". Search still goes through the same ownership filter — **a session you cannot see in the sidebar cannot be found by searching either; search is not a back door around ownership**.
- **Sep 18** The picker grew a **search box**, and it is always there. It used to echo the query after the title once you had typed something — so nobody who had not already typed knew it could search at all; "type to search" lived in the footer, in the middle of a row of key hints, where a line of prose goes unread. The box now sits at the top from the first frame, saying what it filters on while empty (`/resume`: titles, session ids, origin; `/model`: model name and vendor), echoing what you type with the caret following along. **A search that matches nothing keeps the words you typed** — clear the box and there is nothing left to tell you what to backspace. The footer dropped "type to search" on the way: the box is right there, and saying it twice only makes the key hints longer.
- **Sep 18** Picking up an earlier conversation in the terminal went from counting numbers to choosing. Bare `/resume` opens a picker: ↑↓ to move, type to search live (titles, session ids and origin are all searchable), Enter to attach, Esc to forget it — sessions started in the desktop app are in the same list, each row carrying where it came from, how many turns it ran and how long ago. It used to print a numbered table to type a number into, and a number has to be counted by eye: one row off and you attach **a different conversation**, which you only discover when the next answer makes no sense. `/model` uses the same picker, so the two commands share one interaction rather than growing two. Longer lists scroll with the selection always kept on screen. **In a pipe, or on a terminal that cannot paint an overlay, both fall back to the old numbered table** — sharing the one "this terminal can't do it" switch with the `/` menu, because half the interaction beats a scrambled terminal.
- **Sep 18** Three long-missing commands. `/compact` folds the earlier turns into a summary by hand — **the manual one ignores both the threshold and the "auto-compact off" setting**, since that setting means "don't do it behind my back", not "I'm not allowed to"; the originals are still archived, and it reports "12 turns into 1, context 100k → 40k, 60% back" — or says plainly that it compacted nothing rather than hiding behind "below threshold". `/diff` lists the files this session touched, **counted from tool calls rather than from what the model said it did** — a model that claims an edit without ever calling `write_file` exists, and taking its word for it means covering for it; files deleted since are still listed, marked gone. In a git repo it adds `git diff --stat`; outside one it says so, so nobody thinks the diff is broken. `/mcp` reports which connectors are up and how many tools each brought, and **why the others are not** (an expired token says expired token) — a bare cross just makes people restart a hundred times.
- **Sep 18** `/init` has it read the current working directory and write an `AGENTS.md` at its root: what the project is, how to run it and test it, which directories are the trunk, which conventions are already settled. Both the desktop app and the CLI read that file before every run, so how accurate it is decides how well the next few dozen runs go. **It does not write the file itself — it hands the request back as an ordinary task**, so the permission mode, the confirm-before-editing step and the `/diff` record all still apply; a command that silently leaves a new file on your disk with nothing asked in between is exactly the mistake this kind of command invites. Two things are pinned in the wording: copy the commands you actually found in `package.json` or the docs (otherwise it invents an `npm test` this project may not have), and only write down conventions you found evidence for. If an `AGENTS.md` or `CLAUDE.md` is already there it reads it first and adds to it — **never overwrites** — because that one was probably written by hand.
- **Sep 18** Two key bindings, so switching a mode is not a command. `Shift+Tab` cycles this run's permission level (read-only → ask every step → edit files → back round), printing which level and what it means each time, and **touching nothing in the config file**. **The cycle does not reach "full auto"**: that key sits next to Tab, one mis-hit would switch off command confirmation while you are looking at your own half-typed line — full auto has to be typed as `/perm full`. `Esc Esc` lists what you already asked in this session and puts one back on the input line to edit and re-send. It **only puts it back; it does not rewind history**: deleting turns that already ran erases work the model did with nothing on screen to show for it — start over with `/new`, attach elsewhere with `/resume`, both in the open. One fix rode along: `/d` is nobody's shorthand any more, since `/diff` and `/drop` both start with d and claiming it means choosing for someone who typed one letter.
- **Sep 17** When a task finishes, it asks whether to clear out the intermediate files it just made. One short-film task splits out hundreds of frames, one soundtrack separates into four stems, one build spits out a `node_modules` - and once the cut has been delivered all of that is still sitting in the workspace, quietly costing gigabytes a month that nobody ever goes looking for. The end of a run now carries a card: "this run made N intermediate files, XX on disk", grouped by where they came from (extracted frames, separated stems, build output, scratch and temporary files, one-shot scripts), each group saying **why it is safe to delete** (the cut is still there, the original audio is still there, the `package.json` is still there, this is a script that was run once) - tick what you want, press once, and "keep them for now" does nothing at all. The same entry is pinned to the "Give me the work" home screen with the current figure written on it, because something you do this often has to be within reach: a cleanup buried in Settings may as well not exist. **The panel is deliberately two halves**: the top is what I am confident is scratch (deletable, each line carrying its reason), the bottom is where the space actually went - usage per task, reported and not judged. The real weight on the disk is usually the three-week-old video task's finished cut, which no rule will ever touch, but you cannot decide about it until you can see it; every row there can be narrowed to "tidy just this task" or opened in Finder. **The red line**: `apply()` only accepts paths that this round's own `plan()` proposed and refuses everything else - including a `../../users.json` or `/etc/hosts` posted straight at it - because this is an endpoint that deletes files, and outside that allow-list there should be no second way in. Deliverables (the cut, the report, the delivery note) take part in no rule at all; a directory only counts as build output while its `package.json` is still there; a `.py` inside a source tree is not a one-shot script; one batch of wav files is reported once rather than counted twice by two rules. Delete means delete rather than a move to `.trash`, because moving the same bytes somewhere else is not the space back - and every deleted path goes into the audit log, so who cleaned what, and when, is still answerable afterwards
- **Sep 17** The library stopped handing you a screenful of rows that answer "this file no longer exists" when you click them, and it now treats the two causes differently. **Files the upgrade moved are followed, not hidden**: `migrate.js` sweeps loose files older than a day out of the workspace root into a dated archive folder, and afterwards the library was still looking up the bare filename it had recorded — a whole batch of files sitting perfectly well on disk were reported as gone when they had merely moved one level down. They are now reported at their new path, so a click actually opens them, and they are claimed on the way so the same file no longer shows up a second time under "unclaimed". Not searching the whole workspace by filename is deliberate: two task folders each hold a `cover.png`, a search would likely hand back the other one, and "that is not the image I asked for" is harder to notice than "it is gone"; the new path is only trusted under the *current* workspace root, because it is about to be used as an address — finding a namesake under some other root and handing back an address that will not open is worse than saying the file is gone. **Files that really were deleted stay hidden by default**: one video task splits out hundreds of frames and cleans them up after the analysis, and every such row is a claim that a file is there, paid for with a click that gets nothing; a task whose whole output is gone does not appear at all, since an empty shell you can only click into a "no such file" is worth less than nothing. Nothing is swallowed silently — a line underneath says how many are left out and brings them back in one click (struck through, marked gone), and the choice is remembered in localStorage.
- **Sep 17** Every API the company buys now goes through one door, and neither staff nor other programs touch the real keys any more. Upstream keys are entered once in the admin page; what goes out is a virtual key beginning `sk-`, speaking plain OpenAI on the wire — nobody's code changes, only the `base_url`. **Not just chat**: `/v1/chat/completions`, `/v1/embeddings`, `/v1/images/generations`, `/v1/videos/generations`, `/v1/audio/speech`, `/v1/audio/transcriptions` and `/v1/search` all pass the same gate — a company never buys chat alone, and the capabilities left outside the gate are exactly the expensive ones (video is billed by the second). Each key carries its own capability switches: an image-only key that calls speech gets a 401 on the spot, and `/v1/models` does not even list the chat models for it — every id in that list is a promise that you may call it, and a promise that answers 401 sends the developer on the other side digging through their own code before they find the switch. Generated images and clips land at `/v1/files/<id>/content`: they expire in 24 hours and are deleted on expiry, and another tenant fetching that id gets a **404, not a 403** — a 403 would confirm the id exists. The real key is only ever assembled into the upstream request in memory; it appears in no log and no response. Virtual keys are stored hashed, the UI shows the last four characters, and a lost one is reissued rather than recovered.
- **Sep 17** The relay bills the way each vendor actually sells, and the quota is a gate rather than a report. Chat is metered per token (yuan per million, against a built-in price list current to 2026-09-17, USD converted at 7.1); the other four go by their own units — images per picture, video per second, speech per thousand characters, transcription per minute. Folding all of them into tokens would make the numbers look wonderful, and the invoice at the end of the month would not follow. Prices layer three deep: built-in → what the admin edited → what one channel overrides. The admin table edits a single capability-plus-model row, takes effect on save, and every row says whether that price is the built-in one or yours; an auditor can read it and cannot write it, since editing that table is editing every figure on the page down to zero. Anything whose price cannot be computed — a model that shipped last week — is recorded in its own "cost unknown" column and **never as 0**: the moment it is written as 0 the money is unrecoverable. The quota reserves before the call and settles after it, and going over returns 402 `insufficient_quota` there and then. Three levels of cap — organisation, department template, single key — where 0 means unlimited. **What your staff run from the UI, and what the scheduler runs overnight, come out of that same budget**: only the outbound path used to pass the gate, and the half that leaked was the half with a Retry button on it. The message names whose budget ran out — "the whole organisation is out of budget this month, not just you: cap ¥2.00, spent ¥2.10" — because a message that doesn't will be read as bad luck and clicked again.
- **Sep 17** Source files in the workspace now render as code: a line-number gutter that stays put when you scroll sideways, five-colour highlighting, and — the part that mattered — **no wrapping**. They used to fall through to the plain-text `<pre>`, which is tuned for logs and prose: `white-space: pre-wrap` plus `overflow-wrap: anywhere` keeps a long URL from stretching the panel, but the same rule applied to code breaks words at *arbitrary* points — `fun` on one line, `ction` on the next. That is what "looks like mojibake" meant: every character correct, every break wrong. Bundler output is worse still — a rolldown/esbuild `.mjs` is routinely one line of fifty thousand characters. So when a file looks minified (longest line over 400 characters) it is laid out for you by default: broken at `{` `}` `;`, indented by bracket depth, with a banner naming the longest line's length and a "show original" toggle beside it. The reformat only inserts newlines and indentation — not one character changes, and nothing is written back to the file. It **deliberately does not build an AST**: bundler output contains every strange construct there is, and a parser that meets one gives up on the whole file, whereas a scanner that only understands strings, comments and regexes can at worst break lines in ugly places without ever losing content. Those three it must understand — the braces in `var q="}}}"` are not structure, the slashes in `/[/{};]+/g` are not a comment, the semicolons in `for(let i=0;i<3;i++)` are not statement ends — and each has its own test. Highlighting covers six language families (C-like / `#`-comment / JSON / CSS / tag markup / SQL), with colours measured against the dark `#16181f` background (comments went from 3.1 to 5.2 contrast — a comment is there to be read, not to hide). Past 5,000 lines only the first 5,000 are drawn, and the banner says so (the file itself is untouched; download it and it is complete)
- **Sep 17** Skills get screened before they're installed. A skill's body *is* a set of instructions aimed at the agent, and the agent has a shell — install one from a link and it will be followed on the next task. So everything is scanned first: a directory, a single `.md` from GitHub, or text you pasted into the box yourself — all three go through the same gate. Three verdicts: **clean** installs; **worth a look** (outbound hosts, dependency installs, reading environment variables) shows you the actual lines and asks once more; **don't** (`curl | bash`, reading `id_rsa`, rifling `~/.aws`, `rm -rf ~`, reverse shells — 10 rules in all) is blocked, and only a platform admin can override, which is written to the log *and* to `.install.json` inside the skill folder: which repo, which commit, who waved it through, what the scan said. No score: of 34 rules only 10 can block and the other 24 only ever warn, because nobody can answer "it scored 7.5, do I install it?". It reads combinations, not keywords — `printenv` on its own means little; `printenv` next to an outbound address means something — and a line preceded by "don't" / "never" is ignored, since "never use `curl | bash`" is security advice, not an attack. A warning inside the frontmatter is escalated to a block, because `description` is injected into the system prompt of **every** task, so a sentence hidden there runs around the clock. **What it misses is stated too**: static scanners of this kind catch roughly three quarters on public benchmarks — one in four gets through — and on this repo's own 34 skills the false-block rate measured 5.9% (2 skills, both genuine literal matches rather than phantom hits). That number is pinned in the test suite, so a future rule change that starts over-blocking turns CI red instead of surprising someone mid-install.
- **Sep 17** Skill and connector screening can use [toolward](https://github.com/CatCatUncle/toolward) as a second opinion: findings merge and only ever tighten; not a dependency, never `npx`-ed, and connector config is redacted before it is handed over. Switch: `OPENWORKBUDDY_TOOLWARD=off / advisory / auto`
- **Sep 17** The film can carry a music bed: drop an "Audio" node on the canvas with its purpose set to "BGM" / "Score" and the compose panel gains a "lay music underneath" checkbox — with the track it picked named right next to it (when several pieces of music sit on the board, mixing in the wrong one costs you the whole render). The music ducks while someone is speaking (only where this machine's ffmpeg has `sidechaincompress`; otherwise it sits at a fixed level, and you are told before the run starts, not at the last command). A track shorter than the film loops; a longer one never leaves seconds of black at the end. And the dialogue does not lose a single dB: ffmpeg's `amix` divides every input by 2 by default, which is the quietest way to make a whole film's dialogue half as loud — so the voice is boosted back first and the mix goes through a limiter.
- **Sep 17** The `wb` short form is gone from the repo for good — the earlier rename only touched the command name and left everything else behind. Environment variables `WB_*` → `OPENWORKBUDDY_*` (`WB_HOME` names a host directory, which is not the container's `OPENWORKBUDDY_HOME`, so it became `OPENWORKBUDDY_DATA`), the Docker data directory `wb-data/` → `openworkbuddy-data/`, backup filenames `wb-backup-*.tar.gz` → `openworkbuddy-backup-*.tar.gz`, the login cookie `wb_token` → `openworkbuddy_token`, and the browser-local keys and CSS tokens `wb-*` / `--wb-*` → `owb-*` / `--owb-*`. One mismatch fixed along the way: the REPL help still printed `wb>` long after the real prompt became `openworkbuddy>`. **Two things to do when upgrading**: on a Docker install rename the directory (`mv wb-data openworkbuddy-data`) and re-copy the variable names from `deploy/env.example`; and everyone signs in once more, because the cookie changed name. Nothing else is manual — the sidebar's task history migrates itself, and existing backup archives are still recognised and still listed.
- **Sep 17** The `context 87%` bar above the composer no longer follows you into the next conversation. There is only one such bar for the whole UI and it is only repainted when a running task emits an event — switching to a conversation that hasn't started yet leaves nothing to wait for and nobody to clear it, so the previous conversation's line ("it'll be compacted before the next round") stayed on screen pointing at somebody else's number: a brand-new, empty conversation looked nearly full. All three ways of changing conversation (history, following a terminal run, starting a new task) now clear it first and let the conversation's own replayed number fill it back in. The other half is closed too: a conversation running in the background no longer paints its budget onto the one you are looking at.
- **Sep 17** Another wall between branch offices. When a file is looked up by relative path, the fallback pass sweeps **every workspace directory the server remembers** — including other branches' — so someone in a branch could read head-office files with nothing but a filename, or by pasting another directory's fingerprint into the URL. Candidate directories now pass a tenant filter first and anything outside the tenant is refused. Lookups across sub-projects within one tenant still work, and the personal install (no login — Feishu callbacks, cron jobs) takes exactly the old path.
- **Sep 17** Execution traces stop sharing one name. The label used to come from the **first** user message of the conversation, but one turn opens one trace and the whole history is what gets passed in — so ten turns in one session produced ten identical rows, and you had to open each to tell them apart. It now takes the **most recent message that actually says something**, skipping filler like "go on" / "ok" / "sure" (but not "go on and wire up the logs" — that is a real request), and every row is tagged with its turn number so even similar names stay distinguishable. **Traces already on disk are split apart too**: the ledger is not rewritten, but each record still carries the full input it was created from, so it is re-derived on read — and only when the record carries the old rule's fingerprint (the name ends exactly with the history's first message, and there is more after it). A name you chose yourself cannot match that, so it is never touched.
- **Sep 17** Forgot your password, and lost the phone with your 2FA codes? You no longer have to wait for an admin. On the server itself, `openworkbuddy passwd <user>` prints a fresh random password (or takes the one you supply), and `openworkbuddy 2fa <user>` shows two-factor status with `--off` to turn it off. Either way every session that user has open is revoked on the spot. **Neither is exposed over HTTP, ever** — the authority here is "you can read `users.json` on the server", which is already the highest privilege there is; adding a network endpoint would only give it away.
- **Sep 17** Onboarding and offboarding are each one step now, and each prints a receipt. Offboarding closes everything at once: account disabled, every session token revoked, paired phones and tablets knocked offline, unused invite codes they issued revoked, two-factor unbound, and their scheduled jobs stopped and optionally handed to a named successor (**still disabled after the handover**, so whoever takes over reads them before switching them on). Their conversations, output files, usage ledger and audit trail stay exactly as they are — the books still have to balance after someone leaves. Two guards against a slip: you cannot offboard yourself, and you cannot offboard the org's last active admin (nobody could get into the console afterwards). Onboarding applies a department template — role and monthly quota — so hiring the third person into a team does not mean remembering how the first two were configured.
- **Sep 17** You can see what is happening now: runtime logs land on disk daily and rotate on their own so they cannot fill the disk (still redacted — keys do not leak out through the log), and a snapshot of the key metrics is rolled every minute — request volume, latency percentiles, failure rate, spend per channel — on one page under Settings → Operations, with alerts pushed to WeCom / DingTalk when a line is crossed. One alert per rule per 30 minutes, and **recovery is announced too** — a system that only ever cries wolf gets ignored inside two weeks. Percentiles switched to nearest-rank: with few samples, interpolation reports a P95 that **never actually happened**, and it always looks better than reality.
- **Sep 17** The usage ledger is sharded by month, which also fixes an old design that **silently under-counted**. It used to be one `usage.json`: every entry meant reading the whole book, inserting a row and writing it all back — capped at the newest 2000 rows. Fifty people at ten calls a day **hit that ceiling in four days**, after which "usage this month" and "who is using it" quietly under-reported, with no error anywhere. Entries are now one line appended to `data/usage/2026-09.jsonl`; append cost is independent of history length (the old path took 262ms at 100k rows, paid at the end of every single task), "this month" reads one shard, and there is no cap any more — what a cap throws away is billing and audit data. An existing `usage.json` is split apart the first time the ledger is touched; it is **not deleted**, just renamed `.migrated` and left in place so you can reconcile. Execution traces got rotation in the same pass (six files kept, roughly a 144 MB ceiling).
- **Sep 17** The two remote doors each got a switch, and **both are off by default**: "allow pairing remote devices" (a phone, a second computer) and "allow driving a running terminal task from the web or a phone". Left on by default, somebody may not realise what they just exposed to the office LAN. The switches live in the org settings, and **turning one off knocks already-paired devices offline on their next request** — otherwise the switch is a lie: you think the phone is disconnected and it is not.
- **Sep 17** Images in previews can be copied: the preview panel, the lightbox on the infinite canvas, and anything you open from the library all take `Ctrl/⌘+C` straight to the clipboard, so pasting into WeChat, a slide deck or Lark gives you the picture, not a local file path. The desktop app's right-click menu gained Copy image / Copy image address / Save as.
- **Sep 17** The org-settings route hands the whole request body through, so the new table-shaped settings (department templates, quota tables) are scrubbed before they hit disk: known fields only, role restricted to admin/auditor/member, numbers clamped to range. Without that, accepting the key means accepting an arbitrary table into org settings — put a privileged role in it and onboarding would happily create the account that way. Also fixed an older path that stored a whole table as `"[object Object]"`.
- **Sep 17** The CLI is now **`openworkbuddy`**, not `wb`. Not for looks — `wb` is short enough to read as somebody else's initials. Reinstall if you linked it (`npm unlink && npm link`), regenerate completion (`openworkbuddy completion zsh > ~/.zsh/completions/_openworkbuddy`) and delete the old `_wb` / `~/.wb-completion.bash`. Flags and subcommands are untouched; only the name changed
- **Sep 17** Passwords, three gaps closed: a **password policy** (min length 6-64 and optional complexity, set per org; enforced at signup, self-service change and admin reset alike, and the random passwords the system hands out grow with it), **two-factor auth** (TOTP — Google Authenticator, 1Password, anything standard; can be required org-wide), and **self-service password recovery** (`openworkbuddy passwd <user>` on the machine that runs the service — no SMTP dependency; `openworkbuddy 2fa <user> --off` if the phone holding the codes is gone too. The sign-in page spells out both, so nobody has to go find the docs)
- **Sep 17** The licensing story is now explicit: **a commercial licence unlocks nothing.** There is one codebase and the copy you install is all of it; what you buy is a licence to make money with it, the trademark/white-label door, and someone who picks up the phone. The docs used to list SSO, audit export and air-gapped deployment as if they shipped — none of them are written yet, and they are now labelled as roadmap.
- **Sep 17** New exception: deployment configs, CI workflows, `scripts/`, the evaluation task set and baseline, the skill template `skills/skill-creator/`, and the code samples and format specs in the docs are **additionally MIT-licensed** — commercial use included ([the list](COMMERCIAL-LICENSE.md#按-mit-授权的部分)). It also states plainly that skills, plugins and connector configs you write are your own work, not derivatives of this project.
- **Sep 17** Audited the whole repo for "what else could be open-sourced": nothing is being held back on the code side. What stays closed is third-party skill packs (someone else's licence), a skill with no traceable upstream, and user data — each with its reason in §3 of the [boundary doc](docs/开源与商业版边界.md). The admin console's "Enterprise" sidebar badge is now "Admin" — it was a page name being read as a paid tier.
- **Sep 17** Library previews work again: images, Markdown, HTML and PDF all open. When the name is there but the file is not, it says which it is — moved away, or unreadable — instead of a broken-image icon, a blank page or "read failed". Sizes the server cannot determine now render as "—" rather than being floored into a fake "1 KB".
- **Sep 17** "Open in new window" in the desktop app no longer returns `{"error":"not signed in"}`: same-origin links open in a child window inside the app so the login cookie travels with them, and only genuine external links are handed to the system browser. The local-deployment preview is the exception — separate process, separate port, token in the link — and still opens in the browser so a phone can scan it.
- **Sep 17** The terminal asks you questions too now: at a fork ("Word or PDF?") it lays out the options with one line under each saying what picking it commits you to. It accepts the number, the option name or a prefix of it, and you can ignore the list and just type what you want. **It only asks when a human is actually at the terminal** — behind a pipe, under `--json` or in cron the agent is told nobody is online and to proceed with the most reasonable default, so nothing hangs waiting for an answer that will never come.
- **Sep 17** `openworkbuddy` overhaul: five subcommands (`sessions`, `resume`, `engines`, `doctor`, `completion`) with every old flag untouched; `-f` attaches files and images, `--raw` prints unrendered Markdown, `-V` prints the version. Exit codes are distinct (`2` = bad arguments, `130` = Ctrl+C), `NO_COLOR` / `FORCE_COLOR` are honored, and Tab completion for bash/zsh/fish is generated from the same table as `--help`, so a new flag shows up in all three at once.
- **Sep 17** Typing `/` in the interactive shell drops down the command menu — someone who cannot remember the commands types `/` and waits, they do not reach for Tab. It only offers, never picks: unless you pressed ↑↓ or Tab, Enter sends exactly what you typed; if the terminal does not take the control sequences, the menu shuts down for the session and falls back to Tab completion rather than garbling your input line.
- **Sep 17** A stray space, newline or full-width quote dragged in with a pasted key is caught on save, naming the offending character, instead of surfacing later as an unreadable 401. Chat and image/speech/video keys now pass through the same check.
- **Sep 17** `config.json` is written 0600. It used to land at whatever umask gave it (0644), so any other account on the same VPS or office machine could `cat` every key out of it. The file is tightened at boot as well, which fixes machines installed months ago, and the `.bak` alongside it gets the same treatment.
- **Sep 17** The rest of the permissions caught up: `users.json` and `orgs.json` are written 0600 like `config.json` (they hold password hashes, TOTP seeds and unused invite codes — one `cat` from another account on the same machine is enough material to log in as someone), and the `data/` and `logs/` directories are 0700. Logs deserve a separate mention: one JSON object per line, carrying who logged in and when, session ids, raw error text and the arguments that blew up — at the default 0755 that is a **traceless** record of who is doing what. The directories are born 0700 rather than chmod'd afterwards, so there is no 0755 instant in between. `chmod` is close to a no-op on Windows, so this only counts on macOS / Linux.
- **Sep 17** Changing a key is now an audit event: in an organization with several admins you can see who repointed which provider from which key to which (both recorded as `first three…last four` — plaintext keys never enter the audit table).
- **Sep 17** Video generation speaks five protocols (Tongyi Wanxiang / Volcano Ark Seedance / GLM CogVideoX / MiniMax Hailuo / SiliconFlow): the provider type wins, then the domain in the base URL, and a per-entry `protocol` in `config.json` covers gateways with several upstreams behind one address. If none of the three identifies it, **the request is not sent** — these are per-clip async jobs where a wasted call takes minutes to fail. Passing `last_frame` to a provider that has no such field is refused up front, a hand-written `protocol` survives saving from the settings page, and video-only providers no longer appear in the chat-model picker.
- **Sep 17** `[report](report.md)`-style links to workspace files in an answer now open the file (only `https:` used to be recognized, so those printed as literal brackets and parentheses), and Enter works as well as a click. Anything that cannot be resolved confidently is left as plain text, and neither `javascript:` nor quotes smuggled into the path can escape into an attribute.
- **Sep 17** The home screen's scenes are regrouped by what you get back and ended up as **three** (Documents & reporting / Data & content / Code & design) — past four or five, you have to categorise your own job in your head before you dare click anything, and that step was never necessary. Nothing has two entry points any more and there is no "financial services" — an industry label nobody can hand to an agent as a task. The infinite canvas moved above "Projects" in the sidebar, where a growing list of sub-items used to push it down until it read as the last row of the project group.
- **Sep 17** Self-evolution had an ordering bug that silently swallowed rules: when the injected block went over its character budget it always cut the **newest** entries — the ones a human had just approved on the review page, which then showed "in effect" while the model never saw a word of them. It now keeps the new and drops the oldest, names what it dropped, and counts the trailing "N more rules" line against the budget too.
- **Sep 17** The prompt now states what to ask about in exactly three cases (information without which the whole job is wasted / forks that change what gets delivered / anything that spends money, is irreversible, or only you know), and technical routing is always the agent's own call. Audio and video go through `transcribe_audio` before anything else — never `read_file` on a binary and a guess from the filename.
- **Sep 17** Moving to a new machine is now a closed loop: Settings → Data offers Back up now, Download, Import backup and Restore. A backup carries accounts, organizations, sessions, IM sessions, memory, personal preferences, usage ledgers, audit logs, `config.json`, schedules, experts and the skills you wrote yourself. An imported archive is unpacked and checked before anything touches disk — absolute paths, `..`, symlinks and hard links, non-gzip data and unexpected top-level entries are all refused — and the current state is backed up as `before-restore` first.
- **Sep 17** Send a message while a task is running and you choose what happens: Cut in interrupts the current step for an on-the-spot correction, Queue waits for the run to finish and then starts yours in order. A queued line can be taken back, and the choice is stored per device rather than per account.
- **Sep 17** Per-call paid APIs (search, image, video, voice, transcription) now sit behind a quota gate: ask before going over, record who spent it and through which provider, and read it back by day, month or person in the admin console. Everything is unlimited by default, so a solo user with their own key is unaffected.
- **Sep 17** Admin-console usage can be filtered by date range, member and model, and paged through — no longer just the last 25 rows.
- **Sep 16** Feishu now closes the loop for both group chats and document comments: @ the bot in a group to run a task and receive text/files; @ it in a cloud-document comment to identify the target document, read its context, and reply in the original thread. Fixed the comment-reply request shape that caused `field validation failed`; user instructions and document context are now separated so long documents are not echoed back; de-duplication survives restarts.
- **Sep 15** README now leads with a real deliverable and a copyable first task: the first screen explains local file delivery, then shows a fast path to an initial win plus clear Star and contribution actions. English readers now get explicit local-office, self-hosting and agent-learning positioning; `llms.txt` helps search and AI summaries understand the project correctly. The MIT notice for the DAG layout dependency is complete, and the full suite is green again.
- **Sep 15** Infinite-canvas interaction pass: drag blank space to pan, `Shift`+drag to select, then drag any selected node to move the group. A node body selects without opening properties; only its gear opens settings, so panels no longer interrupt work. The page header is tighter to return more height to the canvas.
- **Sep 15** The README now leads with a sanitized real task demo (input → execution → file preview). The AI short-drama canvas follows the feature overview with its own recording and a sanitized DAG screenshot; the recorder no longer lets a canvas recording replace the README hero demo.
- **Sep 14** AI short-drama infinite canvas reached a usable release: Canvas Agent, DAG links and compact layout, marquee selection / undo / context actions, node properties and asset previews, image enlargement, video and audio playback, workspace switching, @ references, model selection and local Trace now live inside one short-drama project; README adds a real, fully sanitized canvas GIF so contributors can understand it fast.
- **Sep 14** Added AI short-drama infinite canvas mode: scripts, characters, locations, storyboards, media, generated results and this project's Agent collaborate on one executable canvas, with multiple drama projects, asset drops, batch selection, auto layout and in-canvas chat.
- **Sep 14** Refined the short-drama canvas UI: reference images, video and audio preview in place; images enlarge; the canvas composer supports attachments, `@` node and asset references, execution mode and model choice.
- **Sep 14** Feishu message de-duplication now survives restarts: processed message IDs persist locally, so old tasks do not run again after a restart; the file stores IDs only, never message content or contact details.
- **Sep 13** It can shoot a story now: the shot list goes to you for a yes first, then character portraits, an opening frame per shot, video, voice, and one captioned vertical cut; change a shot and only that shot costs again
- **Sep 13** Re-running the same shot no longer costs twice: when an image / video / voice call carries byte-for-byte the same parameters as last time, the previous output is reused and the model is never called (pass `no_cache: true` when you do want a different take); there is also a new `POST /api/tool/run` that skips the model entirely, so replaying an identical call no longer burns a round of tokens reading your prompt back to it, and leaves it no chance to reword what you asked for — only image, video, voice and page-screenshot can be called that way
- **Sep 13** The channel table in the admin console can now be added to, edited and deleted from: a self-hosted gateway, a company proxy, a private deployment that moved domain — anything the built-in catalogue doesn't cover — goes in through "New channel"; two channels sharing one address get flagged instead of merged (separate accounts, separate keys, separate bills), and deleting one says up front what goes with it: how many chat models, how many media models, and whether your default model is in there
- **Sep 13** A command that isn't installed no longer comes back as a bare `command not found`: for tools it recognises, the reply says which one is missing, what it's for, and the exact line to install it on this machine (mac / Windows / Linux each get their own); aliases like `ffprobe` and `libreoffice` fold back to the package they ship with, instead of sending you after something that doesn't exist — and for a command it doesn't recognise it adds nothing at all
- **Sep 13** The default "max runtime for a task" disagreed in four places — the config template said 10 minutes, the code ran 30, the settings page said "default 10" — now it's 30 everywhere, with "auto-continue rounds" added to the config template (still 0 by default, since every extra round costs real money); the video skill also warns up front that past six shots you should raise the step and time limits, so a run doesn't die right before the final stitch, with the image and voice spend already gone
- **Sep 13** Image, video and voice generation can now run together in one round — 2 at a time by default, 1-4 in settings — so a twelve-shot short film no longer queues up one by one, kept deliberately below the read-only limit because each of these costs real money; this also fixes unnamed outputs landing on the same filename within a single millisecond, where the second one silently overwrote the first and both reported success
- **Sep 13** Image generation takes reference images (up to 4) and video generation takes a first and last frame, so the same person or product stays the same across shots; a text-to-video model handed an image (or the reverse) is caught before the request goes out, and a channel that really can't take the image says so instead of quietly falling back to text-only
- **Sep 13** Seven specialists — minutes, contract review, email, project plans, PRDs, recruiting and support scripts — now each carry a written spec, so what they hand back has a fixed shape and hard rules instead of a new format every time
- **Sep 13** Point it at an SMTP server and the agent emails the report it just wrote; a recipient allowlist is a hard gate, and every message shows you the full text before it leaves
- **Sep 13** The agent schedules its own recurring work: say "every Monday 9am, turn last week's numbers into a table" once and it runs on time
- **Sep 13** Recordings, meeting videos and voice notes turn into text, and it writes the minutes straight from there
- **Sep 13** Read the Word / Excel / PPT / PDF a client sent you, pull material straight from the shared library, and push a finished file to your group chat in one line
- **Sep 13** Your terminal can finally see what the agent drew: `/open` hands it to the system viewer, and capable terminals render it inline
- **Sep 13** Release pipeline closed up: PRs get a red/green light, a version mismatch fails loudly, one platform failing no longer sinks the whole release
- **Sep 13** Generated sites stop looking alike: pick a visual direction before writing a line
- **Sep 13** Every step now shows how long it took, and a closing tally: tool time vs. model-thinking time
- **Sep 13** Plug in Langfuse and you can read the raw input/output of every model call
- **Sep 13** Five API-key traps fixed at once: key not recognized, wizard every launch, duplicate channels, nowhere to paste, missing models
- **Sep 13** The admin console gets a "Models & Keys" page — admins configure there, not on the home page
- **Sep 11** Sidebar splits work into **Office / Engineering**; an `openworkbuddy` run in your terminal shows up on your phone and takes interjections
- **Sep 11** Image / video / voice / vision each take several models, and one provider key covers all of them
- **Sep 11** Six ways the app could fail to open on launch, each now explained in a real window, plus a boot log
- **Sep 11** Installer is 139 MB smaller: source maps and type declarations are never read at runtime
- **Sep 11** "Check for updates" no longer tells people who installed a package to run `git pull`
- **Sep 11** Three high-severity fixes: a one-letter auth bypass, a filename that could run commands, a preview site open to the internet
- **Sep 11** On a shared server, plain members no longer see a row of buttons that can only 403
- **Sep 11** UI polish: three surface levels, one shadow scale, and scrollbars you can actually see
- **Sep 10** Your task history was never lost — an invented project name was filtering it out. Fixed
- **Sep 10** One command to deploy: `bash deploy.sh`, and `--domain` gets you HTTPS
- **Sep 10** Multi-tenant + admin console: 16 panels; admins, auditors and members each see their own slice
- **Sep 10** No more stopping half-done: the progress file is checked before a task may end
- **Sep 10** One line per step in the process pane; raw arguments and full return values are one click away
- **Sep 10** Long replies stopped stuttering: on a 100k-char stream, DOM rebuild drops 19.7s → 0.7s
- **Sep 10** Goal cards derive and grade criteria through your local CLI, with no silent API spend
- **Sep 10** WeChat / WeCom / Official Accounts / QQ can receive files, images, voice notes and stickers
- **Sep 10** Create a Lark app by QR; the App ID fills itself back in
- **Sep 9** Finishing a task no longer pops a panel over your chat — outputs become a row of chips
- **Sep 9** 39 one-click connector presets; experts up to 15, plus 4 teams
- **Sep 9** English / Chinese in one click; Appearance adds theme, six skins, font size and density
- **Sep 8** Five-step first-run wizard sets up model, engine and IM
- **Sep 8** Local Claude Code / Codex as the engine, with memory, skills, files and media wired in
- **Sep 8** Reasoning models can have thinking turned off or dialed down
- **Sep 7** Scheduled tasks get a "false green" verdict: no exception is not the same as done
- **Sep 5** `openworkbuddy` CLI: one-shot, interactive, pipes, `--json`, honest exit codes
- **Sep 3** In-app preview for docx / xlsx / pptx / zip / csv
- **Aug 31** Paste or drag files and images into the chat; the agent actually reads the image
---
[← README](README.en.md)