Compare commits

..
68 Commits
Author SHA1 Message Date
mesalogo 3ebb4f0b79 chore: prepare Desktop 0.16.1 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-10 01:25:01 +08:00
mesalogo c7f09e1e53 fix(composer): let messages fade right up to the composer edge
Overlap the composer wrap with the message region by its top padding so messages scroll under the fade until the input outline, and paint the solid backdrop only below the composer's top edge. Drop the dark theme rule that still painted the whole wrap, keep the fade at 16px with a stronger curve, and lift the scroll-to-bottom button to preserve its gap.

Release note: 修复深色主题下聊天输入框上方残留的实色横条,消息可一直淡出到输入框边缘。
2026-10-10 01:13:44 +08:00
mesalogo d9dcb65d10 fix(composer): blend the message fade into the composer outline
Extend the composer fade down to the input card's top edge and layer it beneath the card, so scrolling messages fade straight into the composer outline. Remove the full-width composer-wrap background that produced a hard horizontal band, and soften the focus ring. Keep the dark theme fade on the canvas color.

Release note: 优化聊天输入框上方的渐变过渡,消息自然淡出到输入框边缘,不再出现横向色带。
2026-10-10 01:00:31 +08:00
mesalogo 1044a32b3b chore: prepare Desktop 0.16.0 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Prepare the Desktop 0.16.0 candidate with per-conversation expert selection and historical role snapshots. Keep Agent 0.15.6 unchanged because this range contains no Agent production changes.
2026-10-09 23:44:13 +08:00
mesalogo c6a8455f88 feat(composer): move role picker into the input and keep it per conversation
Show the selected role as a compact chip in the input's top-left corner, persist the choice per conversation, and snapshot it on replies so the timeline shows the role as a caption under GoodBuddy.
2026-10-09 23:26:23 +08:00
mesalogo e07a17f79f chore: prepare Desktop 0.15.14 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Prepare the Desktop 0.15.14 candidate with Supervisor recovery fixes and clearer settings categories. Keep Agent 0.15.6 unchanged because this range contains no Agent production changes.
2026-10-09 21:09:28 +08:00
mesalogo 4205ad6317 feat(settings): add category icons 2026-10-09 20:54:23 +08:00
mesalogo 8f1b600798 fix(supervisor): resume legacy changed-source reviews 2026-10-09 20:25:48 +08:00
mesalogo dc582b7acf fix(supervisor): re-review changed sources instead of failing the run 2026-10-09 17:05:26 +08:00
mesalogo 46a40e7fbf update pr 2026-10-09 16:22:28 +08:00
mesalogo 3556df8895 fix: adapt Electron model fetcher type
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
2026-10-09 01:50:03 +08:00
mesalogo d7fb4299a3 test: use real fetch for supervisor profile coverage
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-09 01:16:21 +08:00
mesalogo 4b4531abeb fix: stabilize overlay supervision fixtures
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-09 00:13:35 +08:00
mesalogo 69f64df279 fix: restore composer overlay integration
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 23:49:19 +08:00
mesalogo adb58d9200 fix(model): route requests through Electron net 2026-10-08 21:58:00 +08:00
mesalogo 47917094ad revert: restore batched chat history loading
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 17:08:49 +08:00
mesalogo e2a5f83666 docs: publish chat history navigation workflow
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
2026-10-08 16:34:25 +08:00
mesalogo 670b598530 fix: remove divider from runtime controls dialog
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
2026-10-08 16:23:08 +08:00
mesalogo 1a59c2d880 feat: apply selected experts as role prompts on the conversation runtime 2026-10-08 16:23:08 +08:00
mesalogo 13b58627c0 feat: surface expert and runtime controls in composer toolbar 2026-10-08 15:55:27 +08:00
mesalogo 4266ff5bea docs: expand VitePress user guide 2026-10-08 15:18:25 +08:00
mesalogo 377b14fca6 test: use immediate cancellation signal
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 14:38:59 +08:00
mesalogo a406f5962e test: restore cancellation and drain coverage
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 13:00:41 +08:00
mesalogo 7f0a73e6d0 fix: type storage failure assertion
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 12:29:16 +08:00
mesalogo 2d68d1043d test: drain storage after write completion
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 12:07:54 +08:00
mesalogo 4a2ea49069 docs: simplify README screenshots 2026-10-08 11:47:34 +08:00
mesalogo 277da413c6 test: remove stale cancellation fixture reference
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 11:38:23 +08:00
mesalogo e136026496 test: remove invalid pre-aborted storage request
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 11:19:08 +08:00
mesalogo 59a28209d3 test: match storage cancellation behavior
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 10:48:44 +08:00
mesalogo 9788b4cd8e test: make cancellation fixture deterministic
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 10:21:55 +08:00
mesalogo c90ad2af6f test: accept client readiness loss error
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 09:57:54 +08:00
mesalogo 95ece25c37 test: reject calls after confirmed storage loss
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 09:40:38 +08:00
mesalogo c4ab83a7e3 test: wait for storage loss state before retry
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 02:53:56 +08:00
mesalogo 7eb113028f test: keep crash write messages unique
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 02:37:31 +08:00
mesalogo 5ba127e332 test: accept storage readiness loss during crash
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
2026-10-08 02:16:49 +08:00
mesalogo 3d063937c3 feat(magic-notes): add rich table editing
Add native Quill table insertion and contextual row, column, and table actions. Preserve table-only drafts through composer validation, IPC, and storage, and document the saved behavior.\n\nRelease note: 魔法笔记支持插入和编辑表格,空单元格表格也可保存并在重新打开后恢复。
2026-10-08 02:02:24 +08:00
mesalogo 2a92f8f875 test: await storage replacement before retry
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 01:42:29 +08:00
mesalogo 95f441958d test: make storage loss rejection deterministic
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 01:06:13 +08:00
mesalogo 384683e861 test: stabilize storage loss validation races
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-08 00:39:03 +08:00
mesalogo 7eb51bd5ff test: isolate utility process database permissions
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-07 23:26:37 +08:00
mesalogo 9212fd4359 test: keep storage parity assertion deterministic
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-07 23:00:38 +08:00
mesalogo 42ad8a98b4 test: stabilize storage owner CI fixtures
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Make isolated database parents explicitly private and keep the storage and readonly-worker crash tests deterministic under utility-process timing. Preserve committed-data recovery assertions and dedicated failure coverage.
2026-10-07 22:59:37 +08:00
mesalogo fbfafa15e1 chore: prepare desktop 0.15.13 and agent 0.15.6
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Document the desktop storage utility-process owner, coordinated Agent runtime changes, bounded output continuation, and focused validation boundaries. Record schema 62 compatibility and the remaining concurrent acceptance matrix.
2026-10-07 22:27:02 +08:00
mesalogo c1e4bbf66a test(magic-notes): preserve empty global tags 2026-10-07 22:03:12 +08:00
mesalogo 3e3a021682 fix(magic-notes): preserve duplicate-tag error cause 2026-10-07 21:38:13 +08:00
mesalogo c465d39d58 docs(architecture): clarify storage pass-through mode 2026-10-07 21:26:40 +08:00
mesalogo 13c29badfb feat(magic-notes): improve tag management UI 2026-10-07 21:05:14 +08:00
mesalogo e0607a153f fix(supervisor): expand bounded source aliases 2026-10-07 17:15:00 +08:00
mesalogo 425accf49d fix(settings): preload route on direct navigation 2026-10-07 16:58:09 +08:00
mesalogo 904bb691b8 perf(parsing): remove unsupported worker queue limit 2026-10-07 16:15:28 +08:00
mesalogo 4c25e85b08 test(storage): update fault acceptance capacity 2026-10-07 16:01:18 +08:00
mesalogo 8473189fe2 perf(concurrency): remove unsupported product admission limits 2026-10-07 15:52:07 +08:00
mesalogo 1f85d0c0e9 docs(storage): set the C06 lifecycle acceptance window to five minutes 2026-10-06 15:43:46 +08:00
mesalogo d80d42b032 feat(storage): move desktop SQLite ownership into a storage utility process
Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.

- Remove the Main readWithFallback path and the supervision worker's
  separate business write connection; manifests are scanned read-only and
  committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
  unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
  ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
  storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
  directories and reclaim confirmed stale ones; stop writing duplicate
  parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
  acceptance fixtures, and maintenance/architecture documentation.

Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
2026-10-06 15:40:18 +08:00
mesalogo bc677a3718 chore(repo): keep agent temporary files under the ignored temp directory 2026-10-06 15:39:47 +08:00
mesalogo edf019aaa7 fix: soften the transition above the chat composer
Add a theme-aware 16px fade above the composer to soften the clipped message edge without changing layout or pointer interactions. Document the treatment in the UI design system.

Release note: 聊天输入区顶部增加淡渐变,让消息与输入框之间的衔接更自然,并适配浅色和深色主题。
2026-10-05 21:00:44 +08:00
mesalogo 4bb7573b10 fix: simplify workspace file list metadata
Show only modification time beside file names and share its space with hover actions. Remove the creation-time column and fixed minimum tree width to keep narrow workspace panels compact.

Release note: Workspace file lists now show only modification time and keep file names stable when more actions appear.
2026-10-05 16:20:25 +08:00
mesalogo 56b8b10d46 fix: let direct models continue after tool execution failures
Return ordinary tool exceptions as paired error results so the model can decide how to recover. Preserve cancellation, existing recovery guidance, and failed tool activities without adding automatic retries or retry limits.

Validate all three text protocols with real process execution and cover bounded errors and cancellation. Focused tests: 161 passed, 1 skipped; typecheck and lint passed. Full suite: 6230 passed, 87 skipped, 1 Obsidian temporary-directory assertion failed.

Release note: Direct-model conversations can continue after a tool fails, allowing the model to use the reported error to choose its next step.
2026-10-05 15:20:17 +08:00
mesalogo 0eaaa85d4c ci: provide a real keyring for encrypted Electron settings
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-05 15:05:28 +08:00
mesalogo 36e671a0a2 chore: prepare desktop 0.15.12 and agent 0.15.5
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Document Telegram validation boundaries, custom review ranges, review recovery and responsiveness, channel delivery and remote cancellation fixes. Record schema 62 backup requirements and separate development evidence from release CI.
2026-10-05 14:41:15 +08:00
mesalogo 2be57492e6 fix: resume reviews after conversation deletion and update release docs 2026-10-05 14:22:16 +08:00
mesalogo b90ff50f2a fix: reduce supervisor stalls and preserve task delivery
Load Supervisor graphs only when opened, preserve source details on demand, reuse indexed story digests and window selection lists without losing keyboard or focus behavior. Route heavy UI reads through the existing worker, batch scoped queries and construct each publication snapshot once.

Keep remote cancellation responsive during model-route cleanup, remove late routes and retain reported usage after response-cap cancellation. Preserve Telegram pending replies through polling conflicts, show delivery errors and honor the final rate-limit cooldown.

Schema 62 adds lookup indexes without rewriting user content; older clients cannot open upgraded data. Large review publication still blocks Main and remains documented follow-up work. Record the future storage-service direction without performing the architecture migration.

Validation: exact staged code passed isolated full tests (6212 passed, 87 skipped), typecheck and lint. Three-round source performance comparisons, SQLite migration equivalence, native Electron interaction checks and current-source Linux x64 Host validation completed; eight external text-model requests.

Release note: Supervisor opens reviews without loading the entire graph, keeps long selection lists responsive and preserves details and keyboard navigation. Remote cancellation and Telegram reply recovery are more reliable; remote fixes require an Agent build containing this change.
2026-10-05 14:18:33 +08:00
mesalogo e50ca6d149 fix: retry transient supervisor model failures once 2026-10-05 10:47:09 +08:00
mesalogo b4df493be7 fix: clarify Telegram sender ID setup order
Explain in Chinese and English that sender IDs can be left blank while enabling and saving the channel. Guide users to retrieve their own numeric ID with /whoami and save again, and distinguish connection testing from message receiving.

Validation: 60 related tests, typecheck, lint and diff checks passed before submission.

Release note: Telegram setup now explains how to connect before obtaining your sender ID, avoiding confusion about which configuration step comes first.
2026-10-05 10:35:19 +08:00
mesalogo bbf24c282a fix: resolve runtime, review, channel and chat state failures
Keep remote startup cancellable without blocking other sessions and surface local OpenCode submission failures promptly. Preserve supervisor project isolation, pause behavior and valid story destinations.

Restore generated-image channel delivery, Telegram reconnection on unchanged Save and DingTalk business-error retries. Preserve virtualized message disclosures using sparse state and release stale supervisor UI requests.

Add production-path regressions, Electron checks, isolated SQLite validation and audit evidence. Fix the SDK cleanup test fixture lifetime under garbage collection without weakening its assertions.

Validation: 6136 tests passed, 87 skipped; typecheck, lint and diff checks passed. Seven real text-model calls, current-source Linux x64 Host scenarios and large-history Electron comparisons completed.

Release note: Fix stuck runtime requests, inconsistent review results and channel delivery failures, and retain expanded details while browsing long conversations. Remote runtime fixes require an Agent build containing these changes.
2026-10-05 10:25:43 +08:00
mesalogo 126dbc57e2 feat: support custom supervisor review date ranges 2026-10-05 02:49:02 +08:00
mesalogo d0877d9212 fix: distinguish missing historical timing from zero duration 2026-10-05 02:45:40 +08:00
mesalogo cf598121b1 feat: add Telegram private messaging channel
Connect a user-created Telegram bot to channel projects with encrypted credentials, numeric sender allowlists, unified execution and text replies. Use Electron networking for system proxy support, with polling recovery, cancellable delivery retries and live status updates.

Add schema 61 channel support, lifecycle regression tests, Electron integration probes and feature documentation. Require the performance principles in AGENTS.md and record three alternating baseline/current Main benchmark rounds.

Validation: typecheck, lint and diff checks passed. Full suite: 6087 passed, 87 skipped, one intermittent inline-help Electron failure that passed unchanged in isolation. Real Bot plus model acceptance remains pending; the local TLS proxy probe is opt-in.

Release note: 新增 Telegram 私聊通道,可配置 Bot Token 和用户白名单,通过通道项目处理文字请求并回复结果。支持系统代理、断线重连和停用取消;真实 Bot 与模型的完整联调仍待完成。
2026-10-05 02:42:28 +08:00
421 changed files with 27995 additions and 5386 deletions
+7 -2
View File
@@ -43,7 +43,7 @@ jobs:
- name: Prepare sandboxed Electron tests
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends --yes xvfb xauth
sudo apt-get install --no-install-recommends --yes xvfb xauth dbus-x11 gnome-keyring
sandbox="node_modules/electron/dist/chrome-sandbox"
if [[ ! -f "$sandbox" ]]; then
rm -rf node_modules/electron/dist
@@ -55,7 +55,12 @@ jobs:
- name: Run validators
run: |
xvfb-run --auto-servernum npm test
dbus-run-session -- bash -euo pipefail -c '
export XDG_CURRENT_DESKTOP=GNOME
# An isolated CI login keyring exercises real safeStorage encryption.
head -c 32 /dev/urandom | base64 | gnome-keyring-daemon --unlock --components=secrets
xvfb-run --auto-servernum npm test
'
npm run typecheck
npm run lint
+9
View File
@@ -7,6 +7,8 @@ on:
- main
paths:
- 'sites/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/pages.yml'
permissions:
@@ -29,8 +31,15 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
- name: Validate website
run: |
npm ci --ignore-scripts
npm run docs:build
node sites/scripts/validate.mjs
node --check sites/app.js
+3
View File
@@ -4,7 +4,10 @@ dist/
# Local build output left by the extracted ShareServer project.
shareserver/build-server/
coverage/
/temp/
.vite/
sites/docs-dist/
sites/docs/.vitepress/
.runtime-resources/
.remote-runtime-resources/
.agent-resources/
+33
View File
@@ -140,6 +140,16 @@ additional hardening beyond the actual internal-network threat model:
## Implementation Conventions
- Read and follow
[`docs/architecture/performance-principles.md`](./docs/architecture/performance-principles.md)
before designing, implementing, or reviewing a feature. Treat its performance
budgets, process boundaries, state ownership, startup rules, and review
checklist as implementation constraints. Include this reference in parallel
agent assignments so all agents follow the same rules. For performance or
hot-path changes, provide the applicable measurements and validation required
by that document; do not claim improvements without evidence.
- For new feature work, also follow the repository maintenance rules in
[`docs/development/agent-feature-maintenance.md`](./docs/development/agent-feature-maintenance.md).
- Follow surrounding TypeScript and React patterns.
- Reuse installed libraries and shared contracts before adding dependencies.
- Keep changes focused. Do not add unrelated refactors or documentation.
@@ -297,6 +307,29 @@ Repository-wide release invariants:
## Validation
### Local Temporary Files
- Put all agent-created local temporary files under the repository-root `temp/`
directory (`D:\my_git\goodbuddy\temp` in this checkout), in a task-specific
subdirectory. This includes database copies, scratch scripts, test fixtures,
logs, screenshots, and temporary build or validation artifacts. Do not put
them in Windows Temp or `AppData/Local/Temp/opencode`.
- Resolve this directory from the checkout root rather than hard-coding a
machine path in scripts. Point test-tool temporary directory options and
child-process `TEMP`, `TMP`, or `TMPDIR` here when needed. Leave production
application data paths unchanged.
- `temp/` is Git-ignored and disposable. Never store original user data,
required backups, or the only copy of a deliverable there. Keep reusable
validation scripts in `scripts/` or `tests/` and concise results in the
owning feature documentation.
- Clean task temporary files after validation, including database copies and
their WAL/SHM files, on success and failure. Close owned processes and
database connections first. Reuse a baseline copy where possible instead
of retaining a full database for each test case. If cleanup is blocked,
report the remaining path; do not silently accumulate copies.
- Apply these rules to parallel agents as well. Remote Host temporary files
continue to follow the dedicated remote-testing directory rules below.
Run validators after source changes when function/fix done, not on every source change:
```text
+41 -5
View File
@@ -344,7 +344,7 @@ records are listed separately and do not introduce another feature status.
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
database. Successful tool START/END events appear exactly once, with no
Prompt, provider, or tool replay observed. The current Agent source lock is
`0.15.4`, paired with the current Desktop release candidate `0.15.11`; formal
`0.15.6`, paired with the current Desktop release candidate `0.15.13`; formal
publication status follows the separate Agent and Desktop
release channels. Previous macOS validation covered native package installation,
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
@@ -931,12 +931,13 @@ records are listed separately and do not introduce another feature status.
action errors.
- [x] **Client-computer control tools**: Managed separately from the built-in
browser with scope, cancellation, timeout, output, and activity boundaries.
- [x] **Remote messaging-channel projects**: WeChat ClawBot, WeCom, and
DingTalk each have a system-managed project, independent remote
- [x] **Remote messaging-channel projects**: WeChat ClawBot, WeCom,
DingTalk, and Telegram each have a system-managed project, independent remote
conversations, working directory, processing backend, and
task/activity ownership. Each channel controls complete-response
length and segmentation according to platform capabilities rather than
relying on a shared service truncation.
relying on a shared service truncation. Telegram's live Bot + model chain
remains [pending acceptance](./docs/features/telegram-channel/progress.md).
- [x] **WeChat ClawBot QR login and media**: A separate Sidecar handles local
QR scanning, verification codes, encrypted credentials, and text messaging.
Personal WeChat chats support images and files, with at most four
@@ -982,9 +983,44 @@ records are listed separately and do not introduce another feature status.
### Open source, builds, and releases
- The current Desktop candidate is `0.15.11`; Agent candidate `0.15.4` requires Desktop `0.15.11`, with
- The current Desktop candidate is `0.16.1`; Agent `0.15.6` remains paired with Desktop `0.16.1`, with
OpenCode pinned to `1.18.29` and Continue to `1.5.47`. Publication status follows the independent
Desktop and Agent release channels.
- Desktop `0.16.1` smooths the message-to-composer transition: messages extend to the composer
outline before fading, and light/dark theme backgrounds and focus boundaries no longer create
an abrupt solid band above the input. Schema remains unchanged.
- Desktop `0.16.0` moves expert and team-role selection into the composer and persists the
selection per conversation. Historical messages retain the role used for their request,
including after a role is renamed or removed; schema remains unchanged.
- Desktop `0.15.14` restores continuation for legacy Supervisor reviews that failed because a
source changed or was removed. Sources that keep changing are re-frozen only within a bounded
number of attempts and then omitted so the review can finish while retaining surviving progress.
Settings categories also show matching icons to make the settings page easier to scan. Schema
remains 62.
- Desktop `0.15.13` / Agent `0.15.6` move desktop business SQLite ownership into
one typed storage utility process and keep Remote Runtime changes coordinated.
Assistant, Knowledge, attachments, runtime bindings and ledgers retain their
paths and transaction semantics. Direct models continue after tool failures,
while bounded paged output, Magic Notes tags, Supervisor aliases, settings
preload and workspace metadata are corrected. Schema remains 62.
Focused storage and Supervisor orchestration passed, but the required C01–C07
concurrent acceptance matrix, cross-platform storage-process installation and
low-end comparisons remain explicit follow-up gates; no universal performance
claim is made. See the
[preparation record](./docs/development/release-preparation-0.15.13.md).
- Desktop `0.15.12` / Agent `0.15.5` add Telegram private-channel configuration
(complete real-bot-plus-model acceptance remains pending) and custom local-date
review ranges. Supervisor uses lazy graph/source reads, indexed Worker queries,
cached digests and windowed selectors; final publication can still block Main.
Unpublished reviews omit deleted conversations and recompute affected batches,
retaining published history. Transient model failures allow one extra attempt
with possible additional cost. Remote startup/cancellation, channel delivery,
long-message disclosure state and missing historical timing are corrected.
Schema 62 adds channel support and lookup indexes; back up the complete data
directory and restore it to downgrade. Development audits recorded 15 external
text requests across two separate stages; this release preparation adds none.
Exact-source CI and native packaging remain required. See the
[preparation record](./docs/development/release-preparation-0.15.12.md).
- Desktop `0.15.11` / Agent `0.15.4` remove Ask/Execute and general tool
approvals, including the former read-only boundary of old Ask conversations.
Schema 60 and Runtime settings 22 remove retired metadata; back up the complete
+8 -3
View File
@@ -53,7 +53,7 @@
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 的原生参数按当前账号权限发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并应用多文件补丁;读取和补丁 API 保留工作区相对路径合同。用户无需另行安装 ripgrep。搜索保留原生输出与退出码,大结果通过 `output_read` 完整续读,可处理的参数错误允许模型调整后继续。
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的搜索、命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。请求使用所选 SSH 账号权限和已接入能力,无产品模式或一般工具审批。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖。当前 Agent 源码锁为 `0.15.4`,配套 Desktop 候选为 `0.15.11`;无模式源码要求 runtime/acp 6,发布前须重建配套工件。详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。请求使用所选 SSH 账号权限和已接入能力,无产品模式或一般工具审批。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖。当前 Agent 源码锁为 `0.15.5`,配套 Desktop 候选为 `0.15.12`;无模式源码要求 runtime/acp 6,发布前须重建配套工件。详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
- **SSH Host 环境准备验证记录**:此前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。现有记录未覆盖 Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵,候选 CI/原生打包与系统休眠唤醒也尚未验证;开发记录不代表版本已经发布。
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
@@ -144,7 +144,7 @@
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
- [x] **浏览器关闭与刷新**:AI 使用中的页签也可关闭,不取消整个请求;后续显式导航会创建独立替代页。空白页不能刷新,切换或关闭面板后清除旧操作错误。
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。
- [x] **远程消息通道项目**:微信 ClawBot、企业微信、钉钉和 Telegram 分别拥有系统管理的项目、独立远程会话、工作目录、处理后端及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。Telegram 的真实 Bot 与模型链路仍[待验收](./docs/features/telegram-channel/progress.md)。
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
@@ -159,7 +159,12 @@
### 开源、构建与发布
- Desktop `0.15.3` 修复本地直连手动压缩和请求有效期间的工具授权,区分取消与失败并去除重复提示,优化侧栏搜索、注册成功后的快捷键提示及笔记操作布局,减少启动和存储中的重复处理,并精简重复依赖及开发文件。不宣称整体渲染性能已显著提升;Office 实时编辑仍为设计。证据见[0.15.3 发布准备](./docs/development/release-preparation-0.15.3.md)。
- 当前 Desktop 候选为 `0.15.11`,Agent 候选为 `0.15.4`、最低要求 Desktop `0.15.11`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- 当前 Desktop 候选为 `0.16.1`,Agent 保持 `0.15.6`、最低要求 Desktop `0.16.1`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- Desktop `0.16.1` 平滑消息与输入框的过渡:消息会延伸到输入框轮廓前再淡出,浅色和深色主题的背景与焦点边界也不再在输入框上方形成突兀的实色横带;schema 保持不变。
- Desktop `0.16.0` 将专家和团队角色选择移入输入框,并按会话保存选择。历史消息保留请求当时使用的角色,即使角色之后被重命名或删除也不会丢失这项上下文;schema 保持不变。
- Desktop `0.15.14` 恢复因来源变化或移除而失败的历史监督回顾的继续操作。持续变化的来源只会在有限次数内重新冻结处理,之后自动省略,使回顾能够结束并保留其余来源的进度;设置分类增加对应图标,方便快速查找;schema 仍为 62。
- Desktop `0.15.13` / Agent `0.15.6` 将桌面业务 SQLite 的唯一 owner 移到一个类型化 storage utility process,Assistant、Knowledge、附件、运行绑定和账本保留原路径及事务语义。直连模型在工具失败后可继续,分页输出、Magic Notes 标签、监督来源别名、设置直达预加载和文件元数据同步修正;schema 仍为 62。storage/监督 focused 编排已通过,但 C01–C07 并发验收矩阵、跨平台 storage 进程安装和低端机对照仍是明确后续门槛,不作通用性能承诺。见[发布准备](./docs/development/release-preparation-0.15.13.md)。
- Desktop `0.15.12` / Agent `0.15.5` 增加 Telegram 私聊配置(真实 Bot 与模型完整验收仍待完成)和本地日期自定义回顾。监督图谱/来源按需读取、索引 Worker 查询、摘要缓存和选择列表窗口化减少重复工作,最终发布仍可阻塞 Main;未发布回顾省略已删除会话并重算受影响批次,历史结果保留。临时模型失败最多多试一次,可能增加费用;远程启动取消、通道交付、长消息展开状态及历史缺失时长得到修正。schema 62 增加通道支持和查询索引,升级前备份完整数据目录,降级需恢复备份。两个独立开发审计阶段共记录 15 次外部文本请求,本次发布准备没有新增;精确源码 CI 与原生打包仍为门槛。见[发布准备](./docs/development/release-preparation-0.15.12.md)。
- Desktop `0.15.11` / Agent `0.15.4` 移除 Ask/Execute 与通用工具审批,旧 Ask 会话也不再有产品级只读边界。schema 60 和 Runtime 设置 22 清理退休元数据,升级前备份完整数据目录,降级需恢复备份;远程需配套 runtime/acp 6 工件。工作区导入、文件时间排序、会话图片发现和远程专家保存随成果页移除一起提供,不删除既有成果。执行时长按运行状态计时、不补算旧历史,OpenCode 缓存率采用完整输入量。回顾历史、响应布局及螺旋实际区间着色保留未归属事件与中性空档;汇总读取和回顾初始化减少限定路径 I/O,不承诺普遍提速。开发证据含无模式执行与专家图片保存共 25 次真实文本请求,不是本次发布准备新调用或图片生成;精确候选 CI、签名包、公开验证和跨版本活动操作恢复与这些检查分开记录。见[发布准备](./docs/development/release-preparation-0.15.11.md)。
- Desktop `0.15.10` 统一项目/会话菜单并显示十个最近会话,增加独立监督模型选择及默认开启的会话 Story Graph 开关。活动索引分页与完整汇总、文档 Worker、延迟启动、窄渲染订阅、列表窗口化、终端批次和旧 Runtime 缓存清理减少重复工作,不承诺通用提速比例。schema 58 要求升级前备份数据库及笔记;高频本地写入改用 NORMAL,断电/系统崩溃可能丢失最近部分写入,远程确认、通道投递和任务生命周期仍用 FULL。Agent 保持 0.15.3;四次真实 Host 模型请求及 1000 行 Unicode SSH 终端检查通过,精确候选 CI 与六平台打包仍为发布门槛,不宣称已修复尚未定位的远程恢复错误。见[发布准备](./docs/development/release-preparation-0.15.10.md)。
- Desktop `0.15.9` 增加共享回顾进度、故事经验与时间螺旋,schema 56 要求升级前备份,相关模型整理可能增加费用。会话与消息窗口化、Markdown 缓存、只读 Worker、活动差量保存及远程 checkpoint 批次落库减少重复工作,不承诺所有机器相同比例提速;页内查找不覆盖未挂载行,快速滚动可能显示占位,Worker 不可用时同步回退。Execute 配置修改取消重复原生确认,Electron 为 44.5.1。当前源码四次真实 Host 模型请求通过,仍需精确候选 CI 与打包验证,见[发布准备](./docs/development/release-preparation-0.15.9.md)。
+2 -10
View File
@@ -19,7 +19,7 @@
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
![GoodBuddy workspace](./docs/screenshots/workspace-overview.png)
![GoodBuddy workspace](./docs/screenshots/GoodBuddy_en.png)
GoodBuddy is a desktop application for model chat, coding agents, knowledge
retrieval, notes, and scheduled tasks. No GoodBuddy account is required.
@@ -56,16 +56,12 @@ current limitations.
| Data and execution | Local SQLite and note-file storage, current-account tool execution, and recorded tool activity |
| Knowledge | File, folder, and web imports with full-text, Chinese phrase, vector, and knowledge graph retrieval |
| Desktop releases | Official Windows, macOS, and Linux builds for `x64` and `arm64`, plus an experimental LoongArch preview |
| Integrations | Local or hosted model endpoints, custom MCP servers, WeChat ClawBot, WeCom, and DingTalk |
| Integrations | Local or hosted model endpoints, custom MCP servers, WeChat ClawBot, WeCom, DingTalk, and Telegram ([live Bot + model acceptance pending](./docs/features/telegram-channel/progress.md)) |
## Product tour
### Knowledge bases and graphs
![GoodBuddy knowledge workspace](./docs/screenshots/knowledge-workspace.png)
![GoodBuddy knowledge graph](./docs/screenshots/knowledge-graph.png)
### Magic Notes and Smart Heartbeat
Magic Notes displays a continuous record stream, with a narrow left index and
@@ -77,10 +73,6 @@ default model's image-input capability, with an explicit text-only fallback when
images are unsupported. See the [Magic Notes documentation](./docs/features/magic-notes/README.md)
for behavior and final validation status.
![GoodBuddy Magic Notes](./docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy Smart Heartbeat](./docs/screenshots/smart-heartbeat.png)
## Agent Runtimes
| Runtime | Functions | Configuration |
+2 -10
View File
@@ -19,7 +19,7 @@
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
![GoodBuddy 工作空间](./docs/screenshots/workspace-overview.png)
![GoodBuddy 工作空间](./docs/screenshots/GoodBuddy_zh.png)
GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时任务的桌面应用。使用 GoodBuddy 无需注册账号,工作数据默认保存在本机。模型服务可以运行在本机、组织内网或用户选择的云端服务。
@@ -41,16 +41,12 @@ GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时
| 数据与执行 | 本地 SQLite 与笔记文件存储、当前账号权限下的工具执行、工具调用记录 |
| 知识库 | 导入文件、目录和网页,通过全文、中文词组、向量和知识图谱检索 |
| 桌面版本 | Windows、macOS、Linux 的 `x64` 与 `arm64` 正式版本,以及龙芯 LoongArch 实验预览版 |
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信和钉钉 |
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信、钉钉和 Telegram([真实 Bot 与模型链路待验收](./docs/features/telegram-channel/progress.md)) |
## 产品界面
### 知识库与知识图谱
![GoodBuddy 知识工作区](./docs/screenshots/knowledge-workspace.png)
![GoodBuddy 知识图谱](./docs/screenshots/knowledge-graph.png)
### 魔法笔记与智能心跳
魔法笔记连续展示全部记录,左侧窄栏索引与右侧 AI 评论可独立收起,点击缩略图滚动定位并保留草稿。
@@ -59,10 +55,6 @@ GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时
默认模型的图像输入能力分析,不支持图像时明确提示仅文字降级。具体行为与最终验证状态见
[魔法笔记文档](./docs/features/magic-notes/README.md)。
![GoodBuddy 魔法笔记](./docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy 智能心跳](./docs/screenshots/smart-heartbeat.png)
## Agent Runtime
| Runtime | 适用场景 | 配置来源 |
+1
View File
@@ -163,6 +163,7 @@
- 页面顶部和底部内边距默认为 `32px`。
- `master-detail` 的左栏建议为 `280px` 至 `360px`,右栏占剩余空间。分隔线和滚动容器属于壳层,不由内容卡片模拟。
- 聊天输入区可以粘附在 `reading` 壳层底部,但消息内容和输入区必须共享同一阅读宽度。
- 输入区外层不铺整块背景、不画横向分隔线。输入区以上内边距的高度向上叠入消息区,输入框上方保留 `--space-4`(16px)的透明到页面底色渐变,渐变终点对齐输入框上缘,让滚动的消息一直延伸到输入框轮廓前再淡出;输入框上缘以下才使用实色底;渐变位于输入框之下、不拦截鼠标事件,并随主题切换。输入框上缘只保留自身 1px 描边,不加向上的阴影,避免与渐变终点叠成粗边界。
- 宽表格可在 `dashboard` 内容区内部横向滚动,不得撑宽整个应用窗口。
## 5. 信息层级
+1 -1
View File
@@ -1,6 +1,6 @@
{
"formatVersion": 1,
"agentVersion": "0.15.4",
"agentVersion": "0.15.6",
"protocol": {
"major": 2,
"minor": 0
+102 -8
View File
@@ -6,16 +6,17 @@
const { spawn } = require('node:child_process')
const { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs')
const { createServer } = require('node:http')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const root = resolve(__dirname, '..')
const runnerRoot = resolve(__dirname, '..')
const root = resolve(process.env.GB_PERF_ROOT || runnerRoot)
const concurrentMode = process.env.GB_PERF_MODE?.startsWith('C')
// 11434 is the fresh-profile default endpoint. Another GoodBuddy (or Ollama)
// may already use it; GB_PERF_MODEL_PORT moves the fake model and points the
// isolated profile at it through the GOODBUDDY_MODEL_* environment variables.
const defaultModelPort = 11434
const modelPort = Number(process.env.GB_PERF_MODEL_PORT || defaultModelPort)
const totalTimeoutMs = Number(process.env.GB_PERF_TIMEOUT_MS || 600_000)
const totalTimeoutMs = Number(process.env.GB_PERF_TIMEOUT_MS || (process.env.GB_PERF_MODE === 'C06' ? 900_000 : 600_000))
const keepProfile = process.env.GB_PERF_KEEP_PROFILE === '1'
const streamBytes = Number(process.env.GB_PERF_STREAM_BYTES || 40_000)
const chunkChars = Number(process.env.GB_PERF_CHUNK_CHARS || 12)
@@ -55,6 +56,11 @@ function startModelServer(ledger) {
response.end(JSON.stringify({ object: 'list', data: [{ id: 'qwen3', object: 'model' }] }))
return
}
if (request.method === 'GET' && request.url === '/c03-knowledge.txt') {
response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
response.end('C03 deterministic knowledge source\n'.repeat(80))
return
}
if (request.method !== 'POST' || !request.url?.endsWith('/chat/completions')) {
response.writeHead(404).end()
return
@@ -67,6 +73,75 @@ function startModelServer(ledger) {
const messages = Array.isArray(parsed.messages) ? parsed.messages : []
const last = messages.at(-1)
const lastText = typeof last?.content === 'string' ? last.content : JSON.stringify(last?.content ?? '')
if (lastText.includes('OUTPUT CONTRACT:') && lastText.includes('BOUNDED EVIDENCE:')) {
const evidenceText = lastText.split('BOUNDED EVIDENCE:\n\n')[1]?.split('\n\nReturn only JSON.')[0]
let sourceId = 'missing-source'
try { sourceId = JSON.parse(evidenceText)?.[0]?.id || sourceId } catch { /* invalid fixture prompt is rejected by the production path */ }
const navigation = lastText.includes('navigation summaries')
const output = JSON.stringify({
summary: 'Deterministic concurrent acceptance review', changeDigest: 'fixture', openItems: [],
events: navigation ? [] : [{ title: 'Acceptance event', description: 'Deterministic review event', occurredAt: '2026-10-07T00:00:00.000Z', eventType: 'milestone', entityIds: ['acceptance'], sourceReferenceIds: [sourceId] }],
entities: navigation ? [] : [{ id: 'acceptance', label: 'Acceptance', description: 'Deterministic fixture', sourceReferenceIds: [sourceId] }],
entityChanges: [], relations: []
})
entry.kind = 'supervision-deterministic'
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
const chunk = payload => response.write(`data: ${JSON.stringify(payload)}\n\n`)
chunk({ id: 'supervision', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { role: 'assistant', content: output }, finish_reason: null }] })
chunk({ id: 'supervision', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })
response.end('data: [DONE]\n\n')
entry.finishedAt = Date.now()
return
}
if (concurrentMode && lastText.includes('gb-fault:hang')) {
entry.kind = 'fault-hung'
entry.identity = 'fault-hung'
entry.stream = true
entry.aborted = false
// Keep the production request in flight until the UI cancels it.
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
const timer = setTimeout(() => {
if (!response.destroyed) response.end('data: [DONE]\n\n')
}, 120_000)
response.on('close', () => { clearTimeout(timer); entry.aborted = !response.writableFinished; entry.finishedAt = Date.now() })
return
}
if (concurrentMode && messages.some(message => typeof message.content === 'string' && message.content.includes('gb-concurrent:'))) {
const marker = messages.map(message => typeof message.content === 'string' ? message.content : '').join('\n').match(/gb-concurrent:(parent|child):([a-zA-Z0-9-]+)/)
const child = marker?.[1] === 'child'
const toolsDone = messages.some(message => message.role === 'tool')
entry.kind = child ? 'concurrent-child' : 'concurrent-parent'
entry.identity = marker?.[2]
entry.stream = parsed.stream === true
const durationMs = child ? 12_000 : toolsDone ? 1_000 : 12_000
const count = Math.ceil(durationMs / 16)
const calls = !child && !toolsDone ? [0, 1].map(index => ({ index, id: `child-${marker?.[2]}-${index}`, type: 'function', function: { name: 'subagent_delegate', arguments: JSON.stringify({ task: `gb-concurrent:child:${marker?.[2]}-${index} deterministic output` }) } })) : []
entry.offeredFragments = count
entry.offeredToolCalls = calls.length
entry.chunks = 0
entry.bytes = 0
entry.latenessMs = []
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
const send = (delta, finish_reason = null) => response.write(`data: ${JSON.stringify({ id: entry.identity, object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta, finish_reason }] })}\n\n`)
const started = performance.now()
const tick = () => {
if (response.destroyed) { entry.aborted = true; entry.settledAt = Date.now(); return }
while (entry.chunks < count && performance.now() >= started + entry.chunks * 16) {
entry.latenessMs.push(performance.now() - started - entry.chunks * 16)
const text = `[${entry.identity}:${String(entry.chunks).padStart(5, '0')}] sample\n`
send({ content: text })
entry.bytes += Buffer.byteLength(text)
entry.chunks++
}
if (entry.chunks < count) { setTimeout(tick, 4); return }
if (calls.length) send({ tool_calls: calls })
send({}, calls.length ? 'tool_calls' : 'stop')
response.end('data: [DONE]\n\n')
entry.finishedAt = Date.now()
}
tick()
return
}
const benchmark = lastText.includes('perf-stream')
entry.kind = benchmark ? 'benchmark-stream' : 'auxiliary'
entry.stream = parsed.stream === true
@@ -153,10 +228,27 @@ function printSummary(report) {
}
async function main() {
const taskDirectory = join(runnerRoot, 'temp', 'goodbuddy-concurrent-upgrade')
mkdirSync(taskDirectory, { recursive: true })
if (process.argv.includes('--launch')) {
const { openSync, closeSync } = require('node:fs')
const output = resolve(process.env.GB_PERF_OUTPUT || join(taskDirectory, `run-${Date.now()}`))
mkdirSync(output, { recursive: true })
const log = openSync(join(output, 'execution.log'), 'w')
const child = spawn(process.execPath, [__filename], {
detached: true, stdio: ['ignore', log, log],
env: { ...process.env, GB_PERF_OUTPUT: output }
})
closeSync(log)
writeFileSync(join(output, 'supervisor.json'), JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString(), root, mode: process.env.GB_PERF_MODE, timeoutMs: totalTimeoutMs }))
child.unref()
console.log(`Supervisor ${child.pid}; logs ${output}`)
return
}
const required = ['out/main/index.js', 'out/preload/index.cjs', 'out/renderer/index.html']
const missing = required.filter(file => !existsSync(join(root, file)))
if (missing.length) throw new Error(`Missing production build (${missing.join(', ')}); run npm run build:bundle first`)
const runDirectory = mkdtempSync(join(tmpdir(), 'goodbuddy-app-perf-'))
const runDirectory = mkdtempSync(join(taskDirectory, 'profile-'))
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
mkdirSync(outputDirectory, { recursive: true })
const ledger = []
@@ -166,7 +258,8 @@ async function main() {
} catch (error) {
throw new Error(`Cannot listen on 127.0.0.1:${modelPort} for the fake model (${error.code ?? error.message}); stop the local service using that port, or set GB_PERF_MODEL_PORT to a free port`, { cause: error })
}
const environment = { ...process.env, GB_PERF_DIRECTORY: runDirectory, GB_PERF_ARTIFACTS: outputDirectory, GB_PERF_ROOT: root }
const environment = { ...process.env, GB_PERF_DIRECTORY: runDirectory, GB_PERF_ARTIFACTS: outputDirectory, GB_PERF_ROOT: root, TEMP: runDirectory, TMP: runDirectory, TMPDIR: runDirectory }
if (concurrentMode) environment.GB_PERF_DRIVER_TIMEOUT_MS = String(totalTimeoutMs - 30_000)
delete environment.ELECTRON_RUN_AS_NODE
delete environment.ELECTRON_RENDERER_URL
for (const key of ['GOODBUDDY_MODEL_API_KEY', 'GOODBUDDY_MODEL_BASE_URL', 'GOODBUDDY_MODEL_NAME', 'GOODBUDDY_BIGTOKEN_API_KEY', 'GOODBUDDY_BIGTOKEN_BASE_URL', 'GOODBUDDY_BIGTOKEN_MODEL']) delete environment[key]
@@ -179,7 +272,8 @@ async function main() {
environment.GOODBUDDY_WORKSPACE = join(runDirectory, 'workspace')
mkdirSync(environment.GOODBUDDY_WORKSPACE, { recursive: true })
const started = Date.now()
const child = spawn(require('electron'), [join(root, 'tests', 'support', 'app-perf-driver.mjs')], { cwd: root, env: environment, stdio: 'inherit', windowsHide: false })
const child = spawn(require('electron'), [join(runnerRoot, 'tests', 'support', 'app-perf-driver.mjs')], { cwd: root, env: environment, stdio: 'inherit', windowsHide: false })
writeFileSync(join(outputDirectory, 'owned-processes.json'), JSON.stringify({ supervisorPid: process.pid, electronPid: child.pid, root, runDirectory, startedAt: new Date().toISOString() }))
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
@@ -200,13 +294,13 @@ async function main() {
total: ledger.length,
benchmarkStreams: ledger.filter(entry => entry.kind === 'benchmark-stream').length,
auxiliary: ledger.filter(entry => entry.kind === 'auxiliary').length,
ledger: ledger.map(({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt }) => ({ method, path, kind, stream, chunks, bytes, aborted, durationMs: finishedAt ? finishedAt - startedAt : null }))
ledger: ledger.map(({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt, identity, offeredFragments, offeredToolCalls, latenessMs }) => ({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt, identity, offeredFragments, offeredToolCalls, latenessMs, durationMs: finishedAt ? finishedAt - startedAt : null }))
}
if (timedOut || exit.code !== 0) report.status = 'failed'
writeFileSync(resultPath, JSON.stringify(report, null, 2))
printSummary(report)
console.log(`\nreport: ${resultPath}`)
if (!keepProfile) rmSync(join(runDirectory, 'profile'), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
if (!keepProfile) rmSync(runDirectory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
process.exitCode = String(report.status).startsWith('passed') ? 0 : 1
// PERF-17: GB_PERF_CHECK=1 gates the run on build/perf-thresholds.json
// (exit 1 on a regression, 2 when the report cannot be judged, e.g. throttled).
+25 -13
View File
@@ -1,12 +1,12 @@
// Knowledge retrieval benchmark (PERF-05). Bundles the production
// Owner-local knowledge/read-worker microbenchmark (PERF-05), not a Main IPC probe.
// Bundles the production
// KnowledgeDatabase with esbuild, seeds synthetic libraries of increasing size
// in a temporary SQLite file and times the synchronous search calls that the
// Electron Main process runs inline during retrieval. Because these calls are
// synchronous, their duration is the time Main is blocked per query.
// storage owner runs during retrieval. Synchronous duration measures this harness's
// event loop, not the Electron Main process of the application.
const { buildSync } = require('esbuild')
const { createHash } = require('node:crypto')
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
@@ -31,12 +31,16 @@ const dimensions = Number(process.env.GB_KPERF_DIMENSIONS || 384)
const chunksPerDocument = 100
const queries = Number(process.env.GB_KPERF_QUERIES || 15)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-knowledge-perf-'))
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
mkdirSync(temporaryRoot, { recursive: true })
const directory = mkdtempSync(join(temporaryRoot, 'knowledge-perf-'))
const bundle = join(directory, 'knowledge-database.cjs')
const workerBundle = join(directory, 'readonly-query-worker.cjs')
const readerBundle = join(directory, 'readonly-query-reader.cjs')
for (const [entry, outfile] of [
['src/main/knowledge/knowledge-database.ts', bundle],
['src/main/readonly-query-worker.ts', workerBundle]
['src/main/readonly-query-worker.ts', workerBundle],
['src/main/readonly-query-reader.ts', readerBundle]
]) {
buildSync({
entryPoints: [join(root, entry)],
@@ -51,6 +55,8 @@ for (const [entry, outfile] of [
})
}
const { KnowledgeDatabase } = require(bundle)
const { ReadonlyQueryReader } = require(readerBundle)
const ownedDatabases = new Set(), readers = new Set()
// Deterministic pseudo-random generator so runs are comparable.
let seed = 42
@@ -76,7 +82,7 @@ function time(body) {
}
const round = value => Math.round(value * 10) / 10
// Main event-loop delay while `queries` searches run back to back. A sync
// Harness event-loop delay while `queries` searches run back to back. A sync
// search blocks the loop for its whole duration; a worker search should not.
async function loopDelay(body) {
const histogram = monitorEventLoopDelay({ resolution: 5 })
@@ -103,6 +109,7 @@ async function main() {
for (const size of sizes) {
const path = join(directory, `knowledge-${size}.sqlite`)
const database = new KnowledgeDatabase(path)
ownedDatabases.add(database)
database.initialize()
const library = database.createKnowledgeBase({ name: `Perf ${size}`, storageMode: 'reference' })
const sqlite = database.database
@@ -119,6 +126,7 @@ async function main() {
// Reopen so timings start from a fresh connection, like an app restart.
const reopened = new KnowledgeDatabase(path)
ownedDatabases.add(reopened)
reopened.initialize()
const query = vector()
const entry = {
@@ -129,20 +137,24 @@ async function main() {
vectorMs: time(() => reopened.vectorSearch({ knowledgeBaseId: library.id, provider: 'perf', model: 'perf-model', vector: query, limit: 40 })),
hybridMs: time(() => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
}
// Main event-loop delay: synchronous search on Main vs the readonly worker.
// Harness loop delay: owner-local synchronous search vs its explicit read worker.
entry.mainLoopDelaySyncMs = await loopDelay(async () => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
reopened.enableReadonlyWorker(workerBundle)
entry.mainLoopDelayWorkerMs = await loopDelay(() => reopened.hybridSearchWithDiagnosticsAsync(hybridOptions(library.id, query)))
const reader = new ReadonlyQueryReader('knowledge', path, workerBundle)
readers.add(reader)
entry.mainLoopDelayWorkerMs = await loopDelay(() => reader.call('hybridSearchWithDiagnostics', [hybridOptions(library.id, query)]))
await reader.close()
reopened.close()
results.push(entry)
console.log(`[kperf] ${size} chunks: fts p95 ${entry.ftsMs.p95} ms, vector p95 ${entry.vectorMs.p95} ms, hybrid p95 ${entry.hybridMs.p95} ms (seed ${entry.seedSeconds} s)`)
console.log(`[kperf] ${size} chunks: Main loop delay max sync ${entry.mainLoopDelaySyncMs.max} ms vs worker ${entry.mainLoopDelayWorkerMs.max} ms (p99 ${entry.mainLoopDelaySyncMs.p99} / ${entry.mainLoopDelayWorkerMs.p99} ms; per query ${entry.mainLoopDelaySyncMs.perQueryMs} / ${entry.mainLoopDelayWorkerMs.perQueryMs} ms)`)
}
const output = process.env.GB_KPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-knowledge-retrieval-benchmark', node: process.version, results }, null, 2))
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-knowledge-retrieval-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
console.table(results.map(r => ({ chunks: r.chunks, 'fts p50/p95': `${r.ftsMs.p50} / ${r.ftsMs.p95}`, 'vector p50/p95': `${r.vectorMs.p50} / ${r.vectorMs.p95}`, 'hybrid p50/p95': `${r.hybridMs.p50} / ${r.hybridMs.p95}`, 'loop max sync/worker': `${r.mainLoopDelaySyncMs.max} / ${r.mainLoopDelayWorkerMs.max}` })))
}
const hybridOptions = (knowledgeBaseId, vector) => ({ knowledgeBaseId, query: 'lighthouse budget', limit: 40, provider: 'perf', model: 'perf-model', vector, graphEnabled: false, candidateMultiplier: 1 })
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
await Promise.all([...readers].map(reader => reader.close()))
for (const database of ownedDatabases) database.close()
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+25 -14
View File
@@ -1,10 +1,11 @@
// Main-process database benchmark (PERF-15). Bundles the production
// Owner-local database/read-worker microbenchmark (PERF-15). Not a Main IPC latency probe.
// Bundles the production
// AssistantDatabase and readonly worker with esbuild, seeds a temporary
// assistant database (default 1,000 conversations / 20,000 messages and 24,000
// activity records) and measures, for the IPC paths the renderer hits often:
// - per-call time
// - Main event-loop delay while the calls run back to back
// for the previous synchronous path ("sync") and the current path ("after").
// - harness event-loop delay while the calls run back to back
// for owner-local SQL ("sync") and explicit read-worker dispatch ("after").
// conversations:list-summaries sync listConversationSummaries vs worker
// conversations:get sync getConversation vs worker
// activity-history:replace legacy replace (zod parse + BEGIN IMMEDIATE +
@@ -17,7 +18,6 @@
// GB_MPERF_CALLS, GB_MPERF_OUTPUT (JSON), GB_MPERF_NODE=1 (skip Electron).
const { buildSync } = require('esbuild')
const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
@@ -41,13 +41,17 @@ const messageCount = Number(process.env.GB_MPERF_MESSAGES || 20000)
const activityCount = Number(process.env.GB_MPERF_ACTIVITY || 24000)
const calls = Number(process.env.GB_MPERF_CALLS || 15)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-main-perf-'))
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
mkdirSync(temporaryRoot, { recursive: true })
const directory = mkdtempSync(join(temporaryRoot, 'main-perf-'))
const bundle = join(directory, 'assistant-database.cjs')
const workerBundle = join(directory, 'readonly-query-worker.cjs')
const schemaBundle = join(directory, 'assistant-contracts.cjs')
const readerBundle = join(directory, 'readonly-query-reader.cjs')
for (const [entry, outfile] of [
['src/main/assistant/assistant-database.ts', bundle],
['src/main/readonly-query-worker.ts', workerBundle],
['src/main/readonly-query-reader.ts', readerBundle],
['src/shared/assistant-contracts.ts', schemaBundle]
]) {
buildSync({
@@ -56,6 +60,8 @@ for (const [entry, outfile] of [
})
}
const { AssistantDatabase } = require(bundle)
const { ReadonlyQueryReader } = require(readerBundle)
let ownedDatabase, reader
const { activityHistorySnapshotSchema } = require(schemaBundle)
let seed = 42
@@ -70,7 +76,7 @@ const percentile = (values, p) => {
/**
* Runs `body` `calls` times (after one warm-up) and reports the per-call wall
* time and the Main event-loop delay histogram (p99/max) over the run.
* time and the harness event-loop delay histogram (p99/max) over the run.
*/
async function measure(body) {
await body(-1)
@@ -154,6 +160,7 @@ async function main() {
mkdirSync(workspace)
const path = join(directory, 'assistant.sqlite')
const seeding = new AssistantDatabase(path)
ownedDatabase = seeding
seeding.initialize(workspace)
const seedStarted = performance.now()
const projectId = seeding.listProjects()[0].id
@@ -186,6 +193,7 @@ async function main() {
seeding.close()
const database = new AssistantDatabase(path)
ownedDatabase = database
database.initialize(workspace)
const raw = database.database
const totalMessages = counts.reduce((sum, value) => sum + value, 0)
@@ -199,10 +207,10 @@ async function main() {
results.listSummariesSync = await measure(async () => database.listConversationSummaries(detailIds))
results.getLargestSync = await measure(async () => database.getConversation(largest))
results.getTypicalSync = await measure(async () => database.getConversation(typical))
database.enableReadonlyWorker(workerBundle)
results.listSummariesWorker = await measure(() => database.listConversationSummariesAsync(detailIds))
results.getLargestWorker = await measure(() => database.getConversationAsync(largest))
results.getTypicalWorker = await measure(() => database.getConversationAsync(typical))
reader = new ReadonlyQueryReader('assistant', path, workerBundle)
results.listSummariesWorker = await measure(() => reader.call('listConversationSummaries', [detailIds]))
results.getLargestWorker = await measure(() => reader.call('getConversation', [largest]))
results.getTypicalWorker = await measure(() => reader.call('getConversation', [typical]))
// The renderer sends a fresh structured clone of the whole list every save.
const unchanged = () => structuredClone({ records, legacyHistoryMayBeIncomplete: false })
@@ -236,11 +244,12 @@ async function main() {
results.activityFirstPageSync = await measure(async () => database.getActivityHistoryPage(firstPageRequest))
results.activitySummarySync = await measure(async () => database.getActivityHistorySummary(summaryRequest()))
results.activityStartupWorker = await measure(async () => {
const page = await database.getActivityHistoryPageAsync(firstPageRequest)
await database.getActivityHistorySummaryAsync({ conversationIds: [...new Set(page.records.map(record => record.conversationId))] })
const page = await reader.call('activityHistoryPage', [firstPageRequest])
await reader.call('activityHistorySummary', [{ conversationIds: [...new Set(page.records.map(record => record.conversationId))] }])
})
results.activityFullLoadBytes = JSON.stringify(database.getActivityHistory()).length
results.activityFirstPageBytes = JSON.stringify(database.getActivityHistoryPage(firstPageRequest)).length
await reader.close()
database.close()
console.log(`[mperf] seeded ${results.conversations} conversations / ${results.messages} messages (largest ${results.largestConversationMessages}), ${results.activityRecords} activity records in ${seedSeconds} s`)
@@ -264,9 +273,11 @@ async function main() {
})))
console.log(`[mperf] activity payload: full list ${results.activityFullLoadBytes} bytes, first page ${results.activityFirstPageBytes} bytes`)
const output = process.env.GB_MPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-database-benchmark', node: process.version, results }, null, 2))
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-database-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
await reader?.close()
ownedDatabase?.close()
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+20 -10
View File
@@ -1,8 +1,8 @@
// Main-process database WRITE benchmark on a real data directory (PERF-15).
// Owner-local database WRITE microbenchmark on a closed data copy (PERF-15).
// Copies assistant.sqlite (+ -wal/-shm) from GB_WPERF_DATA (for example the
// portable build's data folder) into a temporary directory - the source is
// never opened - then measures, with the production AssistantDatabase, the
// writes Main performs synchronously while a run streams:
// synchronous repository writes. This is not a production Main IPC latency probe:
// task-events:subagent replay of the largest real subagent progress stream
// (restored events, re-compacted on write, as live)
// task-events:tool replay of real tool events
@@ -10,14 +10,14 @@
// conversations:save the renderer's 500 ms local save (header + the
// streaming assistant message) for a real conversation
// activity:update one incremental activity change
// For each: per-call time and the longest Main event-loop block.
// For each: per-call time and the longest owner-local harness event-loop block.
// Event replays are paced at GB_WPERF_RATE events/s (default 270, the busiest
// second in real data; 0 = back to back).
// Env: GB_WPERF_DATA (required), GB_WPERF_EVENTS (default 2000),
// GB_WPERF_OUTPUT (JSON), GB_WPERF_KEEP=1 (keep the copy).
const { buildSync } = require('esbuild')
const { mkdtempSync, rmSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { Worker } = require('node:worker_threads')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { performance } = require('node:perf_hooks')
@@ -43,7 +43,10 @@ if (!source || !existsSync(join(source, 'assistant.sqlite'))) {
}
const eventLimit = Number(process.env.GB_WPERF_EVENTS || 2000)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-write-perf-'))
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
mkdirSync(temporaryRoot, { recursive: true })
const directory = mkdtempSync(join(temporaryRoot, 'write-perf-'))
let ownedDatabase, checkpoint, checkpointExit
const bundle = join(directory, 'assistant-database.cjs')
const progressBundle = join(directory, 'subagent-progress.cjs')
for (const [entry, outfile] of [
@@ -115,6 +118,7 @@ async function main() {
const workspace = join(directory, 'workspace')
mkdirSync(workspace)
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
ownedDatabase = database
const openStarted = performance.now()
database.initialize(workspace)
const raw = database.database
@@ -124,9 +128,12 @@ async function main() {
results.synchronous = raw.prepare('PRAGMA synchronous').get().synchronous
if (process.env.GB_WPERF_AUTOCHECKPOINT) raw.exec(`PRAGMA wal_autocheckpoint = ${process.env.GB_WPERF_AUTOCHECKPOINT}`)
// As in production: checkpoints run on a worker thread (GB_WPERF_CHECKPOINT_WORKER=0 disables).
if (process.env.GB_WPERF_CHECKPOINT_WORKER !== '0' && typeof database.enableWalCheckpointWorker === 'function') {
database.enableWalCheckpointWorker(join(directory, 'readonly-query-worker.cjs'))
await new Promise(resolve => setTimeout(resolve, 500))
if (process.env.GB_WPERF_CHECKPOINT_WORKER !== '0') {
checkpoint = new Worker(join(directory, 'readonly-query-worker.cjs'), {
workerData: { kind: 'checkpoint', databasePath: join(directory, 'assistant.sqlite'), intervalMs: 250 }
})
checkpointExit = new Promise(resolve => checkpoint.once('exit', resolve))
await new Promise((resolve, reject) => { checkpoint.once('message', resolve); checkpoint.once('error', reject) })
}
results.walAutocheckpoint = raw.prepare('PRAGMA wal_autocheckpoint').get().wal_autocheckpoint
const count = (sql, ...args) => Number(raw.prepare(sql).get(...args).n)
@@ -208,6 +215,7 @@ async function main() {
}
results.walBytes = existsSync(join(directory, 'assistant.sqlite-wal'))
? require('node:fs').statSync(join(directory, 'assistant.sqlite-wal')).size : 0
if (checkpoint) { checkpoint.postMessage({ type: 'close' }); await checkpointExit; checkpoint = undefined }
database.close()
console.log(`[wperf] ${results.taskEvents} task events, ${results.messages} messages, ${results.activityRecords} activity records; open ${results.openMs} ms`)
@@ -226,10 +234,12 @@ async function main() {
for (const [name, r] of rows) if (r.slowCalls.length) console.log(`[wperf] slow calls (index, ms) ${name}: ${JSON.stringify(r.slowCalls)}`)
console.log(`[wperf] synchronous=${results.synchronous} wal_autocheckpoint=${results.walAutocheckpoint} wal=${results.walBytes} B`)
const output = process.env.GB_WPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-write-benchmark', node: process.version, results }, null, 2))
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-write-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
if (checkpoint) { checkpoint.postMessage({ type: 'close' }); await checkpointExit }
ownedDatabase?.close()
if (process.env.GB_WPERF_KEEP === '1') console.log(`[wperf] kept ${directory}`)
else rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
@@ -0,0 +1,64 @@
# Assistant Storage Consumers
The S02 domain consumers use the typed port in
`src/main/assistant-storage-port.ts`. This implements the consumer side of
[Desktop Storage Foundation](./desktop-storage-foundation.md). Production
composition in `src/main/index.ts` supplies the client after storage readiness;
the consumer services do not construct an Assistant database.
## Main Integration
```ts
const storage = new DesktopStorageClient({
...paths,
onChanged: createAssistantStorageOnChanged({
onMagicNotesChanged,
onMagicTodosChanged,
onModelUsageChanged,
onExecutionStatsChanged
})
})
await storage.ready
const assistant = createAssistantStoragePort(storage)
```
`createAssistantStoragePort` binds only the explicit Assistant operation allowlist
and `supervisionContext`. Its return type is `AsyncAssistantStoragePort`: every
operation returns a Promise. There is no SQL, arbitrary property proxy, remote
callback or live repository API. `AssistantStoragePort` uses `Awaitable` results
so existing synchronous database fixtures can be injected into unit tests.
Domain change callbacks run in Main through `DesktopStorageClient.onChanged`.
The subagent service, native client coordinator, terminal manager, computer-control
audit sink, channel dedup/outbox, image service and Magic Notes IPC helpers accept
this port. Notes helpers retain sender validation, schema validation, source-message
checks and revision checks. Note analysis awaits the model-usage callback before
publishing its result. Channel and audit adapters return persistence completion to
their existing asynchronous callers.
Image service `initialize`, `persistUploads`, `getOperation`, `cancel`,
`cancelConversation`, `regenerate` and `dispose` return Promises. Await upload
persistence before submitting generation. Await cancellation and shutdown writes.
`onUsage` may return a Promise and must return the actual persistence operation.
Notification-only `onOperation` errors are reported without changing provider
outcomes; storage and usage-write failures reject instead of reporting durable
success. Shutdown reports retained persistence failures.
Subagent event and model-usage callbacks may return Promises. The service awaits
task creation, events and terminal status writes; shutdown also drains task
creation and queued cancellation writes that have not entered a scheduler slot.
Native client composition additionally forwards the storage-backed
`openModelCallLedger` factory to both browser and terminal clients.
Stop producers and await service disposal before closing `DesktopStorageClient`.
This consumer migration does not establish full desktop performance acceptance or
replace the separate Main/IPC composition and concurrent acceptance work.
## Validation
The 11 focused suites pass with 94 tests, including asynchronous image storage,
commit-before-notification, usage-write rejection, subagent shutdown during task
creation, channel delivery, audit persistence and IPC sender/schema checks.
Repository-wide type checking remains dependent on the concurrent Main/IPC,
Knowledge and runtime-storage integrations. No latency or full concurrent-desktop
acceptance claim is made by these unit and loopback tests.
@@ -0,0 +1,388 @@
# Desktop Storage And Concurrent Execution
## Decision and delivery
Updated 2026-10-05 against `2be57492`. The user selected one complete architecture
delivery for PERF-15/PERF-16 and the necessary performance guards. This document
supersedes the earlier proposal to leave full migration for a later discussion.
The integrated migration and acceptance are not complete. The implemented storage
foundation, production composition and remaining acceptance work are recorded in
[Storage Foundation Integration](./desktop-storage-foundation.md).
Local fixes in `b90ff50f` remain the measured starting point.
Deliver the integrated design in one candidate, with normal product workflows and
concurrent acceptance passing together. Internal implementation dependencies do
not create separate partial product deliveries. A worker-only prototype, an async
facade over Main SQL, or a dual-write transition does not meet completion criteria.
This document owns architecture and scope. [Performance principles](./performance-principles.md)
own normative budgets and concurrency rules; [concurrent acceptance](../quality/concurrent-desktop-acceptance.md)
owns reproducible workloads and evidence. The [roadmap](../roadmap/product-performance-experience-improvement-plan.md)
tracks status, and the [local repair record](../review/kiss-local-fixes-2026-10-05.md)
preserves earlier measurements. Migration completion requires new acceptance evidence.
## Requirements
| ID | Required outcome |
| --- | --- |
| SA-01 | Desktop Main does not open or query business SQLite, directly or through a helper. |
| SA-02 | Complete domain transactions execute under one write owner per database. |
| SA-03 | Multiple projects, conversations and subtasks progress without unrelated global waits. |
| SA-04 | Requests, event buffers, result payloads and caches have bounded lifetimes and capacity. |
| SA-05 | Persistence, ordering, cancellation, recovery and user interaction semantics are preserved. |
| SA-06 | Startup, process failure and shutdown have one tested ownership path. |
| SA-07 | Static guards and concurrent production-path tests enforce the architecture. |
| SA-08 | GoodBuddy eliminates unnecessary intermediate disk writes and duplicate full outputs; necessary temporary resources have explicit ownership and cleanup. |
## Responsibilities
```mermaid
flowchart TB
UI[Renderer: domain stores and visible views] <--> MAIN[Main: trusted IPC and lifecycle]
MAIN <--> STORAGE[Storage utilityProcess: domain operations and write owners]
STORAGE <--> READ[Existing bounded read workers]
MAIN <--> RUNTIME[Runtime hosts and compute workers]
MAIN <--> AGENT[Remote Agent]
```
- Renderer retains presentation, local interaction state and disposable visible-data
caches. Domain selectors subscribe narrowly. Hidden views retain required state,
not continuous full rendering or whole-history refreshes.
- Main retains windows, trusted sender/schema checks, OS APIs, credential decryption,
network/runtime orchestration and process lifecycle. SQL, large result assembly,
hashing and bulk attachment file work do not execute on its event loop.
- One desktop storage utility process owns the existing business database files and
exposes typed asynchronous domain operations. Keep synchronous SQLite inside this
dedicated environment where it simplifies transaction correctness.
- Reuse bounded read-only workers for expensive reads. They open only after upgrade,
read committed WAL snapshots, and do not run initialization or recovery. Storage
owns their lifecycle; a project or conversation does not create a new worker.
- Parsing, OCR, embedding and Runtime execution retain their existing hosts. Network
requests and model execution must not run inside storage transactions or hold the
storage write queue. This work does not relocate browser-dependent OCR engines.
- Remote Agent databases remain remote. Changes to desktop storage must preserve
remote replay, controller ownership and commit-before-ACK.
One write owner means exclusive business-write authority, not necessarily one OS
thread for all reads. Startup upgrades and existing maintenance/checkpoint workers
must be explicitly owned and excluded from competing business writes. Keep their
current bounded mechanisms where valid; do not create independent writers per domain.
## Ownership inventory
Inventory records current owners; destinations below are targets, not implementation facts.
| Surface | Current entry | Required destination |
| --- | --- | --- |
| `assistant.sqlite` | `src/main/assistant/assistant-database.ts`, constructed in Main | Storage service: conversations, tasks/events, activity, notes metadata, channels, supervision and timing persistence |
| `knowledge.sqlite` | `src/main/knowledge/knowledge-service.ts` and `knowledge-database.ts` | Storage service: schema, writes and external metadata; existing workers for costly reads |
| `conversation-attachments.sqlite` and referenced files | `src/main/conversation-attachment-storage.ts` | Storage service: references, adoption, reconciliation and bounded file IO |
| Document-result persistent lookup and note bodies | `document-result-storage.ts`, `magic-notes` storage | Async domain access to associated files; preserve original directories and reference semantics |
| Local remote-runtime binding DB | `src/main/remote-agent/managed-remote-execution-services.ts` | Storage service: transactional binding transitions and uniqueness |
| Native terminal / DS Web model-call ledgers | `native-terminal-client.ts`, `native-dsh-web-client.ts` instantiate `AgentModelCallLedger` | Storage service manages desktop ledger files under the existing client lifecycle; Main retains model/credential routing |
| Read, checkpoint and upgrade connections | `readonly-query-worker.ts`, `assistant-storage-worker.ts` | Service-owned steady-state reads/checkpoints; exclusive upgrade execution before readiness |
| Supervision manifest writer | `readonly-query-worker.ts` supervision mode and `SupervisionReviewStore` | Reuse scanning and manifest logic, but route its business mutations through the same write owner as publication; remove independent writer authority |
| Remote Agent journal, owners, prompts and ledger | `src/agent-daemon/daemon.ts`, `runtime-composition.ts` | Existing remote ownership, unchanged by desktop connection migration |
| Encrypted JSON settings and transient runtime caches | Existing settings/runtime services | Keep existing appropriate owners; async settings do not move merely to match a diagram |
An import scan must also find `openPrivateSqliteDatabase` and transitive constructors:
an implementation under `agent-daemon` can still open SQLite when called in desktop
Main. Do not weaken shared Agent code or relocate remote databases as a side effect.
Preserve existing database names, table formats, IDs, paths, scopes and durability
settings. Process relocation alone does not require a schema bump. Necessary schema
changes follow the [migration guide](../development/database-migrations.md).
## Existing work and consolidation
This inventory was checked against production callers on 2026-10-05. Reuse means
preserving working algorithms and contracts, not claiming their current transport,
cancellation or close semantics already meet the target. Implement the replacement
at the existing seam and remove superseded callers in the same delivery.
| Existing mechanism | Reuse or extend | Retire or avoid |
| --- | --- | --- |
| [ReadonlyQueryReader](../../src/main/readonly-query-reader.ts), [worker handlers](../../src/main/readonly-query-worker.ts) | Reuse request correlation, named operations, errors, lazy lifecycle and reader handlers; adapt the outer utility transport and strengthen typed operations, readiness, byte bounds and settlement-aware close | No second equivalent storage RPC client or admission queue; remove production infrastructure fallback through `readWithFallback`, not legitimate owner-local transaction reads |
| [Startup storage](../../src/main/assistant-storage-startup.ts), [upgrade worker](../../src/main/assistant-storage-worker.ts) | Preserve migration algorithms, progress, retry and cancellation; move SQL preflight into the storage-owned exclusive startup path | Remove Main SQL preflight and competing upgrade launch ownership; do not add a new migration framework |
| [Checkpoint and supervision connections](../../src/main/assistant/assistant-database.ts), [review store](../../src/main/assistant/supervision-review-store.ts) | Reuse PASSIVE checkpoint and manifest scan/revision algorithms; make checkpoint exit awaited and manifest writes owner-controlled | No second maintenance service or independent business writer hidden in a read-worker entry |
| [EmbeddingIndexRepository](../../src/main/knowledge/embedding-index-coordinator.ts), [binding store interface](../../src/main/agent/runtime-session-binding-store.ts), [activity repository](../../src/main/assistant/activity-history-repository.ts) | Reuse domain interfaces and transaction algorithms; adapt the desktop backing implementation and async callers | Do not add remote object proxies or duplicate repositories; Promise-returning methods are not proof of off-Main execution |
| [RemoteEventBatcher](../../src/main/remote-event-batcher.ts) | Extend existing batching with awaited persistence/flush and pending-plus-in-flight bounds; update recovery, background and interactive IPC consumers | No second durable event coalescer, receipt ledger or retry queue; no ACK or forwarding before commit |
| [AgentModelCallLedger](../../src/agent-daemon/agent-model-gateway.ts), [terminal client](../../src/main/agent/native-terminal-client.ts), [DS Web client](../../src/main/agent/native-dsh-web-client.ts) | Preserve schema, claim/deduplication and uncertain outcomes; desktop access becomes awaited storage operations | Do not relocate remote Agent ledgers; remove desktop concrete synchronous opens and early directory deletion |
| [Utility transport](../../src/main/knowledge/embedding-utility-transport.ts), [Harness readiness](../../src/main/agent/deepseek-harness-utility-launcher.ts), [shutdown helpers](../../src/main/shutdown.ts) | Reuse fork/entry/exit adapters and phased application shutdown; add storage readiness and drain at those seams | No new generic process supervisor, duplicate shutdown path or copied Harness streaming/version protocol |
| [Runtime controller](../../src/main/agent/runtime-controller.ts), [selected runtimes](../../src/main/agent/selected-runtime-manager.ts), [remote backend](../../src/agent-daemon/runtime-acp-backend.ts) | Keep request ownership, session reuse and retirement; inspect remaining global open/prepare/close waits with healthy peers | No second Runtime pool or global execution manager; the prior route-cleanup fix does not prove every global wait is gone |
| [Subagent scheduler](../../src/main/assistant/subagent-scheduler.ts), [supervision model pool](../../src/main/assistant/supervision-model-pool.ts), [review admission](../../src/main/assistant/supervisor-service.ts) | Retain task slots, review single-flight and per-model-call pool as distinct existing scopes; extend capacity/settlement tests where needed | Do not merge ordinary chat into the supervision pool or create another task state machine/provider scheduler |
The current ordinary reader removes a cancelled request from `pending` before worker
settlement, and `close()` provides no exit barrier. Supervision waits longer and can
substitute cancellation for a successful result. Extend these semantics to
represent actual execution and committed outcomes; do not copy them unchanged into
the storage client and call its capacity or shutdown complete.
Supervision's temporary scan table belongs to one SQLite connection. Retain the
bounded scan, transfer only bounded manifest data to its write owner, and preserve
initialization/resume invariants. Existing 200-row commits run in synchronous loops;
they do not themselves yield an execution turn. Yield at safe existing batch boundaries
without dividing final atomic publication or introducing a general scheduler.
Renderer consolidation stays within existing domains:
- [Conversation store](../../src/renderer/src/conversation-store.ts) and
[persistence](../../src/renderer/src/conversation-persistence.ts) already own ordered
deltas, save serialization, history requests and quit flushing. Extend those paths,
not a second `conversation-sync` queue. Its current React-aware integration is not
a reason to introduce another store library.
- [Activity sync](../../src/renderer/src/activity-sync.ts) owns paging, pending writes
and retry; [task sync](../../src/renderer/src/task-sync.ts) owns task/artifact refresh.
Tighten projections and retention there. A resolved flush that caught an error and
requeued data is not a durable success barrier; extend its explicit completion
semantics where dependent reads or shutdown require persistence.
- [Live messages](../../src/renderer/src/live-message-store.ts),
[public event buffering](../../src/main/agent-event-buffer.ts), and
[terminal batching](../../src/renderer/src/terminal-output-batcher.ts) serve distinct
display stages. Keep them separate from durable remote batching and avoid an
additional universal event bus or a second coalescing delay.
- Retain domain cache owners: [keep-alive](../../src/renderer/src/keep-alive-cache.ts),
[Markdown](../../src/renderer/src/markdown-render-cache.ts),
[channel status](../../src/renderer/src/channel-status-store.ts), and
[Supervisor scope reads](../../src/renderer/src/SupervisionStories.tsx).
Preserve pinned-pane exceptions and scope/revision guards; strengthen bounds at
their existing owners.
Runtime session behavior remains authoritative in the
[reuse design](../features/assistant-workbar/runtime-process-reuse-technical-design.md).
[Task timing](../features/task-and-job/technical-design.md) and
[supervision model scheduling](../features/conversation-supervision/technical-design.md)
retain their existing semantics. Ending an elapsed-time segment on abort does not
release an execution slot before cleanup; do not unify these clocks or restore
historical timing scans/statistics workers during this migration.
Completion includes a removal inventory tied to the table above: old Main opens,
external live-store factories, production sync fallbacks, obsolete transport callers
and independent business writers have no production references. Remove only the
superseded implementations, not valid in-owner synchronous algorithms, remote stores,
released-data compatibility or historical test evidence. No permanent dual path or
unused compatibility wrapper is part of the delivery.
## Operation boundary
Reuse repository algorithms behind domain operations: save conversation changes,
commit remote events, publish a review, adopt attachment references, or transition
a runtime binding. Public renderer IPC can keep its current shape while Main awaits
the storage client. No generic SQL endpoint or function serialization is introduced.
Existing transaction callbacks and returned live repository objects stay inside the
owner. Replace their external use with typed inputs/results. In particular,
`readSnapshot`, Knowledge publication callbacks and Supervisor store factories must
not be transported or accept an asynchronous callback that outlives a sync transaction.
The channel extends the request/handler and utility-adapter seams identified above,
correlates requests/results and carries named operations,
serializable arguments, cancellation and readiness/failure. Reuse installed Electron
transport patterns; do not add a general RPC dependency, persisted transport sequence,
protocol negotiation or a service discovery layer for this bundled child process.
Validate actual transport behavior for buffers, Maps and errors; use DTOs. Live classes,
subscriptions and handles remain with their owners. Errors retain usable codes and causes
without credentials or user content in diagnostic logs.
Queries return the requested projection. Lists use summaries/pages; source bodies,
tool outputs and attachments load through detail paths. Large results must not be
JSON-assembled in Main or copied through several caches. Existing source records are
not truncated to satisfy a latency check. A read transaction ends before posting its
reply so a slow consumer cannot keep a WAL snapshot open.
## Concurrency and ordering
Keep ordering at the smallest required scope. A conversation's persisted sequence
and a database's commits remain ordered; independent model waits, sessions and
read requests do not share a global long-running lock. Runtime process reuse retains
per-binding/session/operation identity; cancelling one request must not stop its peers.
Use existing finite concurrency and batch mechanisms before adding scheduling code.
Apply capacity to queued bytes as well as operation count where payload size varies.
When a queue fills, wait or report the existing actionable busy/error state; never
silently discard user messages, tool results or acknowledged events. Obsolete read
refreshes and display progress can coalesce; persisted evidence cannot. Preserve one
bounded batch of work before yielding so other admitted producers can progress;
change scheduling only where concurrent measurements demonstrate starvation.
Heavy reads use the read workers and leave necessary writes able to progress. UI/control
requests must not wait behind model networking, import parsing or route cleanup.
Control processing must not depend on acquiring another execution slot. This is a
resource-boundary rule, not a mandate for a new global priority scheduler.
For long publications, compute immutable data before the write transaction when
possible, then validate current database preconditions inside the transaction and
commit the complete result/checkpoint atomically. Do not await models, filesystem
callbacks or IPC while holding a write transaction. An atomic publication cannot be
split into externally visible partial commits to improve timing. Worker relocation
does not eliminate the single-writer bottleneck: acceptance measures queue wait and
commit time, as well as Main responsiveness.
## Persistence and cancellation
| Operation | Required completion meaning |
| --- | --- |
| Cached UI preference/read | Synchronous memory access is allowed; cache ownership and invalidation are explicit |
| Existing coalesced local saves | Preserve current flush semantics and expose the existing flush barrier to dependent reads/exit |
| Remote event batch | Persist event/checkpoint under existing durability, then notify/ACK; enqueueing is not commit |
| Channel outbox / runtime ledger | Preserve duplicate-dispatch prevention and uncertain-outcome handling; no blind retry after lost replies |
| Review publication | Facts, result status and checkpoints commit together or roll back together |
Abort removes queued work before dispatch. In-flight work reaches a defined safe
point and reports its actual outcome; capacity is released only after settlement or
confirmed host exit. Caller cancellation, operation termination and resource release
are distinct observations, not three new persistent product states.
`worker_threads` SharedArrayBuffer cancellation must not be assumed to cross
`utilityProcess` IPC. Reuse it only within verified thread boundaries. A message
cannot interrupt synchronous SQL while the receiver is blocked. Keep SQL/transaction
units bounded, check cancellation between safe units, and report a completed commit
as committed even if cancellation arrived too late. Do not terminate a shared storage
process to cancel one request or promise instantaneous rollback of a committed write.
Filesystem adoption and SQL references are not automatically one atomic transaction.
Preserve existing staging, adoption and reconciliation order with async file APIs.
Do not delete originals or release protected in-flight files early; do not introduce
a cross-database transaction engine or recovery journal.
## Lifecycle and failure
Main starts and supervises one storage process using existing utility-process
patterns. Process spawn is not readiness: required migrations, connection opening
and startup recovery must finish before storage-dependent operations are accepted.
Show the existing startup/upgrade progress and retry UI. Render the application shell
when safe without marking unavailable data as an empty successful result.
After process loss, reject affected pending requests with bounded diagnostics, stop
admitting writes, confirm exit and close transport resources before replacement.
The existing user retry/reopen lifecycle can re-establish storage readiness. Read-only
work can be repeated when requested. For writes without a response, consult existing
task/event/result identities and deduplication before claiming failure or retrying;
model dispatch marked uncertain must not be sent again automatically. No Main SQL
fallback, silent empty database, second durable receipt ledger or unbounded restart loop.
Shutdown stops producers and new admission, cancels unnecessary reads, drains required
writes, closes readers/checkpoints/connections, and observes storage process exit.
Extend the existing phased shutdown and deadline, including explicit storage failure
reporting; existing settled-error helpers alone do not prove a successful drain. Native client ledger
directories are removed only after their connection is closed. Upgrade/maintenance
requires exclusive ownership before changing files, following the existing maintenance
path; it cannot open an extra Main business connection.
## Output and temporary resource ownership
This work is included in the same architecture delivery. The [source investigation](./runtime-storage.md#output-and-temporary-file-investigation)
records current producers and persistence gaps. Scope is GoodBuddy-created material;
third-party MCP/Runtime internals and user workspaces are not cleanup targets.
Decide content ownership before choosing a temporary directory. Ordinary requests,
responses, concatenation and short outputs stay in bounded memory. Where an existing
record or artifact already contains complete content, pass its reference and read pages
from that owner. Do not write a temporary copy merely to cross IPC or call MCP.
Current tool events often contain only previews. Preserve full paging access by adapting
the existing `PagedOutputStore` interface to the storage/attachment owner: output that
must outlive bounded in-memory capture has one backing content object, with events
carrying previews and its reference. Write directly to that backing object; do not spool
to Temp and later copy the same bytes to another full-output store. Reuse existing file
adoption/reference/release operations and paged reads, without a second output database,
generic blob service or durable transport journal. Final answers, previews and full
stdout/stderr remain distinct data.
The storage object's retention follows its consumer. Call-only output is released when
its existing owner no longer needs paging; content referenced by persisted history is
released through that history's existing reference cleanup. Being stored by the storage
service does not make every intermediate output permanent. Keep small call-only output
in memory and preserve existing model limits. Avoid adding whole-output copies to events
or retaining all concurrent streams in an unbounded buffer. Size alone does not justify
an additional temporary copy.
The replacement must specify the complete-content reference and byte cursor through
process, search, subagent and `output_read` paths before removing the old file backend.
Inspect remote mapping separately: it does not use the local process spool and cannot
recover omitted bytes from already-pruned Agent events. Preserve supported remote
content/ACK semantics; this work does not promise new full-history capture from third-party
runtimes. Remove the old spool creation and synchronous Main paging only once the shared
replacement preserves the existing full-output contract.
### Necessary temporary material
External CLI configuration, plugin modules and atomic replacement/install staging may
still require files. Reuse environment/in-memory configuration when already supported;
otherwise keep the actual external contract. Atomic sibling files and installer staging
stay on their required filesystem under their existing owner, not a forced central root.
For disposable runtime launch material, pass a root from application initialization
through existing constructors; create it lazily:
```text
<userData>/temp/goodbuddy-runtime/<appVersion>/<runId>/
goodbuddy-runtime-launch/
```
Add purpose directories only for evidenced necessary files. Do not precreate process,
search or subagent output trees. User-visible disposable roots retain `goodbuddy-`
prefixes. Tests use `<repository>/temp/goodbuddy-<task>-<runId>/`; remote resources use
GoodBuddy-owned prefixed roots on the Host. Do not modify global `TEMP` or relocate
third-party histories/caches through an indiscriminate environment override.
The creator removes partial creations on failure. Existing service shutdown closes
children/handles and then releases its resources. Startup, including the next upgrade,
reclaims obsolete owned run directories after the existing single-instance check and
verification that no owning child remains active. Version/run IDs identify candidates;
they do not establish inactivity. Reuse process ownership checks, with only the minimal
owner information needed to identify surviving children; do not create a lease service.
Cleanup runs in bounded background work and cannot delay the first screen with a full
filesystem scan. Failed deletion leaves the known directory available to retry.
Legacy OS-Temp prefixes require an explicit list of layouts, creator and inactivity
evidence. Reclaim confirmed obsolete instances, preserve ambiguous/live directories,
and report the remaining scope. Never delete arbitrary `goodbuddy-*`, `.tmp` or `.partial`
paths by age. Existing installer/reference reconciliation retains its own roots and
rules; no global garbage collector, cleanup ledger or user-data migration is introduced.
## KISS constraints
- Keep Electron, TypeScript, SQLite and existing provider/runtime integrations.
- Add the one storage utility process, not processes per project/session/subtask.
- Reuse reader, checkpoint, parser, batching and lifecycle mechanisms where appropriate.
- Keep credentials/OS integration in Main; storage receives only required business data.
- Do not add general schedulers, distributed transactions, durable RPC journals,
compatibility readers for unshipped intermediates, new approval gates or user modes.
- Synchronous cache reads and pure calculations need not become RPC calls.
- The runtime host, database driver and UI framework are not replaced as incidental work.
## VS Code reference
VS Code demonstrates service separation, asynchronous execution, lazy activation,
cached state and coordinated close. Its application/profile/workspace storage is
owned by Electron Main, uses async `@vscode/sqlite3`, and batches cached key/value
writes. GoodBuddy retains `DatabaseSync` in a dedicated execution environment because
its relational history, graph queries and durable event acknowledgements differ.
Do not copy full-state memory loading or empty-store recovery into business history.
Sources reviewed on 2026-10-05, upstream main; no same-machine benchmark is claimed:
- [Connection ownership](https://github.com/microsoft/vscode/blob/main/src/vs/platform/storage/electron-main/storageMain.ts)
- [SQLite backend](https://github.com/microsoft/vscode/blob/main/src/vs/base/parts/storage/node/storage.ts)
- [Cache and flush lifecycle](https://github.com/microsoft/vscode/blob/main/src/vs/base/parts/storage/common/storage.ts)
- [Extension hosts](https://code.visualstudio.com/api/advanced-topics/extension-host)
## One-delivery completion
SA-01 through SA-08 must pass together under the [acceptance matrix](../quality/concurrent-desktop-acceptance.md).
All listed desktop owners, normal/background/error/cancel/exit callers, and native
client ledger paths must use the final boundary. Remove the superseded Main openings,
direct storage access and synchronous fallbacks, including helper-based bypasses.
No feature-disable workaround or loss of user interaction counts as completion.
Run production UI-to-storage tests, existing full validators, old-data integrity and
concurrent before/after measurements; validate affected desktop-to-Agent behavior on
the shared Linux x64 Host. Native platform CI verifies process startup, SQLite and
packaged entry/resource paths. Follow the release runbook for production builds and
packaged-app checks, including measurement runs.
The acceptance record must report latency, throughput, backlog and resource cleanup,
including failures. A responsive UI with stalled background commits is not success.
No target implementation, new performance baseline or migration completion is claimed
by this documentation update.
+129
View File
@@ -0,0 +1,129 @@
# Desktop File Storage Integration (S04)
Implemented domain operations, async consumers and production composition on
2026-10-05. The storage owner is registered by the foundation and the current Main
composition passes its file facades to ContextManager, document parsing and model
output. This does not claim that the full application has passed concurrent or
packaged acceptance.
The governing constraints are [performance principles](./performance-principles.md)
and [storage ownership](./desktop-storage-direction.md).
## Foundation API
`src/main/desktop-storage-files.ts` exports the complete integration surface:
- `DesktopFileStorageDomains`: intersect into `DesktopStorageDomains`. The domains
are `attachments` and `documentResults`.
- `attachmentStorageMethods` and `documentResultStorageMethods`: explicit allowed
operations. `attachments.reconcile` is an additional owner composite, with
arguments `[startup?, protectedIds?]`; its callback never crosses IPC.
- `openDesktopStorageFiles(userDataRoot, hasOwner, persistentResultLookup?)`:
construct once inside the existing utility process, after assistant and knowledge
databases open. Pass the existing userData root, not the assistant DB directory.
Add that root to foundation options and forward it in the client's open message.
- `DesktopStorageFilesOwner.dispatch(domain, method, args, signal)`: dispatch the
two domains here from the existing owner's dispatcher. It normalizes transported
original bytes, checks operation allowlists, and supplies cancellation to result
saves. Keep the existing transport admission and result limits.
- Before readiness, run `files.attachments.reconcile(hasOwner, true)`. The callback
uses the already-open assistant owner. It checks released references, marks
interrupted parsing, removes stale call-only output refs, and reclaims owned
orphan UUID directories. Do not run it concurrently with file producers.
- After draining admitted requests, `await files.close()` before closing assistant
and knowledge. Close drops call-only output refs, preserves history refs, removes
temporary document results, and closes the existing attachment database.
No new database, RPC transport, SQL endpoint, or process is introduced. Existing
attachment reference transactions and schema 1 remain in place. New relative paths
use slash separators; reads and reconciliation also accept released Windows paths.
## Main And Knowledge
After `storage.ready`, production composition uses `createDesktopStorageFiles(storage)` to obtain
`{ attachments, results, backingStore }`. Pass attachments to `ContextManager` and
results to `DocumentParsingService` and Knowledge's result-access dependency.
Do not also construct `ConversationAttachmentStorage` or `DocumentResultStorage`
in Main. Facades only call the existing storage client; importing them does not open
SQLite. Their result `close()` is intentionally inert: shared result shutdown belongs
to `DesktopStorageFilesOwner.close()`, after all producers drain.
Context operations that now require await: `storePastedImage`, `enrichRequest`,
`remove`, `serializeForQueue`, `restoreFromQueue`, `getDraft`, `hasImageInputs`,
`validateForSend`, `saveDraft`, `sendOriginal`, and `cancelUnavailableImport`.
Previously async import/reparse/image operations now await every storage operation.
Conversation/target validators may return Promises. Memory-only `clear` and
`activeContextIds` remain synchronous. `ContextManager.dispose()` cancels and drains
the active import before collecting unreferenced assets. Stop other context producers
and await their operations before disposing. Parsing-service disposal drains all
diagnostics, including those without an operation ID.
IPC must await attachment/result calls, including `results.original(id)` before
`shell.openPath`, and array predicates/maps after `get`, `draft`, `pendingParsing`,
and `getDraft`. Replace callback-based Main reconciliation with
`await attachments.reconcile(false, contextManager.activeContextIds())`.
Knowledge must accept `DocumentResultStorageAccess`, await `detach`, `original`,
and `isTemporaryResult`, and remove Main `setPersistentLookup` registration. Supply
that lookup to `openDesktopStorageFiles` instead: use owner-local
`knowledge.getDocumentByParsedResultId(id)` and the existing
`knowledge-assets/<libraryId>/<documentId>/<resultId>` directory and source location.
Keep Knowledge's publish/move/detach/release order. Result release never deletes an
external source original. No user-data directory migration is needed.
Project deletion supplies the project's conversation IDs captured before assistant
deletion to `attachments.deleteProject(conversationIds, protectedIds?)`. It removes
their refs in one attachment transaction, then collects only unreferenced assets.
Shared assets remain available to other conversations. Call this after the assistant
deletion commits; startup reconciliation covers an interrupted cleanup. Use
`deleteConversation` for single conversations and reconciliation for full clearing.
## Full Output
Pass the returned `backingStore` to each existing `PagedOutputStore`. It implements
`create/append/finish/read/release` directly in the existing attachment owner.
An output handle's UUID identifies its attachment row throughout capture, promotion,
history retention, restart, and deletion. Capture writes `original.txt` directly in
the owned attachment directory. Promotion renames that directory; no temporary full
copy or second backing is created. `request.json` contains only the descriptor.
Appends are bounded to 64 KiB; backing reads allow 32 KiB plus UTF-8 lookahead.
Small outputs remain in the existing bounded in-memory capture.
For history retention, await
`attachments.outputAdopt(callOwnerId, handle, conversationId, kind, referenceId)`
before the runtime releases its call reference. It returns the resource ID and adds
the reference without replacing the owner's existing attachments. Persist the
complete-output handle/reference in the owning event/message path; a preview alone
is not full output. Retain the call until required history adoption/persistence has
settled. Failed history publication uses existing release/reconciliation, not a
second durable operation log. Incomplete captures cannot be adopted.
`release` drops only the matching `output-call` ref. Persisted history can page after
call disposal or restart with
`attachments.outputRead(conversationId, handle, cursor, length)`, reading the same
bytes; it must use the owning conversation ID. History deletion uses the existing
message/queue/conversation reference cleanup. Runtime cancellation must await writer
abort/release. Startup removes stale call refs while retaining valid history refs.
The remote Agent path does not use this desktop backing and is unchanged.
## Validation Boundary
The focused suites exercise real SQLite/files through owner operations and async
facades, using structured-cloned arguments/results to cover Buffer-to-Uint8Array
conversion. They cover complete UTF-8 paging, one backing file, adoption, release,
abort, restart, project deletion with shared history, existing path separators,
temporary-result cleanup, failed adoption retry, async restore, and shutdown drain.
New result writes and copies omit `parsed.md`; old manifests and directories remain
readable. Result image writes settle sequentially before failure cleanup.
Local Windows validation: six focused suites passed (83 tests), covering this owner,
attachment references, ContextManager, parsing service, paged output, and HTTP OCR
integration. Scoped ESLint and diff whitespace checks passed. Node type checking
reported no diagnostics in S04 source/tests; the complete checkout still has pending
integration errors in Main/IPC and concurrent domain migrations.
These tests do not replace the foundation's utility-process tests or UI acceptance.
The real utility-process coverage for these domains is present in the focused
storage fixtures. Concurrent desktop acceptance and packaged validation remain
outstanding. No latency or throughput improvement is claimed from these focused
tests. Test resources live under `temp/goodbuddy-files-upgrade` and individual
fixtures remove their files.
@@ -0,0 +1,308 @@
# Desktop Storage Foundation Integration
Implemented 2026-10-05. This is the storage foundation for
[the desktop storage direction](./desktop-storage-direction.md). The production Main
composition now opens one `DesktopStorageClient` and supplies its typed facades to
the migrated domains. Full application acceptance is still incomplete. Do not open
the existing Main databases alongside the owner.
## Client API
Import `DesktopStorageClient` from `src/main/desktop-storage-client.ts` in Main.
Construct it after Electron readiness and the application's single-instance check.
The existing database directories must exist. Preserve the current database paths.
```ts
const storage = new DesktopStorageClient({
assistantPath,
knowledgePath,
defaultRootPath,
userDataPath,
onProgress: progress => publishExistingUpgradeProgress(progress),
onChanged: domain => publishExistingDomainNotification(domain),
onFailure: error => reportStorageFailure(error)
})
await storage.ready
await storage.call('assistant', 'saveLocalConversations', [changes])
const conversation = await storage.call(
'assistant', 'getConversation', [conversationId], { signal }
)
const results = await storage.call('knowledge', 'search', [searchOptions], { signal })
// After stopping producers and awaiting their persistence flushes:
await storage.close()
```
`call(domain, method, argumentTuple, { signal? })` infers argument and result types
from the explicit allowlists in `desktop-storage-contracts.ts`. All results are
Promises. Pass cancellation as the transport option, not inside a DTO. Functions,
signals and live class instances are rejected before posting. Maps and Uint8Arrays
are supported; Node Buffers arrive as Uint8Arrays across Electron IPC.
`userDataPath` is the existing application user-data root, not a database directory
inferred from a filename. `ready` means upgrade worker exit, database initialization,
attachment startup reconciliation and runtime-binding owner initialization completed.
Calls before readiness fail without queueing. `retry()` retains the same public
client and facades. A live failed startup retries in that process; a failed/exited
utility is replaced only after exit confirmation and old-request settlement. The
replacement gets fresh request correlation, never the old writes. The confirmed
upgrade decision is acknowledged into Main memory before upgrade execution, retained
across startup loss, and cleared when that upgrade completes. Wire `retry()` to the
existing retry UI. There is no automatic restart or replay of unconfirmed writes.
The defaults locate `desktop-storage-entry.js`, `readonly-query-worker.js` and
`assistant-storage-worker.js` as packaged siblings. `entryPath`, `readerWorkerPath`
and `upgradeWorkerPath` overrides support isolated tests and explicit composition.
The new entry is included in `electron.vite.config.ts`.
## Owner-Side Operations
| Existing call shape | Storage operation |
| --- | --- |
| Assistant/Knowledge ordinary database methods | `call('assistant' / 'knowledge', method, args)` using the explicit allowlists |
| SSH project create/update | `assistant.createSshProject` / `assistant.updateSshProject` accept `Omit<SshProjectWrite, 'assertCurrent'>`; Main checks the live host/runtime before dispatch, and the owner retains cancellation and project-revision checks inside the existing transaction |
| `activityHistory()` live repository | Existing `getActivityHistoryPage`, `getActivityHistorySummary`, `updateActivityHistory`, `replaceActivityHistory`, `clearActivityHistory`, `reconcileActivityHistory` operations |
| `supervisionReviewStore()` | `call('review', method, args)`; initialization/resume execute on the write owner, not the old independent writer |
| `supervisionStories()` | `call('stories', method, args)` |
| `supervisionExperiences()` | `call('experiences', method, args)` |
| `supervisionSuggestions()` | `call('suggestions', method, args)`; resolution uses `assistant.resolveSupervisionSuggestion` |
| Knowledge `externalStore` | `call('external', method, args)` |
| `readSnapshot(callback)` for review context | `call('assistant', 'supervisionContext', [request])`; summary/background share one reader snapshot |
| Knowledge `publishDocument(..., { afterChunksInserted })` | `call('knowledge', 'publishDocument', [input, chunks, { graph, embeddingReplacement, embeddingError }])` |
| Knowledge graph-only replacement | `call('knowledge', 'replaceDocumentGraph', [library, document, graph])`; reuses the existing evidence replacement transaction |
| Attachment references and full-output backing | `call('attachments', method, args)`; includes owner-local `reconcile(startup?, protectedIds?)` |
| Temporary and persisted document results | `call('documentResults', method, args)` |
| Runtime bindings and native model-call ledgers | `call('runtime', method, args)` |
Repository instances never leave the owner. The publication graph DTO is the
existing `GraphExtractionResult`. The owner creates the graph callback and performs
entity identity resolution, evidence insertion, chunk/embedding publication and
pruning within `KnowledgeDatabase.publishDocument`'s existing transaction. Replace
the consumer's publication callback with this DTO; remove its duplicate graph
publication path when migrating that consumer. Extraction/model work remains outside
storage. Publication and replacement share `storeExtractedGraph` from
`desktop-storage-knowledge-operations.ts`. Callback-bearing `replaceEvidenceForDocument`
is excluded from the transport allowlist. File adoption/release and task-status
orchestration remain outside this SQLite transaction.
After readiness, `createDesktopStorageFiles(storage)` supplies attachments, results
and output backing; `createDesktopRuntimeStorageAdapters((domain, method, args) =>
storage.call(domain, method, args))` supplies runtime bindings and ledger access.
The facade caller types now share the core DTO types, so these calls need no casts.
`StorageData` preserves the existing recursive `JsonValue` as-is rather than
recursively rebuilding it; runtime validation still rejects callbacks and handles.
The owner supplies both file callbacks. Attachment reconciliation checks
`assistant.hasAttachmentOwner`. Persistent document lookup reads
`knowledge.getDocumentByParsedResultId` and resolves the existing
`<userDataPath>/knowledge-assets/<libraryId>/<documentId>/<resultId>` directory and
source location. Main must not register another persistent lookup on the results
facade. The existing `onProgress`, `onChanged` and `onFailure` APIs are unchanged,
including `createAssistantStorageOnChanged`'s four notification mappings.
Keep these composites intact during integration:
- Remote task/conversation batches use the existing `appendRemote*Batch` or
`appendRemote*Once` methods. Await commit before forwarding or ACK. Never translate
a batch into independently acknowledged per-event writes.
- Review publication uses `assistant.saveSupervisionResult`, which owns result facts
and checkpoint commits. `commitCheckpoints` is deliberately not an external review
operation. No callback-based transaction API is exposed.
- Story/experience apply and review batch save retain their existing repository
transactions. A model request must finish before calling these operations.
- Additional multi-statement snapshots need a named owner/reader handler; do not
recreate `readSnapshot` as several awaited Main calls.
## Bounds And Lifecycle
The utility adapter retains `ReadonlyQueryReader` request correlation, errors and
admission. Its existing request limit is 32, with 32 MiB of argument admission
credit. This is no longer a content-size restriction: one oversized complete input
uses the entire credit budget exclusively. Smaller concurrent inputs still share
the budget; saturation returns `STORAGE_CAPACITY` without dropping work.
`desktop-storage-wire.ts` transfers complete values on the same channel in frames
of at most 256 KiB accounted data. Each direction sends one frame and waits for its
acknowledgement before constructing the next. Strings are split without assembling
a whole JSON/serialized copy; binary frames copy only their slice and fill the
receiver's final buffer. The receiver exposes a value only after its last frame.
These frame acknowledgements provide transport backpressure, not durable write ACKs.
Cancellation bypasses the frame queue and is retained for an incompletely received
request until dispatch can reject it safely.
Known complete-note/history/result reads share one materialization/transfer slot,
so queued requests do not build many huge reply objects while another reply drains.
Writes and bounded queries retain ordinary admission. A complete user value itself
still needs memory proportional to its size; the frame budget is not a claim that
total process memory or every supported history fits in 32 MiB. There is no result
size rejection or truncation, and no `STORAGE_RESULT_CAPACITY` outcome after a commit.
Renderer conversation persistence now targets 8 MiB batches as well as the existing
100-conversation bound. A single oversized conversation stays complete and uses the
framed transport. Quit flushing drains all remaining batches and propagates a failed
write. Oversized single histories no longer fail solely because of the transport cap.
`STORAGE_UNCONFIRMED` means the process exited without confirming a pending operation;
reconcile using existing domain identities, not blind retries.
Queued cancellation is checked before dispatch. In-flight reader cancellation uses
the existing thread SharedArrayBuffer inside the owner; that buffer is never sent
over utility-process IPC. Synchronous commits cannot be interrupted by an incoming
message, so a completed commit returns success even when cancellation was requested
too late. Request capacity remains occupied until settlement.
`close()` stops client admission, drains admitted requests, awaits file-owner cleanup,
closes runtime ledgers/bindings and read workers, awaits the existing PASSIVE
checkpoint worker's exit, closes both database owners
and observes utility-process exit. An exit without the close acknowledgement rejects
close. Apply the application's existing shutdown deadline to the whole operation;
the transport separately reports an unconfirmed exit after its 10-second exit wait.
Startup failure follows the same release barriers. A failed close attempts the
remaining independent closes, reports `STORAGE_CLOSE_FAILED`, and exits unsuccessfully;
it does not acknowledge a clean drain or permit a second owner to open in that host.
## Remaining Integration And Acceptance
### Supervision And Heartbeat Callers
`createSupervisionDomainPorts(storage)` in
`src/main/assistant/supervision-domain-ports.ts` supplies Main-side RPC facades:
```ts
const ports = createSupervisionDomainPorts(storage)
const supervisor = createProductionSupervisorService(ports.supervision, getSettings, resolveRuntime, pool, persistUsage)
const phrase = createProductionSuggestionPhraser(ports.supervision, getSettings, resolveRuntime, pool, persistUsage)
const heartbeat = new HeartbeatService(ports.heartbeat, actions)
await deriveSuggestions(ports.suggestions, phrase, input)
```
The production factory's other arguments are unchanged. If provided, `persistUsage`
must return the persistence Promise. All heartbeat methods, including configuration
CRUD and history, now return Promises. Review repository factories may return a
Promise of the typed facade. The facades contain closures only; no database or live
repository is transported. `initializeSupervisionReview` and `resumeSupervisionReview`
route to `review.initialize` / `review.resume`, with signals in transport options.
The caller does not invoke the old independent supervision writer.
`story-assignment-service.ts` and `experience-extraction-service.ts` are the sole
story/experience model orchestrators, including their prompts, output schemas and
model-facing types. Production, tests and live scripts import those services.
The repository files retain their SQL methods and transactions, with type-only
imports for validated model output. `supervision-suggester.ts` awaits candidate
selection and suggestion publication. The heartbeat live script also awaits plan
creation before using its ID.
Callers await leaf/navigation writes, final publication, task usage/status,
heartbeat projections, story/experience results and error bookkeeping. Cancellation
aborts model work immediately, but review admission remains occupied until the
cancel write and active execution settle. A late rejected cancel does not overturn
a confirmed publication. Failure to persist a suggestion failure is propagated;
a model phrasing failure whose status was saved preserves the completed review.
Validation includes unchanged behavioral assertions in the existing review/story/
experience suites, delayed-commit tests, and `supervision-storage.test.ts`: a real
Electron storage utility process runs heartbeat, two review leaves, navigation,
stories, experiences, suggestion publication, usage, a no-change heartbeat and
cancellation using a local deterministic Runtime. This does not validate final
Main/IPC composition or concurrent latency budgets.
On 2026-10-05, after removing the duplicate orchestrators, the targeted supervision/
heartbeat run passed 215 tests across 21 files, including the real utility-process
test. Scoped ESLint and Node type checking passed.
The bounded `scripts/supervision-storage-live.cjs <runtime-settings.json>` probe
used the current supervisor profile (`gpt-6-astra`, OpenAI Responses) and production
utility storage with three synthetic calibration messages. Exactly four actual
HTTP requests completed, all HTTP 200: review, story assignment, experience
extraction and suggestion phrasing. It committed one leaf covering all three
sources (589 characters), assigned three events to one story, created one experience
and published two suggestions. The unchanged review made zero additional requests.
Persisted usage was four calls, 3,495 input and 1,251 output tokens. Two initial
preflight attempts sent zero requests; the probe was corrected to install the
isolated Electron encryption state before readiness. Current settings remained
byte-identical; credentials were not logged, and isolated databases, encryption
state and bundles were removed. This is functional evidence, not concurrent
performance acceptance.
### Integrated Owner
The foundation registers assistant, knowledge, external metadata, supervision,
attachments, document results and runtime operations without Main SQL fallback. The
production composition in `src/main/index.ts` constructs the Assistant, Knowledge,
file and Runtime facades after storage readiness and passes them to the relevant
Main services. Remote Agent database ownership is unchanged.
The remaining work is product-level acceptance: complete C01-C07/A01-A10 coverage,
packaged cross-platform checks, and removal or verification of any caller that is
outside the migrated desktop domains. Focused domain tests do not establish those
results.
The S05 runtime-domain owner, Main adapters, required composition wiring and focused
validation are documented in [Runtime Storage Integration](./desktop-storage-runtime-integration.md).
Supervision now uses `ReadonlyQueryReader.reviewManifest(runId)`. The existing
read-only assistant worker scans once under a read snapshot into connection-local
temporary metadata, ends the snapshot, and returns at most 200 metadata rows per
page. It holds at most one live scan manifest, rejects a competing scan explicitly,
and releases that temporary data in the operation's settlement path. It never
opens a business-write connection.
The sole write owner clears/rebuilds disposable manifest rows in bounded batches,
commits each insertion batch atomically, and yields between commits. Cancellation
and persisted pause/cancel status are checked between batches. `initializing`
remains set after interruption; clearing it and deleted-source cleanup share the
final transaction. Resume rebuilds partial manifests, or validates existing frozen
revisions in read-worker pages of at most 200 rows, yielding between pages. A failed
scan never falls back to synchronous owner scanning. Final result/checkpoint
publication remains the existing atomic operation.
The old writable `supervision` worker mode, connection opener and separate reader
field were removed. `AssistantDatabase`'s direct initialization/resume methods use
the same bounded implementation; in-memory owner-local use retains its synchronous
algorithm because a separate read connection cannot share that database. Other
non-worker operations still execute synchronously inside storage; no latency
improvement or full concurrent performance acceptance is claimed.
The current focused validation passes 70 tests across eleven suites: core storage,
file/runtime domains, assistant facade, read workers, bounded review-store algorithms,
existing supervision worker regressions, startup, wire backpressure, renderer batching
and real supervision/storage orchestration.
Real Electron coverage includes graph replacement rollback, SSH revision conflicts,
owner-side persistent document lookup after reopen, full-output retention and crash
reconciliation, ledger recovery, runtime startup failure/retry and pending file-save
drain before cleanup. Review tests cover 200-row bounds, concurrent owner writes,
frozen scan revisions, cancellation, failed-batch rollback, reader loss, marker
recovery, deleted sources and final marker/cleanup atomicity.
Real Electron large-value tests save an explicitly typed 16 MiB MP4 payload, read a
complete two-entry note, and round-trip all 25,600,230 characters of an 80-message
history. Hashes verify complete video and message bodies. The measured accounted
frame high-water marks were 248,332 bytes in Main and 252,578 bytes in storage, below
262,144 bytes; argument credit peaked at 33,554,432 bytes. Main RSS peaked at
426,049,536 bytes including fixture inputs, complete results and application memory.
This is queue-bound evidence, not a whole-process memory or latency budget claim.
The real process tests also kill an in-progress write transfer and confirm no replay
after retry, and verify startup decision retention/clearing across process loss.
Full Node, Agent and renderer typechecking passes, as does scoped ESLint. Temporary checks and databases use
`temp/goodbuddy-storage-foundation`; tests remove their generated bundles/data.
The earlier foundation production-build check predates this domain integration.
Packaged cross-platform and full UI-to-storage concurrent acceptance remain outstanding.
### Reader Ownership Cleanup
The obsolete `readWithFallback` export, database-owned readonly-reader fields and
checkpoint lifecycle methods, and the database async wrappers were removed. Heavy
reads are dispatched by `DesktopStorageOwner`; owner-local synchronous reads remain
available inside transactions and focused database tests. Read-worker and checkpoint
tests now create explicit `ReadonlyQueryReader`/`Worker` owners and await their close.
The three perf harnesses create explicit owner-local reader/checkpoint resources under
the repository temporary root, report `executionBoundary: owner-local`, and close
those resources before deleting their copies. No production or test source reference
remains to the removed compatibility APIs.
The cleanup validation adds termination barriers and failed-frame poisoning: a reader
cannot reopen capacity while its prior worker termination is pending, a transport
posting failure cannot feed later values into a partial decoder, and close waits for
wire drain and owner settlement. The focused cleanup run passes 122 tests; Node and
renderer type checks and scoped lint pass.
@@ -0,0 +1,66 @@
# Knowledge Main Storage Migration (S03)
Knowledge Main now receives `KnowledgeServiceOptions.database`, a
`KnowledgeStoragePort`. Production composition constructs it with
`createKnowledgeStoragePort(storageClient)` after `storageClient.ready`.
`KnowledgeService` no longer opens, initializes, closes, or starts readers for
the business database. The application storage owner retains those duties.
The adapter sends explicit `knowledge` and `external` domain calls. Search
signals travel as transport options, not serialized arguments. The port's
`Awaitable` return type permits synchronous owner-local test implementations;
the production adapter always returns the storage client's promises.
Main awaits source, document, task, graph, embedding and external metadata
operations. IPC mutations await their writes before returning or refreshing a
snapshot. Parsing, models, credentials and source watchers remain in their
existing Main services. Embedding batches retain their existing sizes and
staging/publication sequence.
Publication passes the extracted graph through foundation
`publishDocument(..., { graph })`. Graph-only replacement calls
`replaceDocumentGraph(library, document, result)`. Its implementation in
`src/main/desktop-storage-knowledge-operations.ts` runs the existing synchronous
graph algorithm inside `replaceEvidenceForDocument`'s transaction. No callback
or SQL crosses the storage boundary.
Shutdown aborts producers immediately, drains tracked work and observes the
task-interruption write and embedding status persistence. The service does not
close the shared storage client. Embedding allocation returns its replacement
ID even if cancellation arrives with the reply, allowing cleanup; a committed
finish is not reported as rolled back. Rebuild admission remains occupied
until its final status write settles, and status-write failures reach the
completion barrier.
## Current Production Boundary
The following boundary is implemented in the current production composition:
- `replaceDocumentGraph(library: KnowledgeBase, document: Document,
result: GraphExtractionResult): void` to the knowledge domain contract and
dispatch it to the exported owner-local operation, passing the owned database
as its first argument. It is not part of the concrete database method
allowlist because it is a composite owner operation.
- Foundation publication reuses the exported `storeExtractedGraph` helper.
- `StorageData` preserves the existing recursive JSON type without rebuilding it
in the adapter.
- Persistent document lookup is resolved by the storage owner through the
Knowledge domain; Main does not register a second lookup.
- Application composition injects the port, awaits asynchronous gateway and
tool-provider calls, and closes shared storage after Knowledge disposal.
## Validation
The focused Knowledge service, external service, embedding coordinator and
embedding repository suites use a Promise-backed adapter over a test-owned
SQLite connection. Coverage includes publication rollback, embedding batch
ordering, late cancellation, replacement cleanup, external request cancellation,
credential persistence and restart. The production gateway now awaits its library
and search operations. Remaining evidence is limited to the focused suites and
does not establish full concurrent desktop acceptance.
Owned production files pass ESLint. The current worktree typecheck and focused
storage validation cover the injected production port and owner dispatch. Global
architecture acceptance, packaged checks and concurrent performance acceptance
remain with the parent delivery; these tests do not establish a new performance
baseline.
@@ -0,0 +1,96 @@
# Desktop Runtime Storage Integration
S05 domain implementation and production composition, 2026-10-05. This supplies
the runtime domain for the [desktop storage foundation](./desktop-storage-foundation.md).
The current composition passes the storage-owned binding store and model-call
ledger factory to Main runtime services. Full desktop migration and performance
acceptance remain incomplete.
## Integration API
`src/main/desktop-storage-runtime-operations.ts` exports:
- `DesktopStorageRuntimeOperations`: the existing `RuntimeSessionBindingStore`
interface plus named ledger lifecycle and transition operations.
- `runtimeStorageMethods`: the explicit host dispatch allowlist.
- `DesktopStorageRuntimeOwner.open(userDataPath)`: creates the sole binding owner
at `remote-runtime-bindings.sqlite`. Await it before publishing storage readiness.
Native ledgers open lazily at their existing paths through `openLedger(id, path)`.
- `createDesktopRuntimeStorageAdapters(call)`: Main adapters returning
`bindingStore` and `openModelCallLedger`. `call` has the same domain/method/tuple
shape as `DesktopStorageClient.call`, restricted to the `runtime` domain.
- `DesktopStorageRuntimeOwner.close()`: closes remaining native ledgers and the
binding database. The outer storage owner must first drain admitted operations.
This method is not part of the external domain allowlist.
The storage-core integration adds `runtime: DesktopStorageRuntimeOperations` to
`DesktopStorageDomains`, initializes one runtime owner from the existing user-data
directory, and dispatches allowed runtime methods against that owner. Include its
close in the storage owner's release path, including startup failure. No new RPC
client, SQL endpoint, admission queue or process is needed.
After registering the domain, Main composition is:
```ts
await storage.ready
const runtimeStorage = createDesktopRuntimeStorageAdapters(
(domain, method, args) => storage.call(domain, method, args)
)
// ManagedRemoteExecutionServices options:
// bindingStore: runtimeStorage.bindingStore
// The former userDataPath option is removed; the host owns that path.
// NativeClientCoordinator, NativeTerminalClient and NativeDshWebClientService options:
// openModelCallLedger: runtimeStorage.openModelCallLedger
```
`ManagedRemoteExecutionServices` borrows the binding interface and does not close
it on disposal. Native clients stop their producers and proxy, await their ledger
close, then delete launch material. DS Web retains native history. A failed or
unconfirmed close retains files and propagates the error. A lost open reply also
retains files because the host may still own the connection. Shutdown must stop
native/remote producers before closing the shared storage client.
## Ordering And Ownership
The shared `ModelCallLedger` type allows synchronous or Promise-returning methods;
the concrete `AgentModelCallLedger` stays synchronous and remains the remote
Agent's owner. Schema, database names, call identities, retention and transaction
algorithms are unchanged.
`AgentModelGateway` awaits claim before provider dispatch, complete before returning
the response, delivered before resolving the delivery acknowledgement, and
outcome-unknown before reporting provider failure. Cancellation while claim is
pending does not dispatch after claim completes. Persistence rejection propagates;
the gateway does not replay a provider request after an unconfirmed write.
## Validation
The final focused run passed 113 tests across eight files, with 11 existing opt-in
cases skipped. It included the official DS Web process using standard Node,
binding uniqueness/rotation/cursor persistence, concrete ledger reopen and uncertain
outcomes, delayed/rejected persistence, and both native clients' close ordering.
`desktop-storage-runtime-operations.test.ts` also builds real Electron fixtures.
The production native terminal loopback gateway uses Main adapters and the real
`DesktopStorageClient`/transport to reach a utility-process runtime owner, then
closes its ledger before removing launch files. Production core registration is
covered by `src/main/index.ts`; the fixture remains focused on the runtime domain.
Its model response is local and deterministic, with no external provider call.
The shared gateway was separately validated on the pinned Linux x64 test Host via
the existing lifecycle harness rebuilt from current source. OpenCode ran a bounded
tool command in a dedicated `/root/tmp/gb-lifecycle-*` workspace. Desktop detached,
the test Agent was promoted, the controller resumed, and the prompt completed.
There were exactly **2 real text-provider calls**, both HTTP 200, **0 image calls**,
33 persisted event entries, 17 checkpoints, and successful replay deduplication.
The old test daemon retired; final owned process count was zero. The harness removed
its remote directories. Local artifacts were isolated in
`temp/goodbuddy-ledger-upgrade` and cleaned after validation.
Scoped ESLint and `git diff --check` passed. The current composition supplies
`bindingStore` to `ManagedRemoteExecutionServices` and `openModelCallLedger` to
the native coordinator. Whole-application acceptance, packaged cross-platform
checks and concurrent performance measurements are not established by these
focused tests.
+54 -11
View File
@@ -5,11 +5,12 @@
- **适用范围:** Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。
- **用途:** 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。
- **关联文档:** `docs/roadmap/product-performance-experience-improvement-plan.md`(测量记录与实施进度)。
- **架构与验收:** [桌面存储与并发执行](./desktop-storage-direction.md)定义一次完整交付的职责和范围;[桌面并发验收](../quality/concurrent-desktop-acceptance.md)定义多项目、多会话、多子任务负载及证据。本文件是性能规范的唯一数值来源。
- **参照:** VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。
## 1. 性能预算
“流畅”用下面的数字判断,不靠感觉。“标准机”指不降速运行 `npm run perf:app`,“低端机”指加上 `GB_PERF_CPU_THROTTLE=4`。
“流畅”用下面的数字判断,不靠感觉。“标准机”指记录了硬件环境、不降速运行的参考机;Renderer 降速场景使用 `GB_PERF_CPU_THROTTLE=4`。该降速仅作用于 Renderer,不等于真实低配整机,也不模拟慢 Main 或磁盘。预算适用于单场景及规定的并发负载。
| 指标 | 标准机 | 低端机 |
| --- | --- | --- |
@@ -21,7 +22,9 @@
| Main 事件循环延迟,p99 | ≤ 30 ms | — |
| DOM 节点数 | 不随会话数、消息数增长 | 同左 |
标准机的阈值由 `npm run perf:check` 检查。低端机的数值目前只做记录,等结果稳定后再加进阈值检查。
当前 `npm run perf:check` 使用历史回归上限,部分数值宽于本表,也未覆盖全部并发入口。检查通过不能宣称本表达标。目标交付须在验收报告中分别给出规范预算和回归上限的结果;低端机降速目前只做记录,真实低配和新增自动检查的完成状态另行记录。
点击反馈、所需内容可用和后台工作完成分别计时。显示加载状态只能证明及时反馈;不能用它替代结果就绪。每个进程分别报告长任务/事件循环和排队耗时,禁止把多个独立探针相加为端到端延迟。
## 2. 原则
@@ -32,6 +35,7 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
- 新代码不能在 Main 里使用 `DatabaseSync` 和同步文件 API。lint 已有白名单,名单只能减少。
- 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
- 目标交付后 Main 不再直接或通过 helper 打开桌面业务 SQLite;只读、失败回退、维护及退出均在检查范围内。当前迁移例外不是新增同步工作的许可。存储宿主可继续使用同步 SQLite,事务和计算仍需有界。
### P2 界面状态局部订阅
@@ -53,7 +57,8 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
- `App.tsx` 只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。
- 每个 store 的单元测试和 store 文件放在一起(`<domain>-store.test.ts`),覆盖顺序、失败重试和边界情况。
- Main 侧对应:`<domain>` 的数据库访问集中在一个 repository 类里,IPC handler 只做校验和转发。
- 领域数据库访问集中在 repository,目标架构中由存储宿主执行;Main 的 IPC handler 只做校验并等待领域操作。受管只读 Worker 与远端 Agent 按各自明确的连接归属执行,不能按源码目录把它们误判为 Main 访问。
- 多项目/多会话流式更新只通知相关领域与实体;后台子任务不得让全部项目视图重渲染。隐藏视图保留草稿、选择和必要执行订阅,停止无关绘制与全量重读,切回时按已有摘要及增量恢复。
### P3 高频数据走专门通道,按帧合并
@@ -62,12 +67,16 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
- 高频路径上不能读取布局(`getBoundingClientRect`、`scrollHeight` 等),布局读取要集中到每帧一次。
- 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
- 显示合并与持久化分开。可以合并进度或替换过时刷新,不能静默丢弃消息、工具结果或需确认的事件;多生产者缓冲只有在用户设置、外部协议、数据安全或实测故障证据要求时才设置容量,否则将负载交给所属 Host。
### P4 数据只有一份来源,只传变化部分
- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
- 写入合并成批次和事务,不按每个事件单独开一个事务。
- 一个数据库有一个业务写入所有者,完整事务不跨 IPC 拆成单条 SQL。Main 不保存第二份可写业务状态;依赖写入的读取等待对应提交,远程 ACK 等待所需事件与 checkpoint 提交成功。
- 缓存必须说明归属、失效和释放条件,不按浏览过的项目/会话无限积累。查询返回摘要、分页或所需详情,保持完整信息可访问,不用截断历史满足预算。
- GoodBuddy 自身的中转数据优先使用有界内存;已有完整记录或文件时复用引用,避免重复落盘。预览、最终回答与工具全文分别核实,不能以“已保存消息”推断全文已保存。需要超出内存保留的内容直接使用所属存储的唯一正文,分页读取同一内容,避免临时全文再复制到正式存储。必要文件及清理范围遵守[输出与临时资源设计](./desktop-storage-direction.md#output-and-temporary-resource-ownership)。
### P5 工作量和可见内容成正比
@@ -80,22 +89,26 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
- 首屏之前只做显示应用壳和恢复上次界面所需的工作。
- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
- 后台模块在第一次使用时才加载。
- 冷启动、首次进入功能和暖返回分别验收;预加载只使用有界空闲工作,不能为每个项目预建进程、连接或整份历史缓存。进程 spawn 不代表服务就绪,首次可用反馈与数据实际可用分开记录。
### P7 先测量,再修改
- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
- 必测多项目、多会话、多子任务及导入/检索/回顾发布重叠场景,负载和采样按[并发验收](../quality/concurrent-desktop-acceptance.md)。记录 offered load、实际吞吐、排队和排空、资源高水位;不得降低输入速率、串行化夹具或丢事件来取得流畅结果。
- 基准使用固定版本、数据和环境。性能测量与真实模型功能验收分开;受发布构建限制时遵守发布手册,不以性能原则授权额外生产打包。
### P8 正确性优先于速度
- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
- 取消请求、执行结算和容量释放不是同一时刻;调用方停止等待不能提前释放仍运行的工作。取消一个会话或子任务不得杀死仍服务其他任务的共享宿主。已提交事务如实报告成功,不伪装成回滚;未确认写入不盲目重放。
### P10 体验不倒退
性能优化只能让体验变好,不能用体验换速度。“行为不变”不仅指数据和结果,也包括用户能感知到的交互:
性能优化必须保留数据、结果和用户交互语义,不能用体验换速度:
- **动画和过渡保持原样。** 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入 `scrollTop`/`scrollTo` 的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。
- **只渲染可见部分(P5)时,用户状态不能丢。** 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过 `<details>` 或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。
@@ -109,6 +122,25 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
- 规则只能收紧,白名单和上限只能往下调。
- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
- 检查直接 SQL 导入及已知间接打开函数/构造点,按实际宿主区分测试、存储与远端代码。白名单和上限需要与已提交基线比较,不能只靠注释约定;全局同步回退同样要被发现。
- 普通 CI 与性能执行环境分别提供功能和预算证据;新增并发与首次进入场景纳入候选必需检查,缺少运行环境或结果时保留验收缺口,不由单测通过推断达标。
### P11 并发隔离与有界工作
同一会话的事件顺序和同一数据库的提交顺序必须保持;不同项目、会话、子任务的模型等待、重查询与清理不因此共用一把长时间全局锁。
- Main 全局控制队列不得等待模型、网络、原生启动或耗时清理。必要登记和状态提交可短暂串行,等待在所属操作范围内完成。
- 重查询复用有界 Worker;必要写入不排在整个导入解析或模型请求之后。长事务先减少事务内无关计算,再测写入排队,不把阻塞简单搬进唯一存储队列。
- 复用现有池、批处理和队列,记录在途数、排队数及实际资源使用。只有有明确依据时才设置容量;没有依据时将负载交给所属 Host,不因内部数字拒绝用户工作。有实测饥饿才引入最小调度调整,不预建通用优先级框架。
- 控制和取消不等待新的执行槽位;如果外部或用户设置要求排队,按该语义执行。禁止静默丢失、无限重启或自动扩增进程;不能把未经证明的队列上限包装成稳定性。
- `ReadonlyQueryReader` 的生产 storage 模式不传 `maxPending`,因此请求直接透传给 storage host;其等待队列只服务显式有界的调用方和测试,不构成第二个 storage owner。
- 每个异步响应携带已有项目/会话/任务/操作归属;迟到响应不得覆盖新选择或新操作。归属检查先于改变共享请求代次、缓存或队列状态。
### P12 架构完整性与 KISS
一次性交付按最终职责边界验收,不以全面 Promise 化或局部热点迁出代替。目标范围内正常、后台、失败、重试、取消和退出路径均使用同一权威实现,清理原直接入口和回退。
保持现有技术栈、数据库文件及业务合同,复用已有资源机制。不得顺带新增分布式事务、持久 RPC receipt、通用恢复框架、每项目独立服务或产品审批模式。同步内存缓存读取可以保留,不把纯函数和每个 getter 远程化。设计新增机制必须指出当前失败、现有机制的不足与对应验证,不以“将来可能并发”代替证据。
## 3. 评审清单
@@ -125,19 +157,30 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
9. 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
10. 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
11. 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
12. 多项目/多会话/多子任务同时运行时,取消或清理一个请求是否拖住其他任务?队列和缓存是否持续增长?
13. 存储提交、远程 ACK、调用方返回和资源释放的顺序是否有真实路径及故障注入验证?
14. 是否删除目标范围内的旧直接访问和同步回退?新增进程或调度机制是否复用现有设施并有实际必要性?
15. 请求或输出是否产生非必要临时文件、重复全文?是否核实完整内容与预览的区别,并验证分页、引用释放、必要文件的创建失败清理及升级后的残留回收?
## 4. 当前差距
截至 2026-10-04(第一期结项,见 `docs/review/performance-phase-1-report-2026-10-04.md`),按原则对照的主要差距:
截至 2026-10-05,以下状态结合[存储升级实施记录](../quality/storage-upgrade-implementation-2026-10-05.md)和[并发验收规范](../quality/concurrent-desktop-acceptance.md)核对。表中“已实现”只表示代码或定向测试已有证据,不表示 PERF-15、PERF-16 或 C01-C07 已完成。
| 原则 | 差距 |
| --- | --- |
| P1 | 同步数据库调用仍在 Main,没有独立数据进程;但热点读已在 worker,高频写单次 0.1–0.3 ms,WAL checkpoint 在后台线程,启动恢复按索引读取。文档解析、分块和大文件哈希已移到 worker。消息附加数据冗余(子任务工具输出、`tools` 与 `blocks` 重复),大会话读取约 170 ms |
| P1 | Assistant、Knowledge、文件和 Runtime 生产域已由 `DesktopStorageClient` 接入 utility process;剩余缺口是全量间接访问扫描、异常/维护/退出路径核验,以及 Main 阻塞和存储排队的并发测量。文档解析、分块和大文件哈希仍在各自 worker。 |
| P2 | `App.tsx` 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App |
| P3 | 已基本满足:流式输出和终端输出都已按帧合并 |
| P4 | 活动记录已改为增量保存、按页读取(界面只保留已加载的页,计数和分组来自 Main 的汇总);部分 `*Changed` 通知之后还会重新查全量 |
| P5 | 已基本满足:消息、会话、笔记、文件树、知识库文档表格都只渲染可见部分 |
| P6 | 空配置启动到输入框可用约 0.96 秒;真实数据(2.4 GB)约 1.36 秒。本地工具环境已后台初始化,低频模块按需加载 |
| P9 | 性能阈值还没有接入 CI,也没有真实低端机的基线 |
| P3 | 流式输出和终端输出已有按帧合并;C01/C02/C06 的长期负载仍需验证是否出现排队增长、长帧或内容丢失。 |
| P4 | 存储域已统一到各自 owner,输出 backing 和附件引用已接入;部分 `*Changed` 通知仍会重新读取全量,重复缓存和跨域通知的并发影响待测。 |
| P5 | 消息、会话、笔记、文件树、知识库文档表格已有窗口化;C07 仍需记录冷/暖内容可用时刻和切换后的状态保留。 |
| P6 | 空配置和真实数据启动有历史测量;当前 C07 尚未完成监督概览、图谱、历史选择和来源正文的完整冷/暖验收。 |
| P9 | 已有 lint 和历史回归上限;间接 SQLite 打开、白名单单调收紧、并发指标缺失和 CI/性能环境接入仍待补齐。 |
| P10 | 已有局部输入、刷新和项目切换修复;需要在 C01/C02/C06 中继续验证动画、焦点、滚动、草稿和取消语义没有因降载或合并而退化。 |
| P11 | 存储请求已移除产品侧 admission 上限并透传给 storage host;运行时仍有若干并发与消息容量数字,需逐项证明属于用户设置、外部协议、数据安全或实测故障约束,否则应删除。C02、C04、C05、C06 尚未证明健康 peer 隔离、故障恢复和队列排空。 |
| P12 | 目标边界和 KISS 约束已写入架构文档;S01-S07 已有生产组合,但删除旧直接入口、异常路径和全候选统一验收仍未完成。 |
2026-10-04 补充:监督回顾的来源初始化和继续前版本校验已移入现有 Worker 入口的独立连接,扫描不持有写锁,清单按 200 条提交,生产失败不回退 Main 同步扫描。最终发布事务仍在 Main;实现与实测边界见[监督者进度](../features/conversation-supervision/progress.md#2026-10-04-回顾初始化阻塞修复)。
2026-10-05 局部修复记录:监督者 UI 的概览、图谱、故事读取也已迁到现有只读 Worker;概览按需加载、列表窗口化及摘要缓存见[KISS 局部修复](../review/kiss-local-fixes-2026-10-05.md)。这与工具侧 Story Graph 读取是不同入口。Main 最终发布及部分大响应仍有超预算样本,不能把局部完成视为全项达标。
随后用户确定按[完整存储与并发架构](./desktop-storage-direction.md)一次性交付。当前已完成存储 owner、主要生产组合和部分输出迁移,但没有完成全面架构验收。P11/P12 与[并发验收矩阵](../quality/concurrent-desktop-acceptance.md)约束当前候选的剩余验证,不能作为已有实现或实测结果;上述第一期历史状态也不替代当前验收。
+211
View File
@@ -1,8 +1,18 @@
# Runtime Storage And Background Reads
## Current Implementation
This file records current runtime timing, reader behavior and temporary-resource
ownership. The production runtime receives the storage-owned output backing store and
output adoption function during Main startup. Main also computes one per-version,
per-run temporary root and passes it to the local runtime cache paths and native
client coordinator.
SQLite owns persisted task, event and conversation evidence. The desktop business
connection is long-lived. WAL permits readers alongside a writer; synchronous SQL
still blocks the thread that executes it. Memory caches are disposable derived data.
Persisted evidence is not necessarily full tool output; the inventory below records
the current distinction. The target ownership rules are in the architecture document.
Startup storage upgrades run in `assistant-storage-worker` before the business
connection opens. Runtime readers must not invoke initialization, migrations or
@@ -16,6 +26,17 @@ or result cache. Main owns current segments independently of renderer visibility
The [duration design](../features/task-and-job/technical-design.md) defines the
state boundaries, restart handling and incomplete history.
## Planned Architecture
The [complete desktop architecture](./desktop-storage-direction.md) defines the
remaining target, including desktop binding databases and native-client model ledgers.
Its migration and [concurrent acceptance](../quality/concurrent-desktop-acceptance.md)
are one delivery; remote Agent database ownership remains unchanged. Main will still
observe runtime transitions while the storage service persists them in order; the
indexed timing representation and semantics remain.
## Runtime Read Lifecycle
Read transactions end before replies are posted. Long-lived read transactions
would prevent WAL checkpoints from reclaiming old pages. Each reader has its own
SQLite page cache; adding workers increases memory usage and requires evidence of
@@ -34,3 +55,193 @@ reopening. Results remain scoped; superseded responses are discarded.
The [task statistics design](../features/task-and-job/technical-design.md) owns the
snapshot contract and timing rules. Tests and timings demonstrate only their fixture
conditions; they do not establish a latency guarantee for arbitrary retained history.
## Output And Temporary File Investigation
Source review on 2026-10-05. The inventory records the current production flow. No
user Temp directory was scanned or deleted.
| Producer | Current content flow | Retention and consequence |
| --- | --- | --- |
| Local process and ripgrep | [Process capture](../../src/main/agent/direct-model-process-service.ts) writes stdout/stderr through [PagedOutputStore](../../src/main/agent/paged-output-store.ts); model receives a JSON-bounded preview, up to 96 KiB per stream, and a reference when truncated | Every stream opens a file, including empty stderr. Small files are deleted on finish; retained files and their in-memory handles belong to the conversation/service |
| Direct-model subagent | [Subagent service](../../src/main/assistant/direct-model-subagent-service.ts) captures child text through the same store, returning up to 192 KiB and an optional reference | [Provider event bridge](../../src/main/agent/model-tool-provider.ts) forwards preview/status but drops the output reference from the subagent event; child text is not an independent full parent-history record |
| Tool events and conversation history | [Model runtime](../../src/main/agent/model-runtime.ts) emits at most 16,000 characters of serialized tool result as the event preview; [IPC](../../src/main/ipc.ts) and [database](../../src/main/assistant/assistant-database.ts) persist the supplied event | Tool events still carry bounded previews, while output history adopts finished handles through attachment storage. Even `output_read` responses are previewed in tool events. Persisted event history does not by itself guarantee a complete stdout/stderr copy |
| Remote ACP | [Agent transcript](../../src/agent-daemon/semantic-prompt-store.ts) retains received notifications pending ACK; [desktop adapter](../../src/main/agent/acp-remote-runtime.ts) bounds mapped events before persistence | ACK prunes Agent transcript rows. Notifications themselves may already be bounded by the Runtime. Do not treat this journal as a permanent full-output source or remove commit-before-ACK |
| MCP calls | [MCP transport](../../src/main/capabilities/mcp-client-transport.ts), provider and [gateway](../../src/main/agent/knowledge-mcp-gateway.ts) pass objects through stdio/HTTP/SSE | No per-call request/response scratch-file creation was found in these paths. Calling a tool over MCP does not itself require a payload file; third-party server internals are outside this inventory |
`PagedOutputStore` keeps short and empty output in memory and delegates larger
or JSON-preview-truncated content to an injected `PagedOutputBackingStore`. Capture
has a shared 8 MiB budget, at most 1,024 pending chunks per writer and 4,096 active or
retained handles per store; backing writes are at most 64 KiB. Producers must await
append, or receive an explicit capacity error. Local process pipes pause until each
append settles. The store does not create a temporary file backend or another DB.
UTF-8 byte cursors, JSON-escaped preview bounds, owner checks and failed/cancelled
output remain covered by focused tests using real async files and local children.
The production owner exposes output create, append, finish, read, adopt and release
through the existing storage transport. Main passes its `backingStore` to the runtime
and passes `outputAdopt` to the tool provider. When output history is enabled, finished
stdout/stderr references are adopted into the conversation attachment reference for
the target message; the bounded preview and output reference are returned together.
Missing backing configuration still explicitly fails capture that requires full
storage. A model's final answer is not an equivalent copy of its tool output.
### Planned Architecture And Remaining Acceptance
The planned architecture still requires full end-to-end history and restart coverage,
resource reconciliation and the remaining storage-owner migration. The file-backed
test fixture alone does not establish that architecture or its acceptance.
Tool previews can also appear in both conversation `tools` and `blocks`. This is a
separate persisted-preview duplication issue, not evidence that full paged output is
already stored. Existing [subagent progress compaction](../../src/main/assistant/subagent-progress-storage.ts)
already handles repeated progress snapshots; retain that implementation.
### Other Owned Files
| Existing owner | Source finding | Disposition for the upgrade |
| --- | --- | --- |
| [DocumentResultStorage](../../src/main/document-result-storage.ts) and [attachment storage](../../src/main/conversation-attachment-storage.ts) | `parsed.md` duplicates `manifest.json.content`; normal reads use the manifest and `copyResult` copies the extra file | Remove the redundant new write/copy. Preserve original files, extracted images, manifests and ownership transfer; old directories follow existing reference cleanup |
| [OpenCode runtime](../../src/main/agent/opencode-runtime.ts) | Configuration already uses `OPENCODE_CONFIG_CONTENT`; scratch supplies isolated XDG state and skill registration | Keep required isolation, reuse immutable skill caches. Do not invent a config-file optimization for a path already using memory/environment |
| [Continue adapter](../../src/main/agent/continue-host-adapter.ts) | CLI uses `--config` plus an isolated global directory; creation can fail before the outer owner receives the path | Retain the external-file interface and clean partial creation at the creating function, including skill-staging failure; preserve pre-existing files on `EEXIST` |
| [Continue helper](../../src/agent-daemon/continue-acp-helper.ts) and [model bridge helper](../../src/agent-daemon/model-bridge-helper.ts) | Temporary config/plugin resources use helper finalization; setup before the protected scope and termination need lifecycle coverage | Extend existing ownership and shutdown, verify child exit before deletion. Plugin file URLs and Unix sockets remain necessary interfaces under current contracts |
| [Managed Python](../../src/main/local-tool-environment/managed-python-install.ts), [model packages](../../src/main/model-package-utils.ts), [Agent packages](../../src/main/remote-agent/agent-package-manager.ts) | Install staging, partial downloads and backups support validation/publication; existing cleanup has owner-specific patterns | Extend those cleanup functions for demonstrated leftovers; preserve active staging and verified reusable installations. No global installer sweep |
| [Magic note storage](../../src/main/magic-notes/magic-note-storage.ts) | Sibling atomic-write `.tmp` files protect replacement; memory-database fixtures have a separate temporary root | Keep atomic replacement and normal failure cleanup. Test scratch follows repository rules; persistent note directories are user data |
Document results may move from temporary ownership into attachment references, including
shared branch references. A directory under `temp/document-parsing` can still be owned
by persisted data. Its location alone does not authorize deletion. Native client history,
model caches, database WAL/SHM and exported user files are likewise not disposable scratch.
Some tests/build helpers share production-looking prefixes, including `goodbuddy-process-*`
and `goodbuddy-opencode-*`. Prefix and age alone cannot establish who owns a legacy directory.
The [architecture rules](./desktop-storage-direction.md#output-and-temporary-resource-ownership)
define the replacement and cleanup scope; the [acceptance matrix](../quality/concurrent-desktop-acceptance.md#输出与临时资源验收)
defines evidence required before claiming improvement.
### Creator Cleanup Validation (2026-10-05)
Continue now removes its partially written config and failed skill-staging directory
at the creator, before returning ownership to the run. Exclusive creation failures
leave existing files/directories intact. Run finalization preserves prepared caches
and external configuration. Both Agent helpers protect setup with finalization and
handle SIGTERM/SIGINT; Continue stops prompt admission, aborts and drains active
runs, while the OpenCode helper waits for child exit before deleting its plugin.
Managed Python's existing pre-install cleanup also removes regular download files
with the exact UUID layout, preserving directories, unknown names, caches and history.
Validation in the concurrent working tree:
- Five focused suites (`continue-host-adapter`, `continue-acp-helper`, `model-bridge`,
`managed-python-install`, `local-tool-environment-service`): 81 passed, 10 Unix-only
cases skipped on Windows. Failure injection covers partial creation, EEXIST,
failed skill staging, setup-time signals and shutdown cleanup.
- Final `npm run typecheck` passed; ESLint passed for all eight owned source/test files.
Repository lint passed with `npm run lint -- --ignore-pattern temp`; unfiltered
lint encountered generated validation bundles in ignored task/release directories.
Task-owned `temp/goodbuddy-scratch-upgrade` was subsequently removed and its absence
verified; unrelated release scratch was preserved.
- Full Vitest run: 517 suites passed, 7 failed, 15 skipped; 6,244 tests passed,
6 failed, 94 skipped. Failures outside this cleanup scope included OpenCode config
preparation (`spawnSync npm.cmd EINVAL`), the MCPVault Electron fixture, and
message-expansion and workspace-import Electron assertions. These results do not
establish full architecture acceptance.
- Reused the existing `a01-runtime-host-build.cjs`/`a01-runtime-host-run.cjs` harness
logic, rebuilding Agent, daemon and Desktop driver from current source under
`temp/goodbuddy-scratch-upgrade`. Connected to Linux x64 over the LAN using
GoodBuddy encrypted credentials and the pinned Host identity. Both routes were
reachable; only the LAN route was used for execution.
- Both OpenCode and Continue passed real tool execution, startup cancellation,
disconnect/reconnect during execution, checkpoint persistence/replay deduplication,
and retirement. Final-source cancellation took 217 ms and 233 ms respectively;
each run reported zero owned processes after retirement and shutdown. There were
eight real model requests across the initial and final runs, four per runtime.
Remote artifacts stayed in dedicated `/root/tmp/gb-lifecycle-*` directories and
followed the harness cleanup. These timings are scenario observations, not a
comparative performance claim.
### Version And Run Scratch Follow-up (2026-10-05)
`runtimeTemporaryRoot(userDataPath, appVersion)` computes the lazy
`temp/goodbuddy-runtime/<version>/<runId>/goodbuddy-runtime-launch` path.
Main passes that same value through existing `continueHostCacheRoot` and
`opencodeSharedCacheRoot` options. The native client coordinator receives it as
`temporaryRoot` and passes it to both native clients. The existing constructors need
no new parameter.
OpenCode creates its isolated launch directory beneath the supplied shared root;
Continue puts generated configuration and isolated globals in one per-call directory.
Prepared bundles and immutable Skill/config caches remain reusable within the run.
The helpers on a remote Host use `goodbuddy-runtime-launch` beside their existing
model-bridge socket, under that process owner's scratch directory. Global `TEMP`
is unchanged.
Each local launch has a sibling owner file containing creator and child PIDs. Keeping
metadata outside the deleted tree preserves retry evidence after partial deletion.
The launch root also records its creator for cache-only reclamation. After the
single-instance check and first screen, call `cleanupRuntimeTemporaryDirectories`
in the existing delayed background startup work. It examines at most 256 directory
entries per invocation, sequentially, and accepts the startup shutdown signal.
Service disposal still stops children before deleting its own launch material;
an unconfirmed child exit retains the material. Startup preserves live/reused PIDs,
permission-denied process checks, malformed ownership and the ambiguous spawn-to-PID
recording window. Empty version/run parents are removed after owned material.
This is not a new process owner or an installer collector.
Legacy candidates are limited to the known OS-Temp layouts `goodbuddy-opencode-*`
(desktop isolated state), `goodbuddy-continue-*` (Continue helper), and
`goodbuddy-opencode-plugin-*` (model-bridge plugin). Deletion requires valid owner
metadata and confirmed inactive creator/child PIDs. Releases that wrote no such
metadata cannot be identified as obsolete from their names, contents or age alone;
those directories are retained and reported. A bounded pass can leave candidates
unvisited. Native-client histories, existing version-only caches and unrelated
OS-Temp files are outside this reclamation contract. The old version/layout-only
cache sweep is not evidence that a surviving child has exited.
Remote environment preparation now retains its existing pending-operation record
when connection acquisition or staging cleanup fails. Because that store has one
record per Host, a replacement prepare retries cleanup before overwriting it;
confirmed cleanup or `operation-unavailable` clears it. Already adopted environments
remain usable. Agent package delayed removals retain their pending entry until
deletion succeeds, retry on inventory access, and rediscover verified obsolete
versions through the existing package scan after restart. Active archive leases
still prevent deletion.
Continue bundle staging now waits for all parallel writes to settle before failure
cleanup. Existing settings atomic replacement and selected-runtime diagnostic
teardown already use `finally`; no global `.tmp` sweep or new diagnostic owner was
added. Native clients now accept an optional `temporaryRoot` for the same application
version/run launch root. `NativeTerminalClient` keeps histories and reusable caches
under its existing `rootDirectory`; only per-launch material moves to scratch.
DS Web keeps `DSH_HOME`, sessions and user preferences in place and supplies its
generated configuration through `web --patch <scratch-file>`. Its ledger also lives
in scratch. Both reuse the existing sibling PID metadata and await ledger close
before releasing material; unconfirmed child exit retains it. ConPTY registration
waits for the actual child PID, which can be unavailable immediately after spawn.
The collector recognizes `goodbuddy-native-dsh-*` alongside the existing prefixes.
The production wiring propagates `temporaryRoot` through the native coordinator to
both constructors without relocating the retained-history root.
Focused verification passed 60 tests, including a real Windows PTY, official DS Web
session persistence across restart, and delayed ledger-close cleanup. Typechecking
and scoped lint passed; this native follow-up made zero real model calls.
Final-source verification for this follow-up is separate from the earlier eight
real requests above: two real provider requests total, one per runtime. Current-source
Continue/OpenCode helpers ran on Linux x64 over pinned SSH, each executed its native
command tool, returned the real model answer, and removed launch files after exit.
A separate active-request shutdown probe passed for both helpers with zero real
provider requests. Four successful scenarios used deterministic command responses;
probe setup retries before these results did not reach a provider. A previous-process
version/run directory was reclaimed using actual dead PIDs. After the final
enumeration-error guard, a separate Host probe confirmed that the current collector
retains a real live child and reclaims that same run after its exit, with zero model
requests. The timed-out probe's ledger was audited and contained zero calls; all
task probe artifacts were removed locally and remotely. These probes cover the
changed helper and scratch lifecycle, not full Desktop UI or installation acceptance.
The seven primary suites passed 267 tests with 10 platform skips; related runtime,
native-terminal and diagnostic-owner suites also passed. Agent and web typechecks
passed. Full desktop typecheck and repository lint remain blocked by concurrent
`portable-storage-upgrade-fixture.ts` typing and `portable-storage-upgrade.mjs` lint
errors; unfiltered lint also visits generated files under `temp`. Focused lint for
the edited source/tests passed. The C01-C07 concurrent acceptance matrix remains
incomplete; these results do not establish full storage-architecture acceptance.
@@ -0,0 +1,188 @@
# Agent 功能维护约束
本文面向在 GoodBuddy 中新增功能的 Agent。它只记录做决定时必须遵守的维护规则,
不替代架构设计、性能规范或功能文档。若本文与代码或权威文档不一致,以已验证的生产
代码和下列权威文档为准,并修正文档链接或内容:
- [桌面存储与并发执行](../architecture/desktop-storage-direction.md):职责边界、存储所有权和操作边界。
- [桌面存储基础集成](../architecture/desktop-storage-foundation.md):`DesktopStorageClient`、domain operation 和生命周期。
- [桌面 Runtime 存储集成](../architecture/desktop-storage-runtime-integration.md):binding、model-call ledger 和关闭顺序。
- [性能原则](../architecture/performance-principles.md):唯一的性能数值、并发规则和评审清单。
- [桌面并发架构验收](../quality/concurrent-desktop-acceptance.md):真实生产路径、负载和证据要求。
- [运行时存储与后台读取](../architecture/runtime-storage.md):输出正文、预览、临时资源和当前迁移状态。
- [数据库迁移与大数据升级指南](./database-migrations.md):已发布用户数据的迁移边界。
- [AGENTS.md](../../AGENTS.md):临时文件、真实 Host 验证、测试和文档维护规则。
## 决策检查
开始编码前,逐项回答下面的问题。任一项没有答案时,先补充设计或查找现有机制,
不要直接增加新的 owner、队列、缓存或恢复记录。
- 数据的唯一权威来源和写入 owner 是什么?它是否已经存在?
- 该功能是否需要数据库事务、跨表快照、文件引用或运行时 ledger?若需要,操作是否落在对应 owner 内?
- Main 是否只负责 IPC 校验、凭据、生命周期和调度?是否把 SQL、大文件读写、哈希或模型工作留在了 Main?
- 新操作能否使用现有 domain port 和显式 allowlist?是否误把 SQL、回调、实例、函数或 handle 放进跨进程 DTO?
- 依赖链是否按“先提交,再通知、ACK、读取或返回成功”的顺序实现?提交未确认时是否禁止盲目重放?
- 排队取消、在途取消、执行结算和资源释放分别是什么时刻?容量是否一直占用到实际结算或确认的 Host 退出?
- 输出是预览还是完整正文?完整正文是否只有一个所属存储,引用是否随历史、分支和重启继续有效?
- 是否创建了临时文件?创建者、使用者、删除条件、子进程退出确认和删除失败后的重试位置是否明确?
- 新的操作数、排队数、字节数、句柄数和缓存是否有界?是否复用了已有预算并避免无限缓冲?
- 测试是否覆盖真实 UI 到生产实现的路径、失败、取消、重试、关闭和迟到响应?是否需要真实数据、模型或 Host?
- 受影响的架构、功能、质量、构建或运维文档由谁维护?是否已有同一事实的权威定义,新增内容能否只链接它?
## 存储所有权
桌面业务 SQLite 由一个 storage utility process 持有。Main 通过
`DesktopStorageClient.call(domain, method, args, { signal })` 使用显式操作;
`src/main/desktop-storage-contracts.ts` 的 allowlist 是可调用面的边界。Main 不得再
打开同一业务数据库,也不得通过 helper、同步回退或隐藏的 repository 构造绕过 owner。
远程 Agent 的数据库仍由远程 Agent 持有,不能因桌面存储改造而迁移。
新增持久化时,优先扩展已有 domain repository 和 owner operation。完整事务、跨表快照、
去重、checkpoint 与发布规则留在 owner 内;模型、网络、解析和文件准备在事务外完成,
进入事务后只校验当前前提并提交完整结果。需要多个语句的读快照,应增加一个有名字的
owner/reader operation,不要在 Main 中拆成多次 `await`。
Main 侧服务只持有异步 port 和普通 DTO。live repository、数据库连接、事务 callback、
订阅、函数、AbortSignal 和运行时 handle 不跨 utility transport。`StorageData` 允许的
值以当前契约为准;新增类型先确认序列化、大小计算和错误传播,再加入 allowlist。
文件和输出也有 owner。附件引用、document result、paged output backing、Runtime
binding 和 native model-call ledger 应通过既有 storage facade 访问。文件所在目录不等于
可删除的临时目录;持久化引用、WAL/SHM、native history、用户导出文件和仍被分支引用的
正文都必须按 owner 释放。
## 增加领域操作
按以下顺序定位扩展点:
1. 找到已有 repository、port 或 facade,以及它实际打开的数据库和文件。
2. 在 owner 内保留原有事务和 ID、顺序、去重、耐久性语义,必要时抽出可复用的内部函数。
3. 为外部调用增加一个有明确名称的 domain operation,更新类型 allowlist 和 transport handler。
4. 在 Main 组合处注入已有 typed port;调用者等待 Promise,不保存第二份可写事实。
5. 为提交前、提交后响应前、取消、失败、重试、关闭和重启增加针对性断言。
操作应表达业务动作,例如 `appendRemoteTaskEventsBatch`、`publishDocument` 或
`transition`。不要暴露 `executeSql`、任意方法代理、通用对象代理或把 repository
整个返回给调用者。一个已有操作能表达需求时,扩展其参数或返回值并更新权威文档,
不要平行创建同义 operation。
通知不是提交确认。持久化成功后才能发送依赖该事实的 change notification、远程 ACK、
模型下一轮输入或成功响应;通知失败要按现有语义处理,不能把已提交事实伪装成未提交。
## Main 与 utility 的边界
Main 保留窗口、可信 sender 和 schema 校验、凭据解密、网络与 Runtime 编排、OS API、
请求归属和进程生命周期。utility storage 保留业务 SQLite、owner-local 事务、升级后
的读 worker 生命周期和必要的同步 SQL。Parser、OCR、embedding、模型请求和远程 Agent
继续使用各自宿主,不因“改成异步”而塞入 storage 事务。
Main 里的同步数据库调用、同步文件 IO 和可能超过 16 ms 的计算都要按性能原则处理。
Promise 返回本身不能证明工作已离开 Main。启动顺序是先完成 storage readiness,再发布
依赖它的服务;关闭顺序是先停止生产者并等待必要的持久化,再关闭 ledgers、bindings、
读 worker、数据库 owner 和 utility process。失败或未确认退出时保留可能仍被 child
持有的文件,并将错误传播给调用方。
## 异步顺序与取消
为每个操作写清四个时点:入队、开始执行、提交或执行结算、资源释放。最小必要范围内
保持顺序:同一会话事件按序提交,同一数据库事务按序落盘;不同项目、会话和子任务
不得共用长时间的全局等待。
取消规则如下:
- 排队中的请求在 dispatch 前检查 abort,取消后不得执行。
- 在途请求先向实际 owner 或 Host 发送取消,调用方可以先得到取消结果,但容量只能在执行结算或确认 Host 退出后释放。
- 同步 SQL 无法被 IPC 消息打断;安全批次之间检查取消,已经提交的事务必须报告为已提交。
- 取消一个请求不得杀掉仍被其他请求使用的共享 Runtime、utility process 或 Worker。
- 迟到结果必须携带已有项目、会话、任务或 operation 归属,先通过归属检查,再改变代次、缓存或队列。
- 存储进程替换前等待旧进程退出并结算旧请求;未确认写入按 domain identity reconciliation 处理,不能自动重放写入。
`AbortSignal` 是 transport option,不是业务 DTO 字段。不要用 UI 隐藏、组件卸载或清空
按钮代替服务层取消。控制和取消不得等待新的执行槽位。
## 输出引用与临时文件
短输出可以保留有界预览;超过预览上限的输出必须由所属 backing store 保存完整正文,
并在后续模型 `output_read`、历史和分支路径中复用同一 handle。分别核对模型收到的内容、
工具事件预览和历史正文,不能用最终回答代替工具全文,也不能把正文再次复制成临时全文。
输出 writer 的 append 必须被生产者等待;容量错误要显式传播,不能丢块或无限排队。
创建临时配置、插件、socket 或 launch 目录的函数承担创建失败清理。删除前关闭拥有
文件或目录的 child、ledger 和句柄,并确认 child 已退出;无法确认时保留材料和 owner
metadata,等待后续有界回收。Agent 和测试产生的验证文件使用仓库根目录 `temp/<task>/`;
产品运行时临时资源继续使用各自 owner 管理的运行时路径,不能因为位于仓库 `temp/` 就获得删除权。
远程 Host 使用专用 `/root/tmp` 子目录。不要扫描或删除用户 Temp、未知归属目录或仅凭名称和年龄
判断为过期的资源。
持久化目录可以位于 `temp/` 下,位置本身不能授权删除。删除前先检查 attachment、
document result、history、branch 或其他 durable reference;释放一个调用引用不能删除
仍被历史引用的正文。旧数据清理沿用所属 owner,不增加全局 Temp sweep。
## 并发预算
数值只引用[性能原则](../architecture/performance-principles.md),负载和证据只引用
[并发验收](../quality/concurrent-desktop-acceptance.md)。当前 storage client 的请求、
读写 pending 不设置产品侧 admission 上限,负载透传给 storage host;paged output 的 8 MiB capture、
每 writer 1,024 个 pending chunks、4,096 个 active/retained handles 和 64 KiB backing
write 也必须服从当前实现与测试。修改前先确认代码中的常量和适用范围,不在本文件复制
一套新数字。
只有用户明确配置、外部协议要求、数据结构安全,或已有实测证明不限制会导致崩溃、
数据损坏或不可恢复失败时,才设置上限。读写请求默认透传给所属 Host;不能用内部
并发数、队列长度或容量数字替用户决定吞吐。仍禁止静默丢事件、无限重启、按项目预建
服务、为每个调用新增 scheduler,或用串行夹具掩盖真实负载。
Worker 发生故障时可以进入已有退避和恢复流程;这不应被伪装成负载上限。正常排队的
解析、存储和 Host 工作不能因为一个未经测量的队列数字直接返回 busy。
`ReadonlyQueryReader` 同时服务只读 Worker 和桌面 storage client。只有调用方显式传入
`maxPending` 时,`waiting` 队列才承担有界等待语义;生产 storage client 不传该选项,
请求直接交给 storage host。这个共享类不是第二个 storage owner,不能因为看到等待队列
就给生产路径恢复默认 admission 上限。若未来实测证明某个边界需要容量策略,应先说明
资源、故障和测量依据,再拆出最小的显式 wrapper 或增加调用方配置。
## 测试与真实验证
单元测试用于确定字节边界、事务回滚、顺序、归属、取消竞态、失败清理和重试。行为改动
还必须覆盖生产组合:正常 UI action 经过 Preload、Main、storage/Runtime 宿主并产生
实际结果。mock bridge、独立 SQL、只测试握手或只测试 schema 都不能替代这条路径。
需要持久化时使用真实 SQLite owner 和隔离数据库;需要文件时使用真实异步文件;需要
Runtime 时使用本地确定性模型验证工具、事件、取消和关闭。不要把真实提供商网络速度
混入性能基准。需要模型能力证明时,配置检查不够,必须发起实际生成或工具请求,并
记录实际请求数和范围。
涉及桌面到 Agent 的 Runtime、安装、workspace、模型 bridge 或生命周期时,按
`AGENTS.md` 在共享 Linux x64 Host 使用当前源码验证。覆盖面应对应改动,包括断开、重连、
取消或重启;远程临时目录必须专用,验证后清理。未能连接 Host 时记录为验证阻塞,不能
用单测或旧发布包替代当前源码证据。
性能改动先取得基线,至少按权威验收矩阵记录 offered load、排队、吞吐、排空、资源高水位、
Main/Renderer 指标和样本条件。功能测试通过不等于并发预算通过;未测平台和缺失指标保留
为缺口。
## 文档与重复设计
代码行为改变后,检查受影响的架构、功能、质量、构建和运维文档,更新真正的 owner、
顺序、限制和验证证据。每项事实只保留一个权威定义:本文件写维护决策,架构文档写
职责与边界,性能原则写数值,验收文档写场景和证据,功能 README 或 progress 写功能
行为和状态。其他地方使用链接,不复制完整表格、状态机、预算或验收结果。
新增机制前先搜索同类名称、队列、owner、transport、shutdown 和测试。若现有机制只缺
一个操作、一个容量检查或一个清理分支,扩展它并补测试;只有现有 owner 无法表达真实
边界时才提出新设计,并在对应架构文档中记录问题、取舍和验证条件。未提交的实验代码、
测试夹具或接口存在,不等于生产路径已经接入。
## 允许与禁止的模式
| 允许 | 禁止 |
| --- | --- |
| 在已有 storage domain 增加命名 operation,并由 owner 完成事务 | 在 Main 新建同一 SQLite 的第二个连接、同步回退或隐藏 writer |
| 先在 Main 完成模型/解析,再把不可变 DTO 交给 owner 原子发布 | 在 SQLite 事务内 `await` 模型、网络、IPC 或文件回调 |
| `storage.call('assistant', 'appendRemoteTaskEventsBatch', [batch], { signal })`,提交后再 ACK | 暴露 `executeSql`、任意 repository proxy,或把 live repository/callback 放进 DTO |
| 使用同一 output handle 进行预览、分页、历史引用和 release | 每个工具结果写一个临时全文,再复制到 history 或用最终回答代替全文 |
| child 退出确认后删除创建者拥有的 launch material | 按目录前缀、年龄或路径位置删除未知归属的用户文件 |
| 取消在途请求并等待 settlement,再归还容量 | 调用方停止等待后立即释放槽位,或取消一个请求时杀共享宿主 |
| 用真实 owner、隔离数据、确定性模型和生产 UI 路径验证 | 只验证 schema、mock bridge、旧包或 utility ready 就宣称功能完成 |
| 更新一个权威文档,其他文档链接它 | 在多个文档复制同一预算、状态机、所有权表并分别维护 |
@@ -0,0 +1,74 @@
# Desktop 0.15.12 / Agent 0.15.5 发布准备
日期:2026-10-05。基线 `v0.15.11`,冻结源码
`2be57492e6de2d639597d0fde740a18b917d7dbb`,8 个提交、144 个文件。
检查时工作区干净,只纳入已提交实现和发布准备;保留后来出现的并行工作。
标准六平台 Desktop、三平台 Agent,不含 LoongArch。
## 范围
- Telegram 私聊配置、数字白名单、whoami 设置顺序、系统代理路径、
轮询恢复、待发回复、业务错误与跨层 retry_after;通道图片结果合同及钉钉错误修复。
- 自定义回顾天数和本地日期,包含结束日、今天截止提交时刻,
保留增量及显式重新整理规则。
- 回顾项目隔离、暂停后发布校验、失效故事目标拒绝;已删除会话从未发布
清单省略、混合批次和依赖导航重算,历史结果不变,已发模型输入不可撤回。
- 监督临时错误最多重试一次,认证/配置/取消/超时/容量不重试,
保留失败流用量,额外尝试可能增加费用。超容量中止保留已收到用量,
正文清空,最多再消费一秒用量事件,仍返回容量错误。
- 概览不预载图谱、来源详情按需加载、现有 Worker 重读取、索引与批量查询、
摘要缓存、选择列表窗口化及键盘焦点;发布快照只构造一次,仍在原 Main 事务。
- 长消息展开状态仅存用户覆盖项,删除来源清理;历史计时缺失不显示为零。
- 本地 OpenCode 提交失败及时终止;Agent 原生启动和路由清理不占全局队列,
迟到 Session/路由清理,保持取消、peer、回放去重。
独立守护进程生产实现改变,必须发 Agent 0.15.5,最低 Desktop 0.15.12;
wire 2.0/runtime-acp 6 和 Runtime/Node/Electron 锁不变。
## 数据与性能边界
schema 61 定向扩展 projects/conversations 通道 CHECK,事务中校验并恢复
writable_schema/defensive 设置,保留行、rowid 和索引;schema 62 仅新增监督索引。
升级前关闭应用备份完整数据目录(数据库、相邻 notes、设置),旧客户端回退需恢复备份。
没有进行独立存储服务迁移,不把架构方向文档写成已实现。
已审阅[性能原则](../architecture/performance-principles.md)与
[阶段审计](../review/stage-audit-fixes-2026-10-05.md)、
[KISS 实测](../review/kiss-local-fixes-2026-10-05.md)。
三轮交替测量仅限指定合成库、设备和源码路径。schema 61→62 的 55 表、
32947 行含 rowid 哈希一致及完整性检查通过,不代表全部用户库。
大批发布仍约 271~303ms、部分大响应和冷模块存在超预算样本,不宣称全部达标。
最终删除会话改动另有四文件 95 项回归,未做完整 UI 或大库删除延迟测量。
## 开发证据与发布验证分开
- 第一阶段审计:7 次外部文本请求(Host OpenCode 2、Continue 2、
本机 OpenCode 1、监督服务 2),0 次图片;6136 测试通过、87 跳过,
typecheck/lint 通过。Host 工具、断开恢复、Agent 提升、回放和启动挂起取消
经生产路径验证,结束后所属进程清零。
- K07/K08:8 次外部文本 HTTP 请求、均 200、0 次图片,含首次预算
不足尝试的 2 次;当前源码两 Runtime 的路由清理、peer、取消、工具、
恢复及去重通过,自有进程和目录清零。隔离暂存源码全量 6212 通过、
87 跳过,typecheck/lint 通过,不含之后的删除会话改动。
- 上述两阶段共 15 次外部文本调用属于既有开发记录,本次发布准备新增调用零。
- Telegram 内部链路有真实 Electron/IPC/SQLite 与 loopback API/模型证据,
真实 Bot 加模型完整收发仍待验证,旧独立 Node API 成功不替代该验收。
本次不使用凭据向外部收件人发送消息,不把内部测试当作真实平台通过。
- 最终精确候选仍需 CI 全量验证及生产构建;按用户要求不执行本地
测试套件、lint、typecheck、生产构建或安装包探针,只校验发布元数据。
## 发布检查
同步审阅并更新双语 FEATURES、Desktop 和 Agent 发布说明。
推送双远端、等待精确源码验证后创建不可变双版本标签;
公开核验六平台 Desktop 20 项资产/12 安装包、macOS 签名公证、双源索引、
网站 12 种标准选择,以及 Agent 三平台目录/签名和生产读取器。
不下载 Desktop 安装包,不发布已提交宣传稿。
## 精确候选 CI
`36e671a0` 的 Desktop CI `37273689682`:6254 通过、57 跳过、1 失败,
生产构建未执行;Agent CI `37273689788` 通过。
唯一失败为 Telegram Electron 探针启动时真实 safeStorage 不可用。
Ubuntu 验证环境增加 gnome-keyring/D-Bus,在独立会话中以随机临时密码解锁
CI keyring 后运行 sandboxed Electron 测试;不使用明文回退、不替换加密接口,
保留生产配置加密保存断言。密码仅经管道交给临时 keyring,不写源码或日志。
@@ -0,0 +1,51 @@
# Desktop 0.15.13 / Agent 0.15.6 发布准备
日期:2026-10-07。基线 `v0.15.12`,冻结源码
`c1e4bbf66a418dbd4e0f99a36ff0522dbf610553`。只纳入已提交实现和发布准备,
保留开始准备后出现的并行工作。标准六平台 Desktop、三平台 Agent,不含 LoongArch。
## 范围与兼容性
- 一个 Desktop storage utility process 统一拥有 Assistant、Knowledge、附件、
Runtime binding/ledger 等业务 SQLite;Main 通过类型化异步操作访问。
保留数据库路径、表、ID、事务、提交顺序和远程 Agent 数据库。
- storage utility 使用现有读取 Worker 和受界定 IPC,覆盖 readiness、取消、
容量、进程退出、关闭排空和错误传播;移除生产 Main 业务 SQLite owner。
- schema 仍为 62,无数据迁移;升级前仍备份完整数据目录、notes 和设置。
- 直连模型工具失败后可继续下一轮,分页输出保持有界;Magic Notes 标签、
监督来源别名、设置直达预加载、文件元数据和 Composer 过渡同步修复。
- Agent 生产 Runtime/模型桥有变化,需 0.15.6;runtime/acp 6、wire 2.0、
Node/Electron/OpenCode/Continue 锁不变。
## 验证边界
storage foundation focused 结果:utility process 真实 Electron 编排、监督
heartbeat/回顾叶子/导航、异步 Worker、存储帧高水位约 248,332 bytes Main、
252,578 bytes storage;focused 70 项跨 11 suites 通过,Node/Agent/Renderer
typecheck 和 scoped lint 通过。工具失败继续、分页输出、Magic Notes 和监督
别名另有定向回归。
并发验收文档明确的 C01–C07 和 A01–A10 完整矩阵、跨平台 storage process
安装、低端机对照及完整 UI→Preload→Main→storage→SQLite 验收仍不是这份文档
中的已完成事实。不得把 focused 结果或架构文档目标写成全部 PERF-15/PERF-16
已验收。Agent 远程路径需在当前源码 CI/Host 规则下重新验证。
## 发布门槛
按用户约定不运行本地测试套件、typecheck、lint、生产构建或安装包探针;
先校验发布元数据,推送双远端,要求精确候选 main CI 和生产构建通过后再创建
Desktop/Agent 不可变标签。通过后核验六平台 20 项 Desktop 资产、macOS 签名公证、
Agent 三平台目录、OSS/GitHub 元数据和网站 12 种标准下载选择。
## 精确候选首轮 CI
`fbfafa15` 的 Agent CI 已通过。Desktop CI `37636800633` 为 6430 通过、
58 跳过、3 失败,生产构建未执行:
- C05 storage 进程故障 fixture 没有稳定观察到拒绝;
- utility-process fixture 的临时数据库父目录权限在 Linux runner 上未满足;
- readonly reader 测试把快速完成的 worker 请求固定断言为拒绝。
修正只涉及测试稳定性:显式把隔离目录设为 `0700`,允许快速 worker 在终止
竞争中完成(专用 crash 测试仍覆盖拒绝),并保留 storage 故障恢复和已提交
数据断言。下一候选 CI 仍必须重新验证这些场景;不跳过 storage owner 测试。
@@ -0,0 +1,25 @@
# Desktop 0.15.14 发布准备
日期:2026-10-09。基线 `v0.15.13`,候选源码 `8f1b6007986f31fb5e2a80e80021602a13fa20a2`。
本候选只更新 Desktop;Agent 保持 `0.15.6`,不创建新的 Agent 标签。标准六平台 Desktop,
不含 LoongArch。
## 范围与兼容性
- 恢复旧版因监督来源变化或移除而失败的历史回顾的继续操作。
- 来源在回顾期间变化时,在有限次数内重新冻结处理;持续变化的来源之后省略,
已删除、归档或不可用来源沿用既有省略规则,其他来源继续保留进度。
- 保留暂停、失败、取消和已保存批次的监督活动状态及项目范围、来源处理规则。
- 设置分类显示对应图标,帮助用户更快找到各类设置。
- 不新增数据库 schema,继续使用 schema 62;Agent、runtime/acp、wire 和运行时锁版本不变。
## 验证边界
本候选包含监督存储、Worker、服务和 Renderer 回归测试,以及历史失败回顾继续操作的
界面覆盖。完整并发验收矩阵、跨平台存储进程安装和其他未完成验收不在本候选中宣称通过。
## 发布门槛
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
要求精确候选的主分支 CI 和生产构建通过,再由用户确认后创建不可变的 `v0.15.14` 标签。
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
@@ -0,0 +1,23 @@
# Desktop 0.16.0 发布准备
日期:2026-10-09。基线 `v0.15.14`,候选源码为本次发布准备提交。
本候选只更新 Desktop;Agent 保持 `0.15.6`,不创建新的 Agent 标签。标准六平台 Desktop,
不含 LoongArch。
## 范围与兼容性
- 专家和团队角色选择移动到 Composer 输入框顶部,发送前更容易发现。
- 角色选择按会话保存,切换会话不会带入其他会话的选择。
- 历史消息保留请求发送时使用的角色,即使角色后来重命名或删除,历史上下文仍可识别。
- 不新增数据库 schema,Agent、runtime/acp、wire 和运行时锁版本不变。
## 验证边界
本候选包含 Composer、会话选择状态、历史消息元数据和角色选择的回归测试。
完整跨平台安装、远程 Host 和未覆盖的整包验收仍以 CI 与发布工作流结果为准。
## 发布门槛
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
要求精确候选的主分支 CI 和生产构建通过,再由用户确认后创建不可变的 `v0.16.0` 标签。
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
@@ -0,0 +1,18 @@
# Desktop 0.16.1 发布准备
日期:2026-10-10。基线 `v0.16.0`,候选源码包含 `d9dcb65d` 和 `c7f09e1e`
两个 Composer 视觉修复提交。本候选只更新 Desktop;Agent 保持 `0.15.6`,
不创建新的 Agent 标签。标准六平台 Desktop,不含 LoongArch。
## 范围与兼容性
- 消息内容延伸到 Composer 输入框轮廓前再淡出,不再被输入区上方的实色区域提前截断。
- 浅色和深色主题使用匹配的消息到输入框渐变,输入框焦点边界不再形成粗边。
- 滚动到底部按钮保持与新的输入区过渡相配的可见位置。
- 不新增数据库 schema,Agent、runtime/acp、wire 和运行时锁版本不变。
## 发布门槛
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
要求精确候选的主分支 CI 和生产构建通过,再创建不可变的 `v0.16.1` 标签。
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
@@ -1,5 +1,51 @@
# 应用导航实施进度
## 2026-10-05:展开状态缓存复查
独立复查发现,首次 A10 修复在每个折叠项挂载时保存默认值;无引用的消息也会因引用 Hook 留下一条记录。下节首次验收只检查了 DOM 上限,没有覆盖这部分缓存。新增回归在该实现上失败:分批挂载 10,000 个未操作的消息作用域后,预期缓存为 0,实际为 10,000;恢复默认状态后也没有释放条目。
现改为只保存偏离自动默认值的用户选择,恢复默认时立即删除覆盖项。消息更新后,清理逻辑只检查已保存的覆盖项,借用历史面板已有的消息索引定位来源,清除已删除消息、工具/输入/推理块/子代理/引用及已过期的流式默认值对应的记录。清理不在滚动或渲染期执行。正常的虚拟行卸载、子代理收起继续保留有效选择,行卸载期间完成的推理仍按原规则自动收起。
本次执行结果:
- `npm test -- --project unit src/renderer/src/message-expansion.test.tsx src/renderer/src/ChatHistoryPane.test.tsx src/renderer/src/ChatTimeline.test.tsx src/renderer/src/SupervisorWorkspace.test.tsx`:4 个文件、112 项通过。其中新增 4 项覆盖 10,000 个未操作作用域零缓存、用户选择跨卸载保留、恢复默认释放、消息及折叠项删除、未挂载推理完成。
- `npm test -- --project unit src/renderer/src/App.test.tsx -t 'keeps each conversation history window|isolates history and task strips'`:2 项通过,346 项未选中。
- `npm test -- --project integration tests/message-expansion.electron.test.ts`:三轮交替比较后,最终测量探针调整也单独复跑通过。`npm test -- --project integration tests/frosted-glass.electron.test.ts`:1 项通过,继续覆盖浅深主题、原生点击、跳底、锚点和缓存切换。
- `npm run typecheck`、`npm run lint` 通过。该组定向验收后,完整集成测试为 6,136 项通过、0 失败、87 跳过,见[阶段修复验收](../../review/stage-audit-fixes-2026-10-05.md#最终检查);本组模型调用 0 次。
Electron 环境与合成消息结构同下节。此次比较对象为首次 A10 的默认值缓存实现和本次稀疏覆盖实现,不是 `126dbc57`。测试通过 `GB_EXPANSION_DENSE_SOURCE` 加载修改前保留的模块副本,其余生产 Renderer 源码相同。fixture 临时观察首次展开状态写入,捕获该 Map 后恢复原 `Map.set`;产品未增加诊断接口。每个数据量先进行 200 次未操作导航,实际见到 1,397 个不同消息行(一轮旧实现为 1,401),再原生展开 13 条消息的推理、工具结果和输入,返回原行确认恢复,用 Space 收起一项,最后删除消息。
| 历史消息数 | 旧实现:未操作导航后条目 | 新实现:未操作导航后条目 | 用户操作后:旧/新条目 | 删除消息后:旧/新条目 |
| --- | --- | --- | --- | --- |
| 2,000 | 5,600 | 0 | 5,708 / 38 | 5,708 / 0 |
| 10,000 | 5,604 | 0 | 5,904 / 38 | 5,904 / 0 |
以上计数在三轮比较中一致。交互阶段两版均最多采样到 15 个消息行、926 个 DOM 元素,原消息的三个展开项均恢复;A09 迟到来源响应检查也继续通过。计数针对缓存条目,不是浏览器堆内存字节数。
| 消息数 | 旧实现每轮导航中位数(ms) | 新实现每轮导航中位数(ms) | 旧实现每轮 p95(ms) | 新实现每轮 p95(ms) |
| --- | --- | --- | --- | --- |
| 2,000 | 38.8 / 38.8 / 38.4 | 38.8 / 39.1 / 38.5 | 40.4 / 39.6 / 46.9 | 39.2 / 46.7 / 40.1 |
| 10,000 | 38.7 / 39.1 / 38.9 | 38.8 / 38.9 / 38.6 | 39.7 / 39.5 / 46.9 | 39.4 / 39.3 / 46.6 |
导航计时仍为目标聚焦后再等待两个动画帧,每轮 14 个样本;此次先扫过历史,不能与下节首次进入的计时直接比较。三轮比较中,200 次未操作导航的总耗时,旧实现为 4,753.5–5,080.5 ms,新实现为 4,820.3–5,228.8 ms。数据证明默认条目和删除后的条目不再积累,未证明提速;未测堆内存峰值、低端机节流或完整 App/生产 IPC。
## 2026-10-05:虚拟消息行的展开状态
聊天历史面板通过 `message-expansion.tsx` 按消息、子代理及折叠项的稳定标识保留展开状态。虚拟行卸载后只保留状态值,不保留 DOM;仍沿用最多 8 个近期交互行及焦点、选区、问题的挂载保护。阅读行为见[聊天阅读状态](./ui-design.md#聊天阅读状态)。本次没有修改 Runtime 执行或消息持久化。
旧代码上的新增组件回归在返回原消息时失败:预期展开,实际收起。最终三文件 108 项通过,覆盖嵌套工具、父子同名调用隔离、手动收起、行卸载期间流式完成、既有焦点/选区及平滑滚动。命令:`npm test -- --project unit src/renderer/src/ChatHistoryPane.test.tsx src/renderer/src/ChatTimeline.test.tsx src/renderer/src/SupervisorWorkspace.test.tsx`。另有会话存储、同步、流式缓冲、窗口模型和侧栏回归 48 项,以及 App 历史加载、重试、热切换、流式隔离和超过 500 条消息的定向回归 5 项通过。
真实 Electron 使用当前 `ChatHistoryPane`、样式及 Markdown 渲染,数据为 2,000/10,000 条合成助手消息,每条含正文、推理、工具结果与输入。Windows x64、Electron 44.5.1、1280×800、缩放 1;原生鼠标展开 13 条相距较远的消息,确认原行卸载后再返回,并用原生 Space 操作。基线为临时 worktree 中的 `126dbc57`。三轮交替运行 `npm test -- --project integration tests/message-expansion.electron.test.ts`;基线运行另设 `GB_HISTORY_SOURCE` 指向该 worktree。每轮、每种数据量采样 14 次导航;计时从请求消息定位到目标 article 获得焦点后的两个动画帧,含这两个帧的等待,不等同于纯渲染耗时。
| 消息数 | 基线每轮中位数(ms) | 修复后每轮中位数(ms) | 基线每轮 p95(ms) | 修复后每轮 p95(ms) |
| --- | --- | --- | --- | --- |
| 2,000 | 47.0 / 39.2 / 39.3 | 39.4 / 39.0 / 39.1 | 177.6 / 136.4 / 137.4 | 129.7 / 128.8 / 138.7 |
| 10,000 | 47.2 / 39.4 / 39.2 | 38.8 / 39.2 / 39.5 | 56.0 / 40.2 / 43.7 | 41.4 / 39.7 / 40.0 |
两版在两种数据量下的采样最大值均为 15 个消息行、926 个 DOM 元素;基线三轮均丢失原消息的三个展开状态,修复后三轮均恢复。数字只证明本场景的 DOM 数量有界,计时存在波动,不据此宣称提速或整体性能预算达标。该 Electron fixture 替换了数据来源,不是完整 App、生产 IPC 或真实模型验收;模型调用 0 次。
既有 `npm test -- --project integration tests/frosted-glass.electron.test.ts` 单独复跑通过,覆盖浅深主题、原生点击、跳底、阅读锚点、会话缓存、加载更早消息及 960×540 短窗口。首次与监督者布局测试合并运行在工具的 60 秒期限内未返回结果,不计为通过;`tests/supervisor-layout.electron.test.ts` 随后单独后台运行通过,总耗时 65.26 秒,stderr 为空。最终完整 typecheck、全仓 lint 和修改范围差异检查通过。首次 lint 曾受其他并行修改中的三个 Runtime 错误阻断,后续复跑已通过。未运行完整测试集或低端机节流测量。
## 2026-10-04:新建本地项目必选目录
对应 FR-14 的项目创建入口,规则见 [PRD](./prd.md#fr-14-项目活动汇总)及[界面设计](./ui-design.md#10-项目活动汇总)。创建表单已增加中英文必填提示、无障碍关联及空目录提交检查;共享创建 schema 拒绝空白路径,更新 IPC 使用独立 schema,保留历史空目录项目的保存能力。
@@ -270,6 +270,14 @@ CPU 4% · 内存 620 MB · 桌面进程 7
导出也适用于消息通道会话。持久化与刷新规则见
[会话置顶存储](../assistant-workbar/execution-history-storage.md#会话置顶)。
### 聊天阅读状态
在同一历史面板内,滚动到别处再返回时,消息的推理、工具结果、工具输入、引用和子代理详情保留用户的展开或收起选择。子代理收起后重新展开,其内部工具详情也保留选择;不同消息及父子工具分别记忆。状态保留在历史面板中,不依赖把所有已展开的消息留在 DOM 中;面板被缓存淘汰或应用重启后不恢复这些临时阅读选择。
未操作的折叠项不留下默认状态记录;用户恢复默认选择、消息或对应折叠项被删除后,释放相应记录。正常滚动卸载不视为删除。
推理仍在流式输出时默认展开、完成时自动收起,流式期间手动收起不因滚动重挂载而重新展开。有焦点、文本选区端点、待回答问题和待审批项的消息继续按既有规则保留挂载;平滑跳到底部及滚动锚定行为不变。
## 9. 未来插件入口
FR-15、US-F1/US-F2 的市场 URL、YAML 导入导出及 ShareServer 分发在后续设计中定义。
+19 -1
View File
@@ -1,9 +1,27 @@
# 工作栏实现与验证进度
## 2026-10-05:文件列表只保留修改时间
按 [FR-WF3、US-WF2](./workspace-files.md) 移除创建时间列及排序入口,修改时间采用紧凑格式,
完整日期保留在悬停提示中。更多操作与时间共用行尾空间,悬停、聚焦及菜单展开时文件名宽度不变。
本机与远程共用 Renderer,无文件访问或 Agent 协议变化。
验证:`WorkspaceFilesPanel.test.tsx` 41 项通过,Renderer TypeScript 检查、相关文件 ESLint
及 `git diff --check` 通过。尚未进行真实 Electron 窗口视觉验收。
## 2026-10-05:Runtime 提交失败与启动取消
本机 OpenCode 提交失败后会结束所属 SSE 等待;远程原生 Session 启动等待期间,
取消与其他 binding 的控制请求可继续处理。保留无默认 Prompt 总时限、共享 peer
隔离、取消后的会话清理及原有事件顺序。实现规则见
[进程复用设计](./runtime-process-reuse-technical-design.md),旧/新回归、当前源码
Windows/Linux x64 实测、真实数据检查及本轮 5 次文本模型调用计数统一记录在
[统一执行进度](../unified-execution/progress.md#2026-10-05-runtime-生命周期审查修复)。
## 2026-10-04:移除成果页,工作区导入与文件时间
对应 [FR-WF1~FR-WF3、US-WF1~US-WF3](./workspace-files.md)。成果 Tab、目录注册及独立导入
界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间与可用创建时间,支持同级排序,
界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间(2026-10-05 收敛为唯一时间列),支持同级排序,
向所选目录导入原件。本机及远程共用分块管理动作,同名文件不覆盖,失败项独立反馈并清理部分文件。
来源检查确认:`AssistantDatabase.createImageArtifact` 把图片保存为 `artifacts.inline_content`
@@ -17,6 +17,11 @@ Host 环境。2026-09-14 的已发布 Desktop `0.13.2` / Agent `0.11.25` 已包
## 运行与失败
OpenCode 提交请求失败时直接显示该失败,不等待没有被接受的请求产生结束事件。
远程 Runtime 在原生 Session 启动期间也可取消;启动等待不阻塞其他会话的状态读取。
取消及清理范围仍限于所属请求,完整规则见
[Runtime 进程复用技术设计](./runtime-process-reuse-technical-design.md)。
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
- 前台同一请求的待答问题按公开 `questionId` 去重,按到达顺序保存在消息的
`pendingQuestions` 中。每次显示队首表单;有多个待答请求时显示数量。后续提问和重复
@@ -168,6 +168,11 @@ Session 和 Prompt 路由删除产品模式,按实际能力和请求归属接
使用独立的 ACP 取消路径。
现有 `snapshot: false` 必须保留。
提交 Prompt 的 Promise 与 SSE 消费同时运行。提交返回错误、抛错或控制请求超时后,
立即中止本请求的 SSE 订阅并保留提交错误;不能等待服务端为未接受的 Prompt 发送
`session.idle`。回收仍先 abort 订阅再结束迭代器,随后完成该 Session 的有界取消,
不关闭其他会话的订阅或共享 Server。生产运行不增加整个 Prompt 的默认总时限。
`mcpMutationTail` 目前属于 Runtime 对象;共享后同一原生目录的变更必须由同一 owner
串行协调。注册名称、工具可见性、knowledge token 和清理仍按请求区分,不能用一个全局
“当前 MCP 列表”覆盖同目录或跨目录的其他请求。探针未覆盖动态 MCP,这是实施验收项。
@@ -282,6 +287,22 @@ Agent 当前 Runtime installation + 兼容配置
- 同一 PID 的多个 binding 不代表同一权限或同一 operation。Host/installation identity
变化仍使用已有失效和退役规则,不把不同 Host、账号或 Runtime generation 合并。
`runtime/startPrompt` 校验及 owner 登记经过 backend 控制队列,原生握手、Session
创建/恢复和模型路由准备在队列外等待。协议接收端也不等待该 RPC 的最终响应才分派
后续帧;未完成的响应继续占用已有 request channel,沿用连接通道容量限制及 FIFO
写入。其他方法的分派顺序保持不变,Prompt 终态仍经 backend 控制队列提交。
同一 binding 的相同启动请求共用 Promise,内容不同的并发启动返回冲突。
启动等待属于 Session owner,可以被取消、启动 watchdog 或关闭中断。取消后不得
发送 Prompt;原生调用迟到返回的 Session 只关闭自身,不能回收仍服务其他 binding
的共享进程。原生握手本身仍由进程 owner 共享,一个等待者退出不取消其他等待者。
模型路由撤销不等待原生 HTTP 响应后才释放 backend 控制队列;broker 关闭、凭据释放
及 terminal 提交仍按原顺序执行。启动准备保存原 transport 和取消 signal,安装完成时
若已取消,按原 Session/operation 再撤销一次,避免早先清理被迟到安装覆盖。撤销沿用
helper 的 operation 匹配检查,不删除同 Session 的其他 operation。HTTP 沿用 10 秒
超时,进程退出或 transport dispose 中止未完成请求;无需新的持久状态或协议阶段。
原生 `chat.message` 在模型请求前设置该 Session 的工具与模型路由,不再按产品模式
过滤 Shell、编辑或写入;请求级端点与子会话归属仍独立。
OpenCode 1.18.29 的 ACP 不转发未注册子 Session 的权限请求,且原生子代理只继承父
@@ -15,16 +15,17 @@ Host 的项目目录。支持二进制和空文件,不调用文档解析器,
显示。写入失败时尝试移除本次新建的部分文件,清理失败则报告残留路径。取消文件选择不写入,
导入期间禁用重复提交。完成后刷新目录。当前不提供传输中取消或进度百分比。
FR-WF3:名称默认升序。名称、修改时间和有可用数据时的创建时间列均可点击切换排序;首次
FR-WF3:文件列表仅显示名称和修改时间,不显示创建时间列。名称默认升序,两列均可点击切换排序;首次
选择时间列按最新优先。每一级目录独立排序且目录优先,缺失时间放在同类条目末尾,显示 `-`。
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。窄栏横向滚动仅发生在
文件视口,工具栏保持可见。
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。修改时间常显月日和时分,
悬停显示完整日期时间;行悬停、键盘聚焦或菜单展开时,在时间位置显示更多操作,文件名宽度保持不变。
窄栏中文件名省略,不因时间列撑宽列表。
US-WF1:选中子目录后导入二进制及空文件,目录刷新后可见,原文件不变。同名项失败而已有
内容保留;成功项仍落在所选目录。
US-WF2:切换排序后文件按真实文件系统时间显示;不支持创建时间的文件系统不显示创建时间列,
混合数据中缺失值保留为空。Linux 的 inode 状态变更时间不冒充创建时间。
US-WF2:切换排序后文件按真实修改时间显示;即使文件系统提供创建时间,列表也只显示修改时间。
缺失修改时间显示 `-`。文件夹使用目录本身的修改时间,不递归计算子文件的最新时间。
US-WF3:升级前会话图片在聊天和重新打开应用后仍可读取;打开工作区不会把会话图片或附件复制
到项目目录。生成的普通文件由 Runtime 文件工具写入工作区并出现在同一目录列表。
@@ -0,0 +1,17 @@
# 聊天历史导航
聊天历史导航负责长会话的连续滚动、窗口化渲染,以及后续的对话 minimap。第一阶段先取消向上滚动时每次增加 80 条消息的产品限制,保留已有的消息窗口化机制;第二阶段再评估 minimap。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [产品需求](./prd.md) | 用户问题、范围、验收和非目标 |
| [技术设计与测试计划](./technical-design.md) | 当前实现边界、实验分组、性能指标和执行步骤 |
| [实施进度](./progress.md) | 已验证事实、命令、结果和遗留问题 |
## 术语
- **历史窗口**:允许滚动访问的消息范围。第一阶段扩大为当前已加载会话的全部消息。
- **DOM 窗口**:实际挂载消息组件的范围。继续由消息高度估算和最多约 60 条挂载消息控制。
- **minimap**:对话右侧的轻量纵向全局导航概览,不等于完整渲染所有消息。
@@ -0,0 +1,34 @@
# 聊天历史连续滚动
## 用户问题
长会话向上阅读时,当前界面每次只增加 80 条消息,并在顶部显示“加载更早的消息”控件。这个控件把连续滚动切成多个操作,用户无法像 VSCode 一样直接拖动滚动条或连续滚到会话顶部。
## 目标
- 已加载的当前会话可以从底部连续滚动到顶部。
- 消息仍按可见范围挂载,长会话不会因为取消 80 条限制而一次性创建全部消息组件。
- 滚动位置、锚点、消息展开状态和流式输出行为保持稳定。
- 为后续 minimap 提供完整的消息索引和可定位滚动范围。
## 范围
- 取消真实聊天会话的 80 条历史窗口增量。
- 保留现有消息窗口化、占位高度、滚动锚定和按需挂载。
- 对 100、500、2,000、10,000 条消息进行功能和性能对比。
- 记录 minimap 的下一阶段性能测试入口,但本阶段不实现 minimap。
## 非目标
- 不改变未打开会话的摘要加载策略。
- 不要求一次从数据库加载所有未加载的会话历史。
- 不取消最多约 60 条消息组件的 DOM 窗口化限制。
- 不在本阶段加入 minimap、消息摘要接口或新的 IPC。
## 产品验收
- 打开一个已加载历史的会话后,顶部不再出现“加载更早的消息”按钮。
- 用户可以连续滚动到第一条消息,滚动位置不会因消息挂载或高度测量而跳动。
- 2,000 条消息会话仍保持窗口化渲染,实际挂载消息数量维持现有上限附近。
- 流式输出、输入、消息展开、附件和工具输出行为没有回归。
- 若会话只有摘要,仍按现有逻辑按需加载完整历史。
@@ -0,0 +1,17 @@
# 实施进度
## 2026-10-08
- 已确认 80 条限制由 `ChatHistoryPane` 的 `messageRenderBatchSize` 和 `revealEarlierMessages` 驱动。
- 已确认消息窗口化仍以高度占位和最多约 60 条挂载消息为上限。
- 已建立基线、本阶段和 minimap 阶段的对比测试计划。
- 已将生产会话接线改为直接使用当前已加载消息总数,保留 DOM 窗口化。
- `ChatHistoryPane` 与 `ChatTimeline` 定向测试通过:58/58。
- 改动文件定向 web 类型检查和 lint 通过。
- `npm run build:bundle` 通过。
- `npm run perf:app` 通过。本次 B 组测量中,长会话打开交互延迟为 86.9 ms,长会话滚动帧间隔 p95 为 16.9 ms、无长任务;流式输入场景帧间隔 p95 为 16.9 ms,输入延迟 p95 为 17.1 ms。
- B 组第二次运行仍通过:长会话打开交互延迟为 46.9 ms,长会话滚动帧间隔 p95 为 16.8 ms、无长任务;流式输入场景输入延迟 p95 为 19.1 ms。
- App 测试通过 341/349;剩余 8 个失败集中在既有图片附件异步场景,未出现聊天历史窗口相关失败。
- 全局 `npm run typecheck` 仍被既有 `src/main/desktop-storage-electron-fixture.ts:215-217` 类型错误阻断;全局 lint 仍包含生成的 VitePress cache 错误。
- A 组临时恢复后连续两次在 composer 初始化阶段超时,未进入性能场景,因此没有有效 A 组数据,不能据此推导 A/B 数值差异。B 组代码已恢复并重新完成 bundle、定向类型检查、定向 lint 和 58/58 聊天窗口测试。
- 下一步实现 minimap 前仍需先修复性能驱动在 A 组接线下的 composer 初始化问题,或建立不依赖 composer 的长会话专用 benchmark,再进行严格 A/B 测量。
@@ -0,0 +1,52 @@
# 技术设计与测试计划
## 当前边界
`ChatHistoryPane` 当前有两层机制:`messageRenderBatchSize = 80` 控制向上滚动时可访问的历史窗口;`maxRenderedMessageCount = 60` 控制实际挂载的消息组件。第一阶段只移除前者在生产接线中的作用,保留后者以及消息高度占位和锚点恢复。
已加载历史的消息数组仍由会话 store 提供。带 `messageSummary` 的会话继续先显示加载状态,由现有 `loadHistory` 路径取得完整历史。这样本实验测量的是滚动历史窗口的变化,不混入存储或 IPC 改造。
## 实验分组
| 组别 | 历史窗口 | DOM 窗口 | minimap |
| --- | --- | --- | --- |
| A 基线 | 每次增加 80 条 | 保留 | 无 |
| B 本阶段 | 当前已加载消息全部可访问 | 保留 | 无 |
| C 下一阶段 | 当前已加载消息全部可访问 | 保留 | 有 |
A 组保留在组件级回归测试中;B 组是当前生产实验;C 组待 B 组稳定后实现。
## 功能测试计划
- 100 条消息:打开会话后直接滚到第一条,不出现加载按钮。
- 500 条消息:从底部连续滚到顶部,检查消息顺序、首尾消息和滚动位置。
- 2,000 条消息:在中部、顶部和底部来回滚动,确认高度占位和锚点不跳动。
- 消息交互:展开 reasoning/tool details、输入答案、选择文本、打开附件后滚出再滚回,确认状态保留。
- 流式消息:生成过程中滚动、暂停跟随底部,再回到底部,确认自动跟随行为不变。
- 摘要会话:打开只有 `messageSummary` 的会话,确认仍然先加载历史,不把摘要误当作完整消息。
## 性能测试计划
每个规模至少运行 5 次,分别记录冷打开和已经打开后的滚动表现。低端机模拟沿用已有性能测试配置,结果记录中注明运行环境。
记录指标:
- 会话打开到首屏可交互的耗时;
- 首次滚到顶部的耗时;
- 连续滚动期间长任务和掉帧;
- Renderer 内存峰值和测试结束后的回收情况;
- 实际挂载消息数量;
- 流式输出时输入延迟。
通过条件:B 组功能测试全部通过,实际挂载消息数量仍受窗口上限控制;相对 A 组没有明显的输入、流式输出或滚动回归。若 B 组稳定,再以同一批数据实现 C 组并做增量对比。
## 验证命令
```text
npm exec vitest run src/renderer/src/ChatHistoryPane.test.tsx src/renderer/src/ChatTimeline.test.tsx
npm run typecheck
npm run lint
npm run perf:app
```
`perf:app` 的具体参数和输出以仓库脚本当前实现为准;无法在自动化环境观察的内存和掉帧指标,需要补充 Electron 手工记录。
@@ -12,6 +12,8 @@
监督回顾按来源版本及正文位置增量处理,手动回顾与心跳共享进度,无变化时跳过模型;“重新整理”才重读所选区间。旧的进度、失败重试和删除边界见[自动增量与手动重看](./logic-design.md#自动增量与手动重看)。
未发布回顾在会话删除后省略相关来源并继续,具体覆盖和历史保留规则见[会话删除](./logic-design.md#未发布回顾遇到会话删除)。
| 文档 | 权威职责 |
| --- | --- |
| [会话监督 PRD](./prd.md) | 监督目标、证据、介入方式和产品验收 |
@@ -24,6 +26,7 @@
| [回顾分块调度设计](./review-scheduling-design.md) | 第 0 节定义生产分页、批次持久化、续跑、当前设置及剩余边界;其余章节保留改造前基线、目标设计和原型证据 |
| [故事线模型改造设计](./storyline-model-design.md) | 全局 › 项目 › 功能 › 子线索的故事层级、故事归属与经验抽取阶段、用户调整及实施顺序(目标设计) |
| [实施进度](./progress.md) | 已验证实现、验证证据和当前边界 |
| [Renderer 修复与测量](./frontend-progress.md) | K01、K03、K04 的界面回归、源码 Electron 对照及测量限制 |
| [故事线图谱设计](../story-graph/prd.md) | 图谱对象、环绕时间轴与实体交互 |
生产入口已接通完整来源分页、叶子持久化、持续处理至完成、活动页主动暂停/继续及批次详情;设置可调页大小、批次正文/消息上限、单次响应容量、两个模型超时和共享并发。实际合同及限制见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。真实 DeepSeek 验证已覆盖一个完整的 27 条消息会话;较早的 129 条消息实验仍未完成。两次实验共 5 次付费请求,详情见进度。原始 AbortError 原因尚未由日志确定。
@@ -0,0 +1,99 @@
# Renderer 修复与测量
最终集成补注:本轮暂存内容在排除并行功能的隔离工作树通过完整验收,6,212 项通过、0 失败、87 跳过;类型检查与 lint 通过。下文保留分组与早期失败记录,最终提交验收以[统一记录](../../review/kiss-local-fixes-2026-10-05.md#最终检查)为准。
## 2026-10-05:K01、K03、K04
对应[局部修复清单](../../review/kiss-local-fixes-2026-10-05.md)及[性能原则](../../architecture/performance-principles.md)的 P5、P6、P7、P8、P10。此处只记录 Renderer;Main 查询、Worker、发布事务和数据库验证由[实施进度](./progress.md)记录。
`SupervisorWorkspace` 首次工作回顾不再请求图谱。进入图谱页时按所选结果读取,内存中只保留当前结果的一份图谱;普通页签切换复用,刷新、历史导航和修改后的刷新清除缓存。图谱错误不阻止阅读回顾,失效请求不能重新填入旧缓存。来源正文继续在点击后读取,未改变 IPC DTO。
工作回顾与图谱使用同一份故事读取状态,按范围和刷新版本复用。摘要在 workspace 中按故事快照和时间区间计算一次,子组件直接渲染该结果;父子关系索引替代逐故事全量扫描。保留直接子线索聚合、孤立子线索、跨项目关联、时间边界及排序语义。App 只移除了监督者刷新中的成果列表读取,未拆分全局状态或新增领域 store。
事件、实体、关系列表使用 `useListWindow`,保留选中和焦点行,按实际行高计算占位。新增组件处理跨窗口的方向键、Home/End、Tab/Shift+Tab 和选择,完整记录仍可滚动访问。原生 Electron 回归发现,窗口切换把已渲染行换成估计高度的占位后,末条焦点行会移出视口;共享 hook 已补可见焦点保护,并验证主动滚离焦点行时不会被拉回。该行为也有独立共享 hook 回归。
## 回归证据
修改前运行新用例,三项均按预期失败:概览调用了 `graph`;同一故事快照计算摘要两次;600 个事件全部挂载。修改后,监督者、心跳中心、摘要及共享列表相关回归通过。历史结果、迟到图谱和来源响应、图谱失败与重试、实体修改后重新读取、来源正文按需读取均有覆盖。App 集成回归验证首次和缓存路由返回不再重读成果列表。
最后复查补充了历史结果已删除的回归:显式结果 ID 查询为空时,工作回顾显示可重试错误,重试仍使用原 ID,不退回最新结果,也不请求图谱。该失败分支在修复前已运行并失败,修复后单独验证;不改变下述性能 fixture 的成功结果路径。
`tests/supervisor-layout.electron.test.ts` 在 Vite 中运行生产组件,新增 `?windowed=1&recap=1` 固定数据,包含各 600 条事件、实体和关系。原生键盘遍历跨越未挂载行,检查首尾可达、焦点可见、选中状态、滚动后视口有内容;随后执行来源读取、回顾与图谱切换、历史切换、实体编辑和 390px 窗口检查。常规 1440、1024、390px、浅深主题及密集图谱布局场景继续通过。
测试使用真实 Chromium 布局、鼠标及键盘输入和生产 React 组件。bridge 返回固定合成数据,编辑写入测试内存;这不是生产 Main、SQLite、模型调用的端到端证明。完整 App 路由由 jsdom 集成用例覆盖。未运行模型请求、生产构建、打包或已打包应用探针。
## 源码性能对照
Windows x64,Intel Core Ultra X7 358H,Electron 44.5.1,不降速。基线取独立临时 worktree 的 `e50ca6d1`,复用现有 Supervisor Electron/Vite fixture;Vite 从 worktree 读取基线 Renderer/shared 源码。两版本使用相同合成数据和原生鼠标输入,最终顺序为基线 1、修复 1、基线 2、修复 2、基线 3、修复 3。每次启动独立 Vite 缓存和 Electron 用户目录。
下表每格按三轮列值,单位为 ms。点击反馈为点击事件至两次 `requestAnimationFrame` 的代理指标;就绪帧指帧回调中观察到 `aria-busy=false`,不等于完整内容提交或合成器呈现时刻。长任务来自 Renderer `PerformanceObserver`,未与 Main 查询耗时相加。
| 场景与指标 | 基线三轮 | 修复三轮 |
| --- | --- | --- |
| 已预载路由,2,000 事件,图谱点击反馈 | 346.3 / 325.0 / 188.3 | 29.9 / 38.5 / 24.3 |
| 同场景,图谱长任务时长 | 338+203 / 317+169 / 182+131 | 0 / 0 / 0 |
| 同场景,挂载行数 / 文档元素数 | 2,000 / 8,207 | 24 / 329 |
| 2,000 故事各十事件,首次回顾摘要 CPU | 80.0 / 73.2 / 48.2 | 15.4 / 16.2 / 9.3 |
| 同场景,首次回顾长任务时长 | 83 / 75 / 50 | 0 / 0 / 0 |
| 同场景,图谱返回回顾的摘要 CPU | 140.8 / 117.5 / 88.7 | 0 / 0 / 0 |
| 同场景,缓存路由返回的摘要 CPU | 67.4 / 74.1 / 42.6 | 0 / 0 / 0 |
| 同场景,缓存路由返回点击反馈 | 73.4 / 81.1 / 47.5 | 5.2 / 8.7 / 6.7 |
| 注入 250ms 图谱延迟,首次回顾就绪帧 | 282.7 / 265.1 / 263.8 | 16.2 / 10.9 / 10.6 |
| 未预载模块,八事件,首次回顾出现帧 | 340.9 / 341.9 / 308.1 | 312.5 / 343.8 / 331.1 |
修复版本首次回顾的图谱请求数为零;图谱首次打开才承担模拟延迟。普通图谱/回顾切换不再请求故事,缓存路由返回不再计算摘要。摘要首次的计数包含一次空数据计算和一次已加载快照计算,后续同一快照复用。
未预载模块的首次回顾仍约需 0.31~0.34 秒,包括 Vite 模块加载及 React Suspense 行为,未证明这一项有改善。在该场景随后首次打开八事件图谱时,修复版本第二轮仍出现一次 55ms 长任务;其余修复场景未观察到长任务。三轮样本不足以声明 p95 达标,源码模式也不能代替发布包测量。三个场景均未加载 Three.js,此处没有测量螺旋的 WebGL 初始化。
原始记录及临时测量脚本保留在本机批准的临时目录 `opencode`:`supervisor-comparison.jsonl`、`supervisor-audit-runner.mjs`、`supervisor-audit-driver.mjs`。最终数据标签为 `baseline-final` 和 `fixed-final`;文件还保留较早的对照轮次,不能混算。首次审查和各轮间波动明显,因此记录逐轮数值,不把模块加载、查询等待和渲染 CPU 合并归因为存储。
## 验证状态
- 最终定向 Renderer 与共享列表测试:8 个文件、139 个用例通过。此前包含相关 Main 文件的定向重跑为 368 通过、1 跳过。
- App 首次入口、缓存路由返回及侧栏图谱导航:3 通过。
- 完整 Supervisor 源码 Electron 布局与交互:通过;`npm run lint` 通过。
- 完整 `npm test` 执行结束:515 个测试文件通过、6 个失败、15 个跳过;6,208 个用例通过、7 个失败、87 个跳过,另有一个未处理拒绝。随后修正并重跑了本任务的焦点问题;相关 Main 失败在其负责方更新后重跑通过,Magic Notes 原生布局独立重跑通过。
- 当时 `tests/message-expansion.electron.test.ts` 独立重跑仍触发 90 秒超时;后续定位、修复及定向绿色验证见下节。全量测试尚未整体重跑,不能将定向验证记为全量通过。
- 最终 `npm run typecheck`、`npm run lint`、`git diff --check` 通过。文案扫描无阻断项;复核候选为加载、错误和测量范围的必要限制说明,已保留。
## 2026-10-05:消息展开测试复查
该 Electron 测试在 2,000 和 10,000 条聊天记录场景之后,还执行监督者来源请求的语言切换失效场景。聊天历史使用独立的消息窗口机制,不调用本轮修改的 `useListWindow`;监督者 fixture 只有一个事件,也未进入长列表窗口路径。相同断言下,`e50ca6d1` 和当前源码的普通复跑分别在 16.4 秒和 15.8 秒通过,未复现确定性的 Renderer 性能回退。
加入有界的 100ms 替换图谱响应后,两版本均重现测试同步错误:语言已切换,旧来源按钮仍短暂存在,测试便继续执行;固定两帧后读取按钮的 `disabled` 时,刷新已卸载该节点,抛出空引用错误。测试现在通过 fixture 的来源标题区分第几次图谱响应,等待第二次响应实际渲染后再完成旧来源请求并检查失效结果。初始语言固定为中文,保证切换到英文确实触发刷新。
轮询辅助函数另有一个已验证的超时问题:第二次及后续动画帧中的谓词异常没有进入 Promise 的拒绝路径,Promise 会永久等待,直到外层 90 秒看门狗杀死 Electron。批准临时目录中的 `expansion-wait-probe.mjs` 使用两版本的原始辅助函数和同一“节点消失”输入验证:旧版为 `pending` 加未捕获错误,修复版为 `rejected` 且无未捕获错误。已将谓词异常接入拒绝路径,并保留原有 10 秒等待上限;外层 90 秒上限、DOM 数量、展开状态、键盘和旧来源断言均未放宽。
原先的 90 秒日志没有阶段信息,无法证明它一定由上述某一个等待点触发。此次修复针对已重现的同步错误与未结束的等待 Promise,不将先前超时归因于共享列表或存储。新增阶段耗时及 Renderer 错误输出便于后续定位。没有修改产品 Renderer 或后端代码来绕过测试。
严格 fixture 下,基线完整测试在 15.6 秒通过。当前源码最终合并运行消息展开、监督者布局和共享列表等 10 个测试文件,141 个用例通过,总耗时 69.7 秒;其中消息展开各阶段合计 14.7 秒。两种聊天规模均访问 1,397 条不同记录,最多挂载 15 行、926 个文档元素;保留的展开项从 38 条在删除后归零,原生 Space 收起、输入草稿、监督者迟到来源失效均通过。Supervisor 继续覆盖原生首尾跳转、跨窗口 Tab、焦点可见、主动滚离焦点、历史、来源、编辑及窄窗口。最终类型检查和 lint 通过。
## 2026-10-05:独立复核修正
独立复核发现本轮改动引入了两个问题,均在修复前运行回归确认。
故事加载的旧 `act()` 会保留发起时的 `load()` 回调。A 范围的重命名尚未结束时切到 B,若 A 修改完成后再调用旧回调,它会先递增共享 generation,导致 B 的在途读取也被丢弃。相同范围换刷新版本时,旧回调还会覆盖新结果。新测试分别重现了 B 保持空数据和刷新后显示旧快照;另一个测试确认旧刷新回调会多发一次读取。
`SupervisionStories` 现在在提交时记录当前范围与刷新版本,在递增 generation 和发出读取之前核对回调归属;响应也须属于同一归属对象和请求序号。卸载后清除归属。四个 hook 回归覆盖跨范围修改完成、同范围刷新版本变化、保留回调在刷新后及卸载后的调用,以及当前修改正常重读并在页签返回时复用快照。修复前四项中三项失败,修复后全部通过;没有改变修改请求本身或增加全局状态管理。
列表原先只保留视口附近、选中和焦点行。滚到中间后,从面板按 Tab 会进入第一条已挂载记录;从后续控件按 Shift+Tab 会进入最后一条已挂载记录。原生 Electron 回归实际得到 `event-285` 和 `event-314`,且随后不能按完整列表的边界顺序退出。
`SupervisorSelectionList` 通过已有 `keepIds` 额外保留逻辑首、末行,继续使用原生 Tab 进入和退出,不增加焦点哨兵或拦截外部控件的键盘事件。新增 Electron 场景先选择中间记录、将列表滚到中部,再从真实前后控件用原生 Tab/Shift+Tab 进入。事件、实体、关系三类均检查首末记录、焦点可见、反向退出至前一控件和正向退出至后一控件;随后继续原有方向键、Home/End、跨窗口 Tab、滚动保留、历史、来源和编辑检查。测试准备阶段在同一次浏览器求值中检查并选择中间行,等待选中提交,避免把尺寸重测期间的临时 DOM 当作稳定状态。
最终定向运行包括消息展开、完整 Supervisor 布局、故事加载、workspace、心跳中心、摘要和共享列表:11 个文件、145 个用例通过,总耗时 69.3 秒。App 中相关监督者、心跳和图谱导航另有 11 个用例通过;类型检查和 lint 通过。消息展开仍覆盖 2,000/10,000 条记录,最多挂载 15 行、926 个元素,三个展开状态恢复、原生 Space、草稿保留、删除清理及迟到来源失效均通过。
本次没有运行完整仓库测试、生产构建或模型调用,也没有提交。上文性能表保留其测量时的代码状态;首末行保留会额外挂载至多两行,本次未重跑性能对照,不将旧节点数称为此次修正后的测量结果。原生列表回归中的有界 DOM 断言继续通过。
## 修改归属
本次 Renderer 任务的产品文件为 `App.tsx`(仅删除成果刷新)、`SupervisorWorkspace.tsx`、`SupervisorSelectionList.tsx`、`SupervisionStories.tsx`、`SupervisionStoryDigest.tsx`、`supervision-story-digest.ts`、`supervisor-workspace.css`、`use-list-window.ts`,以及对应的 `App.test.tsx`、`HeartbeatCenter.test.tsx`、`SupervisorWorkspace.test.tsx`、`SupervisionStories.test.tsx`、`supervision-story-digest.test.ts`、`use-list-window.test.tsx`。当前 `SupervisorWorkspace.tsx` 相对 `e50ca6d1` 的修改均来自本任务,包括最后的历史结果缺失检查。
本任务的 Electron 文件为 `tests/supervisor-layout.electron.test.ts`、`tests/message-expansion.electron.test.ts`,以及 `tests/support/` 下的 `supervisor-layout-driver.mjs`、`supervisor-layout-regression.tsx`、`supervisor-selection-driver.mjs`、`message-expansion-driver.mjs`、`message-expansion-regression.tsx`。
| 文档 | 本任务修改 | 并行修改 |
| --- | --- | --- |
| `frontend-progress.md` | 全文 | 无已识别的其他修改 |
| `README.md` | 文档表中的“Renderer 修复与测量”一行 | 未发布回顾遇到会话删除的说明段落 |
| `ui-design.md` | 按需图谱读取、历史和刷新等待规则、列表窗口与键盘规则 | 删除会话后省略来源的活动说明段落 |
其余 Main、Agent、shared DTO、`SupervisorActivity`、`SupervisionReviewStages`、中英文 heartbeat 文案、监督者技术/主进度/需求/逻辑文档、公共评审与路线图、Telegram 和宣传稿修改均不属于本 Renderer 任务。此归属用于区分本任务的改动,未替其他负责方审定或提交其代码。
@@ -10,11 +10,21 @@
回顾获得执行位置后,读取一次连接配置;同次执行中的批次、输出重试、汇总、故事与经验阶段保持该模型。修改监督者选择、应用默认模型或连接配置,仅影响后续执行。继续暂停或失败的回顾、单独重试故事、生成或重试建议时重新读取当前选择;跨重启不保存模型或凭据快照。显式选择的连接被删除、改为图片协议或缺少所需 API Key 时,执行报错并提示修改设置,不自动换成默认模型。
对应 FR-S6。监督模型遇到可识别的临时失败时,统一等待 500ms 后重试一次:HTTP 429、500/502/503/504,以及短暂网络故障或响应流中断。取消、模型超时、认证、配置、额度或容量错误直接停止;无法确认可恢复的错误也不重试。失败尝试的部分正文丢弃,已报告用量和已保存批次保留。叶子输出校验后的缩块重试、导航输出重试保持原规则;请求生命周期见[模型阶段超时与调度](./technical-design.md#模型阶段超时与调度)。
## 整体逻辑架构
报告、监督叶子与导航整理共用监督专用 FIFO 并发池,普通聊天不入池;监督服务外层 run 仍串行。报告在排队前冻结超时,获槽后领取,实际模型请求前校验并刷新租约;报告提交或无变化后先释放槽位,再执行监督。监督超时在 run 创建时保存,节点获槽并解析 Runtime 后计时。阶段超时、租约、传输时限、后台检查与活动轮询各自独立,取值及生效规则见[当前设置合同](./review-scheduling-design.md#当前设置合同)。
完整回顾的生产接线已增加分页来源清单、逐批输出/offset 事务和导航节点。用户主动暂停后显示 `paused`,模型或保存失败显示 `failed`;两者均保留此前成功批次,可继续同一 run。回顾不按累计运行时长自动暂停。存在未读来源、覆盖空洞或未成功导航时不能标 `completed`。手动位置与自动 checkpoint 分开,自动位置只在最终发布事务推进。来源变更目前要求重新回顾,模型配置跨重启固定等差异见[生产接线与剩余边界](./review-scheduling-design.md#0-生产接线与剩余边界)。真实模型验证覆盖 27 条消息的完整会话,较大样本失败单独记录。
完整回顾的生产接线已增加分页来源清单、逐批输出/offset 事务和导航节点。用户主动暂停后显示 `paused`,模型或保存失败显示 `failed`;两者均保留此前成功批次,可继续同一 run。回顾不按累计运行时长自动暂停。存在未读来源、覆盖空洞或未成功导航时不能标 `completed`。手动位置与自动 checkpoint 分开,自动位置只在最终发布事务推进。现存来源修改仍要求重新回顾;会话删除按下节规则省略。其他差异见[生产接线与剩余边界](./review-scheduling-design.md#0-生产接线与剩余边界)。真实模型验证覆盖 27 条消息的完整会话,较大样本失败单独记录。
### 未发布回顾遇到会话删除
对应 FR-S9、US-S35。会话物理删除后,尚未发布的回顾省略其冻结来源,继续处理其他会话;省略包含消息、消息携带的知识引用及已记录会话归属的任务。归档不等同于删除,现存来源修改、单独消息移除或其他不可用原因仍明确失败。初始化前已删除的会话不进入清单。
含删除来源的叶子整批丢弃,其父级导航失效。混合批次中的存活来源从本次初始 offset 重做,包含这些来源的其他批次也失效,避免覆盖空洞或重复事实;与之无关的批次及导航保留。不从保存片段恢复或重读已删除会话。
活动及运行详情显示省略来源数;完成仅表示保留来源已处理并发布,不代表原冻结集合全部覆盖。所有来源均被省略时记录 `no_change`,不生成空报告、不推进这些来源的 checkpoint。删除前已发布的结果、事实和历史片段保留规则不变。已发出的模型请求可能仍返回,但其受影响输出不得发布。
```mermaid
flowchart TB
@@ -379,7 +379,7 @@ type SupervisorRecord = {
5. 普通会话监督失败不影响主回答;自动化配置的强制监督失败进入明确关注状态。
6. 评论和通知不包含完整私人消息,证据点击后才在原对象中查看。
7. Renderer 传入的 Evidence ID 必须由 Main 重新验证归属。
8. 删除会话时按产品数据保留策略删除或匿名化监督记录。
8. 删除会话时省略未发布回顾中的该会话来源,继续其他来源,并显示省略范围;已发布历史结果保留。具体规则见[未发布回顾遇到会话删除](./logic-design.md#未发布回顾遇到会话删除)。
9. 用户关闭监督后停止新分析,但保留历史,除非用户明确删除。
## 19. 性能与预算
@@ -4,6 +4,87 @@
当前记录以已验证生产行为为准。监督者尚未覆盖全部 user stories。
## 2026-10-05 监督页面读取与发布局部优化
本节记录 K01 的后端来源投影及 K02、K05、K06,基线为 `e50ca6d1`。生产 IPC 的概览、图谱、故事/经验读取已接入现有只读 Worker,文件数据库失败时不回退 Main 查询。schema 62 只增加概览查询索引;图谱只传来源标题,完整正文仍可按来源 ID 读取。发布只构造一次最终图谱快照,原 Main 事务和原子提交规则保留。接口及顺序规则见[技术设计](./technical-design.md#页面读取与发布2026-10-05)。本节不承担 Renderer 懒加载、摘要和虚拟列表的验证。
先加回归后改实现:最初 6 个用例在旧代码全部失败,分别暴露来源正文随图谱返回、无关项目仍读取全部经验、缺少 UI Worker 路径及重复重写快照等问题。新增文件最终 8 项通过,包含另一连接提交期间的读快照一致性、跨叶子复用身份时首叶子快照与来源引用保留。原子性用例在 checkpoint 写入处注入失败,确认结果、来源、运行状态和 checkpoint 一起回滚,叶子批次可重试。IPC 回归使用真实文件数据库和在临时目录编译的 Worker 源码,断言三个生产 handler 不调用 Main 同步读取;Worker 终止后请求拒绝,来源详情仍返回完整正文。
源码测量复用 `build/run-main-perf.cjs` 的 esbuild/Electron-as-Node 方法。基线从独立临时 worktree 读取,修复版从工作树读取,各运行三轮,第二轮反转版本及数据量顺序。Windows x64,Core Ultra X7 358H,Electron 44.5.1/Node 24.21.0,无 CPU 降速;同机还有并行开发活动,保留全部波动。每个版本每轮使用独立合成 SQLite,100 个会话、20 份结果,最新结果持有全部图谱事实;小/中/大样本分别为 2,000/20,000/40,000 条消息和 200/2,000/8,000 个事件及来源,消息与来源正文各 2,048 个 ASCII 字节。最大库约 212 MB。这是压力分布,不代表用户库。
每个读取入口测首次调用和三个立即重复调用,期间用 `setImmediate` 记录调用方事件循环最大间隔。首次概览包含懒启动 Worker;它不是冷 OS 缓存测量。下表为大样本每轮四次调用中的最大 Main 间隔,单位 ms:
| 入口 | 基线三轮 | 修复后三轮 |
| --- | --- | --- |
| 概览 | 389.03/471.16/872.84 | 2.45/2.10/3.17 |
| 结果图谱 | 583.03/571.86/646.16 | 12.04/12.29/15.79 |
| 故事及经验 | 82.92/77.06/72.28 | 8.56/6.94/19.74 |
修复后大样本概览的首次完成耗时为 94.01/89.65/117.37 ms,暖调用中位数为 0.74/0.74/1.05 ms。图谱首次完成仍为 453.89/476.75/518.11 ms,暖调用中位数为 573.05/551.27/686.15 ms,小时密度仍需读取消息正文。故事及经验首次完成为 90.99/84.18/95.01 ms;SQL 数量从 1,204 降至 6,但全局返回的 17,201 条 SQL 行和完整成员响应保留,不能据此声称整个故事请求变快。
大样本图谱 JSON 从 25,742,067 降至 7,143,667 字节;概览保持 7,901 字节,故事响应保持 3,317,180 字节。数值是 UTF-8 JSON 大小,不是 Electron IPC 实际传输字节。以上计时覆盖生产仓储方法、真实 Worker 及线程传输;不含 Renderer 解析/渲染、窗口 IPC 序列化、点击到下一帧,也不把几个探针耗时相加为端到端结果。
发布另用独立合成库,每批 20 个真实消息来源、20 个事件和 20 个实体,实际执行清单初始化、叶子保存和 `saveSupervisionResult`。本次复测保留并行加入的删除会话校验,测试来源确实存在。测量期间关闭自动 WAL checkpoint,以分离事务本身,未改变产品配置或落盘同步级别。三轮交替结果如下,单位 ms:
| 批数/来源数 | 基线同步耗时 | 修复后同步耗时 |
| --- | --- | --- |
| 10/200 | 27.86/28.77/30.96 | 28.60/18.33/24.88 |
| 50/1,000 | 194.20/228.55/223.33 | 146.28/151.44/127.86 |
| 100/2,000 | 686.15/729.16/729.95 | 271.02/286.14/303.41 |
100 批的快照 UPDATE 从 101 次降至 1 次,快照 SQL 从 444.31/473.67/463.00 降至 14.85/15.49/14.78 ms;修复后 COMMIT 为 27.40/30.89/33.01 ms,整个事务仍为 271.00/286.11/303.39 ms,事件循环最大间隔为 271.06/313.32/303.73 ms。小批次波动大于收益,不声明稳定提速。所有发布样本的 checkpoint 数都与来源数相等。合并后的叶子保存有一次 21.93 ms 峰值,也超过 16 ms,未修改其校验或放宽断言。
迁移验证直接复制基线 worktree 生成并已关闭的 schema-61 合成库,再运行生产 `upgradeAssistantStorage`。55 张用户表、32,947 行及 rowid 的逐行哈希在升级前后相同;20 份概览、2,000 条来源标题及完整正文、100 个故事、200 条经验的结果与排序等价,另比对项目及无关项目读取。升级耗时 61.73 ms,文件由 95,526,912 增至 95,711,232 字节;只报告 `upgrading`,没有内容转换或压缩,重复升级不再执行。`integrity_check` 为 `ok`、外键检查为空,基线文件 SHA-256 未变。单元测试另覆盖旧库有空闲页时也不回收。
最终定向命令包含 `supervision-ui-reads`、`supervision-history`、`supervision-timeline`、`supervision-stories`、`supervision-experiences`、`supervision-review`、`supervision-worker`、`readonly-query-reader`、`assistant-storage-upgrade`、`assistant-database` 和 `ipc` 的测试文件:11 文件、422 项通过、1 项原有跳过。`tests/supervisor-layout.electron.test.ts` 与 `tests/magic-notes-layout.electron.test.ts` 独立复跑,2 项通过。`npm test` 全量曾有 6 项失败、6,209 项通过、87 项跳过:两个新发布 fixture 未保留真实会话、旧 IPC fixture 只模拟一次 429 却要求整个请求失败,以及并行删除和两项 Electron 布局回归。发布 fixture 已改用真实消息;IPC fixture 覆盖重试耗尽后再次执行恢复;上述失败文件的最终复跑均通过。未将这次复跑记成全量重跑通过。
最终 `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。类型检查发现新 fixture 的阶段字面量不在合同中,已改为既有 `saving`,该文件 8 项再次通过。两份后端文档的中文审校扫描阻断项为 0;人工复核保留数据范围、性能预算和未测项说明。
原始脚本、18 组读取样本及迁移报告保留在系统临时 `opencode/supervisor-kiss-comparison-DzVb85`,读取汇总为 `entry-summary.json`,迁移报告为 `migration-integrity.json`。并行删除校验合并后的 18 组发布样本位于其 `publication-current/publication-measurements.json`;此前同目录外层的发布报告只代表合并前源码。
剩余预算问题:故事读取有一次 19.74 ms 的 Main 间隔,超过 16 ms;大图谱仍要等待数百毫秒的 Worker 查询;首次 Worker 启动也会增加小库概览等待。Main 最终发布仍有约 271~303 ms 的同步阻塞,并未达到性能原则。业务写入迁移、独立存储服务、IPC 实际序列化与并发写锁等待不在本次已验证范围,后续方向见[桌面存储职责](../../architecture/desktop-storage-direction.md)。本轮不迁移全部写入,也不放宽预算。外部模型调用为 0;没有读取用户正文、修改用户库、读取环境文件、生产构建、打包或启动已打包应用,没有提交。
## 2026-10-05 删除会话后继续未发布回顾
对应 FR-S9、US-S35。删除会话现在使受影响的未发布批次及依赖导航失效,其他来源继续;混合批次重算、省略覆盖和历史保留规则见[逻辑设计](./logic-design.md#未发布回顾遇到会话删除)。活动和运行详情已增加中英文省略数量提示。原先因已删除会话而标记需要重建的运行,可重新继续;现存来源的修改或归档仍失败。
验证使用隔离文件 SQLite、真实初始化/恢复 Worker 和可控模型返回,未接触用户数据库:
- `npx vitest run src/main/assistant/supervision-review.test.ts src/main/assistant/supervision-worker.test.ts src/main/assistant/supervision-production.test.ts src/renderer/src/SupervisorActivity.test.tsx`:4 文件、95 项通过。新增场景包括恢复前删除整会话及知识引用/任务、Worker 复制清单时删除、两路处理及导航期间删除、发布前另一连接删除、混合批次存活来源重做、父导航递归失效、历史结果及来源逐字段不变、全部来源删除无空报告和中英文省略提示。
- Node/Web TypeScript 检查、11 个改动 TS/TSX 文件的定向 ESLint、`git diff --check` 通过。差异检查仅有仓库既有 LF/CRLF 提示。
- `npx vitest run src/main/assistant/assistant-database.test.ts -t explicitly.deletes.only.local`:既有本地会话删除与消息级联回归通过,其余 124 项按筛选跳过。
外部模型调用为 0;未运行全量测试、Electron 界面实测或大库删除延迟测量,未提交或推送。回顾仍在桌面直连模型路径执行,不改变远程 Agent。已发出的请求可在删除后返回,结果会被丢弃;不承诺撤回已发送给模型的输入。旧清单若未保存任务会话归属且任务早已被外部删除,不能推断其归属,仍按来源不可用失败。
## 2026-10-05 监督模型临时失败重试
对应 FR-S6。`runSupervisionModel` 已接入一次可取消的临时失败重试,覆盖监督者各模型阶段;错误分类、清理及每次尝试的超时规则见[技术设计](./technical-design.md#模型阶段超时与调度)。直连文本请求保留 HTTP status,并交出失败流中已报告的用量。OpenCode/Continue、直连传输重试和叶子/导航输出重试未改。监督者仍在桌面 Main 直连模型,不经过远程 Agent 或模型桥。
验证结果:
- `npx vitest run src/main/assistant/supervision-model-retry.test.ts src/main/assistant/supervision-production.test.ts src/main/assistant/supervision-review.test.ts src/main/assistant/supervision-model-pool.test.ts src/main/agent/model-runtime.test.ts`:5 个文件,177 项通过,1 项原有跳过。覆盖 429、可恢复 5xx、部分正文后 terminated、连续失败最多两次包装层调用、认证/配置/容量/超时不重试、等待中取消、流和会话清理、用量保留,以及已保存叶子在失败和继续后不重算。
- `npx tsc --noEmit -p tsconfig.node.json`、`npx tsc --noEmit -p tsconfig.agent.json`、4 个改动 TypeScript 文件的定向 ESLint 和 `git diff --check` 通过。
请求验证使用生产 `ModelAgentRuntime` 和注入的假 fetch/响应流,数据保存使用内存 SQLite;外部模型调用为 0。未运行全量测试、真实模型或远程 Host 验证,未提交或推送。
## 2026-10-05 后端阶段审查 A03/A04/A05
对应 SL-5、SL-8、FR-S6、FR-S10。生产候选读取现在将批次项目和跨项目设置传到只读 Worker;最终候选读取返回后检查暂停/取消;故事归属和拆分在原写事务内拒绝已移除或合并的目标。存储、停止与重试规则见[技术设计](./technical-design.md#单次执行与取消)。没有 schema 变更,原始消息、历史结果及人工确认字段保留。
先加回归、后改实现的结果:8 个新用例中旧代码失败 7 个,取消用例原本通过。失败覆盖跨项目开/关两种生产候选、最终候选读取期间暂停、归属期间移除/合并目标、拆分前移除功能/子线索。旧暂停用例实际返回 `completed` 并发布结果;旧归属用例写入隐藏目标且把事件标为已处理。修复后暂停不发布结果或 checkpoint,继续仍复用原批次;失效故事批次回滚且事件可重试。生产身份测试补充真实项目事件关联,保留 UUID 别名、确认字段及失败后继续的断言。
验证命令与结果:
- `npm test -- src/main/assistant/supervision-production.test.ts src/main/assistant/supervision-worker.test.ts src/main/assistant/supervision-stories.test.ts src/main/assistant/supervision-review.test.ts src/main/assistant/supervision-timeline.test.ts src/main/assistant/supervision-experiences.test.ts src/main/assistant/supervisor-service.test.ts src/main/readonly-query-reader.test.ts`:8 个文件、96 项通过。Worker 回归每轮使用两个项目、422 条来源,并检查候选隔离、实体数量、外键及暂停后的完整发布。
- 本次修改的 Main 源码、测试和验证脚本定向 ESLint、`git diff --check` 通过。`npm run typecheck` 首轮发现本轮 fixture 缺默认设置字段,修正后曾被并行 `tests/message-expansion.electron.test.ts:39` 类型错误阻塞;该文件由其负责方修正后,Main/Agent/Web 三组最终全部通过。本轮未修改该文件。
- 定向验收时 `npm run lint` 曾被并行 OpenCode 修改中的三项错误阻塞;负责方修正后,最终全仓 lint 与 typecheck 均通过。两份后端文档扫描阻断项为 0;人工复核保留取消与事务顺序的边界说明。完整集成结果见[阶段修复验收](../../review/stage-audit-fixes-2026-10-05.md#最终检查)。
- `node --import jiti/register scripts/supervision-audit-validation.ts scale <临时父目录>`:真实文件 SQLite,10 个项目、1,000 个实体、10,000 个事件、10,000 条事件实体关联、10 条来源,库文件 10,964,992 字节。三轮交替查询中,每轮十次旧相关子查询合计 3,517/5,302/4,190 ms;新 Worker 查询合计 39.18/34.01/36.04 ms,包含传输。逐项目返回的 100 个候选 ID 及顺序与旧 SQL 完全一致;跨项目读取每轮 0.95~1.79 ms。120 个事件的单批归属事务为 6.63 ms。计数不变,`foreign_key_check` 为空,`integrity_check` 为 `ok`。
- 查询测量是同一合成库内的 SQL 对照,非完整 App 基线;同时有其他本地验证任务运行。原相关查询在修复传参后首次实测暴露每十次 3.60~4.65 秒耗时,随后改为实体集合查询。没有新增索引或迁移,也不据此宣称整窗性能改善。
真实模型检查沿用 `scripts/supervision-production-live.ts` 的环境文件读取与 `ModelAgentRuntime` 无工具模式,由新增合成验证脚本执行。`node --import jiti/register scripts/supervision-audit-validation.ts live <临时父目录> .env.deepseek.local` 只发送一条 147 字符合成消息,真实生产工厂和文件 Worker 完成提取、保存及故事归属。外部请求准确为 **2 次**:提取 1 次、故事归属 1 次;无变化重跑新增请求 0 次。原文片段精确重建,外键与完整性检查通过,总耗时 3.84 秒。脚本上限为 3 次 HTTP 请求、每次模型阶段 45 秒、1,800 输出 token;凭据仅在进程内读取,终端与指标文件只输出计数和验证信息。本机继承的 TLS 环境设置未由本次修改。
报告位于系统临时 `opencode/supervision-audit-Ggn5uW/metrics.json`(规模检查)和 `opencode/supervision-audit-WNu08s/metrics.json`(模型检查)。只写隔离合成库,未读取或修改用户库,未运行完整 App/Electron 回归或整包构建;没有提交。修改位于桌面监督服务及其 SQLite Worker,GoodBuddy Agent 与远程 Runtime 控制路径未改变。
## 2026-10-04 螺旋工具栏与事件颜色修正
接续未提交的回顾布局调整。平铺与螺旋共用标题工具栏样式,项目层级菜单放在面包屑旁,画布内的视角入口和左下角圈数说明已移除。默认斜俯视,拖动及键盘操作保留;两种图谱均以共享 InlineHelp 的 i 图标打开图例。共用历史回顾栏、下钻返回、详情选择与中窄窗口详情面板保留。当前规则见 [UI 设计](./ui-design.md#故事线图谱)。
@@ -138,7 +219,7 @@ FR-S13 已接通监督者“设置 → 模型”、应用设置持久化和 Main
- 叶子批次输出不可用(无效 JSON、结构不符、引用不存在的来源或实体)时,在同一段待处理正文上缩小到 1/2、再到 1/4(不少于 500 字符)重试,最多 2 次;缩小只是先处理前一部分,其余正文留在待处理里,不跳过、不截断。
- 导航合并输出不可用时,用相同输入再问 1 次。
- 提供方错误、超时、取消和响应超出容量不重试,仍按原来的方式失败并保留已保存批次。
- 当时提供方错误、超时、取消和响应超出容量不重试,失败保留已保存批次。2026-10-05 起临时提供方错误采用上文的一次重试;超时、取消和响应超出容量仍不重试。
改后结果(DeepSeek `deepseek-flash`):
@@ -721,7 +802,7 @@ npx eslint scripts/review-algorithm.mjs scripts/review-algorithm.test.mjs script
### 2026-09-24 模型阶段超时、并发与设计边界
- 本次静态核对当前新代码:报告 `heartbeatReportTimeoutSeconds`、监督 `supervisorOrganizeTimeoutSeconds` 已接入共享校验、设置存储、App/设置表单及 Main 摘要器;整数 30..600 秒、默认 240。报告在等待池前冻结值,监督在摘要器入池前读取值,均在获槽及 Runtime 解析后才计时。完整控制及未覆盖环节见[控制清单](./review-scheduling-design.md#当前超时与调度控制清单已实现)。
- 本次静态核对当前新代码:报告 `heartbeatReportTimeoutSeconds`、监督 `supervisorOrganizeTimeoutSeconds` 已接入共享校验、设置存储、App/设置表单及 Main 摘要器;整数 30..600 秒、默认 240。报告在等待池前冻结值,监督在摘要器入池前读取值,均在获槽及 Runtime 解析后才计时。该次记录对应的完整控制及未覆盖环节见[初稿控制清单](./review-scheduling-design.md#初稿时超时与调度控制清单历史截面),不代表当前实现。
- `supervisorModelConcurrency` 已接入同一设置链路,整数 1..4、默认 1;报告与监督共用 FIFO 池,普通聊天排除,降限不终止在途项。报告获槽后才领取,每次到期领取最多一条;租约为 `max(300, 报告超时 + 60)` 秒,实际请求开始前按运行 ID、owner、attempt、claimed 和未过期条件校验并刷新。报告提交或无变化后先释放槽位,再进入下游监督。尚无提供商全局并发、RPM、TPM 或监督层 Retry-After 控制。
- 本轮协作提供的新增验证记录:24 项并发相关测试、33 项租约相关测试及 typecheck、lint 通过。本次文档修订仅核对实现,没有重新执行这些测试;记录未附精确过滤命令,不补造命令或与此前批次相加。
- 实现代理报告:87 项测试、App/IPC 聚焦用例、另 19 项取消测试,以及 typecheck、lint 通过。本次文档修订未复跑这些命令,不将数量相加当作独立用例总数,也不推断全量测试已通过。
@@ -12,6 +12,8 @@
Main 的手动 IPC 和心跳完成回调均使用 `createProductionSupervisorService`,该工厂连接 `SupervisorService`、`SupervisionReviewStore`、共享模型池及无工具 Runtime。生产路径不再调用旧的 20 会话 × 20 消息 collector,也不使用旧服务分支的 100 条 / 48,000 字符裁剪。旧的有界服务注入方式仍供原有测试和实验脚本使用;心跳报告本身继续使用原报告预算。
2026-10-05 起,监督模型包装层对可识别的临时失败统一重试一次,包含 429、可恢复 5xx 和网络/响应流中断;取消、模型超时、配置及容量错误不重试。每次尝试独立计时,等待可取消,成功批次保留。具体错误与生命周期合同见[技术设计](./technical-design.md#模型阶段超时与调度);下文旧草案中“不增加节点网络重试”的描述已由此规则替代。
### 已接入的调度与存储
schema 47 增加运行配置、来源清单、成功批次、导航节点四张表和一个实时来源视图。迁移只创建新结构,保留此前迁移、结果及用户数据。清单只保存来源身份、版本、长度及增量起点;正文随后按主键和有界 `substr` 读取,派发顺序仍由 `(project_id, conversation_id, sequence, source)` 决定。
@@ -50,7 +52,7 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
| `supervisionReview.executionSeconds` | 300 | 30..3600 | 旧设置兼容字段,执行时忽略,设置页不再展示;恢复旧 run 同样不按此值暂停 |
| `supervisionReview.crossProject` | 关闭 | 开关 | 创建回顾时冻结;关闭时每批只以本项目已有实体为候选,也不生成跨项目故事;开启后候选来自全部项目,故事归属可建立跨项目故事 |
| `supervisionReview.storyThreadEvents` | 20 | 4..500 | 创建回顾时冻结;功能的事件数低于此值时,模型提出的子线索并入功能 |
| `supervisorOrganizeTimeoutSeconds` | 240 | 30..600 | 创建回顾时冻结,节点获槽并解析 Runtime 后计时 |
| `supervisorOrganizeTimeoutSeconds` | 240 | 30..600 | 创建回顾时冻结,节点获槽并解析 Runtime 后,每次模型尝试独立计时 |
| `heartbeatReportTimeoutSeconds` | 240 | 30..600 | 维持报告发起时冻结及既有租约规则 |
| `supervisorModelConcurrency` | 1 | 1..4 | 回顾保存接纳并发;共享池上限仍随应用设置实时变化,实际执行取较小值 |
@@ -59,7 +61,7 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
### 未完成与实测限制
- 真实验证覆盖一个 27 条消息的完整会话;较早的 129 条消息实验在第二次输出 JSON 无效后停止。两实验合计 5 次付费请求,没有重复付费或回退。范围及缓存复核说明见[进度](./progress.md#2026-09-24-完整会话生产验证与收尾)。
- 来源被修改或删除时保留旧批次并明确失败,标记需要新建回顾,活动页停止提供无效的继续按钮;下一次自动检查可创建新 run,不会永久重复该失败。尚未实现仅使受影响叶子和父节点失效后重算。清单记录冻结版本,不保存全库正文快照。
- 现存来源修改仍明确失败并要求新建回顾;会话物理删除时省略该会话来源,丢弃受影响的未发布叶子及依赖导航,保留其他进度。混合批次重算及覆盖规则见[会话删除](./logic-design.md#未发布回顾遇到会话删除)。清单记录冻结版本,不保存全库正文快照。
- 分块配置、超时、接纳并发及算法版本已保存。监督者模型连接在每次执行开始时解析,同次回顾复用;继续或重试读取当前选择,规则见[监督者模型选择](./logic-design.md#监督者模型选择)。候选背景仍在执行时读取;未实现跨重启模型/提示词版本固定,也未实现共享池排空后切换冻结并发的目标规则。
- 元数据初始化已移到 Worker,并通过 2.43 GB 隔离副本验证;[测量记录](./progress.md#2026-10-04-回顾初始化阻塞修复)包括取消与来源一致性。最终发布仍在 Main 同步提交,未完成同等规模的全局发布延迟验收。批次详情已分页,既有历史图谱查询仍返回整份结果;大图谱的响应大小尚未改造。
- 没有 token/费用预算、提供商 RPM/TPM、Retry-After 冷却、执行硬期限、独立清理期限或逐层可配置超时。默认正文预算不保证任意模型上下文容量;超长输出、非法 JSON 和引用错误会失败,保留已成功批次。
@@ -96,9 +98,11 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
目标是不再静默裁剪,把单次上限改成分页、分块和暂缓派发的依据,直至冻结范围内全部项目、会话、消息及其他入选来源处理完毕。模型提取仍可能遗漏原文含义,但已产出的叶子事实、事件、冲突及来源必须完整保留、可分页读取。摘要只用于导航,不能替代叶子结果;“原文已处理”也不等于模型识别了全部事实。
### 当前超时与调度控制清单(已实现)
### 初稿时超时与调度控制清单(历史截面)
以下是当前代码事实,不是后文拟议的多层调度预算。设置暴露两个阶段超时和一个监督并发数。
以下表格及其后说明保留初稿时的实现截面,不定义当前生产行为或本次存储迁移需要新增的机制。
其中“无用户取消入口”“收齐后检查 100KB”和原心跳报告交接已被后续实现替代。
现状以本页第 0 节及[监督者技术设计](./technical-design.md)为准,复用既有回顾准入、模型池和暂停/继续机制,不据此再建一套调度器。
| 控制 | 实际值、读取时点与范围 | 代码依据与边界 |
| --- | --- | --- |
@@ -284,7 +288,7 @@ run(request):
| 执行软预算到达 | 停止读页、接纳和派发,在飞节点在节点及硬期限内结束并保存成功批次 | 未完成时显示预算暂停,可继续;若已满足全部完成条件则可正常发布 |
| 批次或发布事务失败 | 该事务回滚;此前成功批次不回滚 | 从最后持久化位置恢复;发布失败不重算已有合法叶子 |
| 达到采集预算,已接纳任务全部成功 | 保存有限覆盖批次及位置,仍有未读内容 | 显示“未完成,可继续”,不以 `completed` 加提示代替真实状态 |
| 有来源读取失败或运行中被删除 | 初版按本轮失败处理,不将读取失败当作没有新证据 | 显示来源不可用的数量和范围,后续重新查询 |
| 有来源读取失败或运行中被删除 | 会话物理删除按[省略规则](./logic-design.md#未发布回顾遇到会话删除)继续;其他读取失败仍报错 | 省略数独立于保留来源的覆盖统计 |
| 自动无新来源 | 保存 `no_change`,无新结果或图谱事件 | 保留旧结果,模型调用数为零 |
成功批次持久化是用户已授权的续跑要求。最小记录包含 run 冻结范围、目录/页位置、来源 revision 与连续 offset、已校验叶子输出及引用、必要的汇总依赖和成功输出。输出与对应位置同事务提交,以 run 和来源区间等稳定身份防止重复保存;不持久化每次进度动画或完整提示词。崩溃前未提交的在飞批次允许重算,已提交成功批次必须复用。表结构及保留/清理实现仍待设计落地,不能声称现有 schema 46 已支持此能力。
@@ -21,7 +21,7 @@ FR-S1:将现有智能心跳入口整合、升级为监督者,不增加另一
FR-S2:应用包含“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”五个主视图。工作回顾提供统一监督结果阅读、历史选择和手动新回顾,结果显示自身范围与日期;自动监督集中展示计划状态、范围、周期及下次执行时间,以及运行概览、指标、趋势、自动监督报告、待处理记忆与行动建议和心跳审计,创建和编辑使用同一份 Modal 表单;活动记录展示统一实际执行、进度、详情及结果入口,支持按计划 ID 筛选;设置管理模型选择、超时、并发、分页、分批及单次响应容量,分类页签见 UI 设计。计划保存失败保留草稿,关闭未保存修改须确认;编辑已暂停计划保持暂停,创建须显式保存并启用。原有运行记录及应用、存储标识保留。
当前设置还支持分别调整报告与监督整理的模型超时,以及两者共用的监督并发数。并发设置不限制普通聊天,也不保证避免上游限流;排队不消耗模型阶段时限,降低并发不打断在途请求。取值及生效规则由[当前控制清单](./review-scheduling-design.md#当前超时与调度控制清单已实现)定义。
当前设置支持监督整理模型超时及监督模型并发数。并发设置不限制普通聊天,也不保证避免上游限流;排队不消耗模型阶段时限,降低并发不打断在途请求。取值、生效规则及历史报告设置的保留边界由[模型阶段调度](./technical-design.md#模型阶段超时与调度)定义。
FR-S13:监督者的“设置 → 模型”可选择跟随应用默认文本模型或已有文本模型连接,复用连接及其凭据。回顾执行中修改设置不切换模型;生效时点、继续和重试规则见[监督者模型选择](./logic-design.md#监督者模型选择)。
@@ -55,6 +55,8 @@ FR-S8:本地知识库提供双向定位:资料详情可进入相关故事线
FR-S9:来源事实、自动归纳、待核对关联和用户确认内容分别标注。移除关联不删除原文,纠正操作可撤销。来源失效时显示不可用状态;回放历史时使用当时已有的归纳与依据,无法恢复旧版原文时明确提示,不能以当前版本冒充历史内容。
未发布回顾遇到用户删除会话时,跳过该会话来源并继续其他会话,明确显示省略数;无剩余来源时结束且不生成空报告。已发布历史的保留规则不变。具体失效与覆盖语义见[会话删除规则](./logic-design.md#未发布回顾遇到会话删除),验收见 US-S35。
## 落地范围
FR-S11(D1 已实现,D2/D3 待实现):监督者以时态 Story Graph 为主要产出,统一用户侧工作记忆概念并保留已确认偏好与已有数据。Agent 在继续工作、查询此前决定及其修订时,按需使用只读的 `story_graph_search`、`story_graph_get_context`、`story_graph_read_source`,读取范围、时间、演变、覆盖及可引用来源;工具不调用模型或触发回顾,监督者关闭时不暴露且拒绝调用。本地和远程 Runtime 共用读取语义。后续目标为同一来源版本提取一次并跨 scope 复用叶子事实;当前精确 scope 独立处理及 memory 读取继续保留。`as_of` 仅在历史重建能力具备后另行交付。合同与过渡阶段由[专项设计](./story-graph-mcp-design.md)定义,验收见 US-S28 至 US-S31;本轮没有数据迁移。
@@ -9,15 +9,17 @@
| 产品设计 | [监督者应用产品设计](./supervisor-prd.md) |
| 行为规则 | [监督者逻辑设计](./logic-design.md) |
| 界面设计 | [监督者 UI 设计](./ui-design.md) |
| 实施进度 | SQLite schema 50(心跳介入与监督建议见[心跳触发与建议](#心跳触发与建议2026-09-30));当前算法、设置和验收边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界) |
| 实施进度 | SQLite schema 62;页面读取与索引见[页面读取与发布](#页面读取与发布2026-10-05),当前算法、设置和验收边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界) |
本文回答如何在现有 GoodBuddy 桌面端中实现监督者。它不改变产品范围,也不把模拟 Demo 当作生产数据模型。
FR-S11 的读取架构见[时态 Story Graph 内置 MCP 设计](./story-graph-mcp-design.md)。三个只读工具通过 `readStoryGraph` 投影现有 SQLite,Main 在发现、调用和返回前检查监督者启用、会话开关及 Runtime 分配。绑定携带会话 ID,按主键读取当前设置;原生客户端的复用键包含会话 ID 和开关值。本地 MCP、Model、Harness Main 代理和远程受管工具通道共用此入口;会话开关复用 `context_state_json`,无 schema 迁移。该文独立定义对象/版本合同、时间语义、配置映射及后续跨 scope 复用和记忆过渡。当前 memory 背景读取、精确 scope checkpoint 与候选身份保留;结果快照不支持 `as_of` 历史重建。
FR-S11 的读取架构见[时态 Story Graph 内置 MCP 设计](./story-graph-mcp-design.md)。三个只读工具通过 `readStoryGraph` 投影现有 SQLite,Main 在发现、调用和返回前检查监督者启用、会话开关及 Runtime 分配。绑定携带会话 ID,按主键读取当前设置;原生客户端的复用键包含会话 ID 和开关值。本地 MCP、Model、Harness Main 代理和远程受管工具通道共用此入口;会话开关复用 `context_state_json`,无 schema 迁移。当前监督阶段使用共享时间线 checkpoint;候选按下文的批次项目与跨项目设置筛选。memory 继续作为背景读取,结果快照不支持 `as_of` 历史重建。
生产入口由 `supervision-production.ts` 组装服务、SQLite、共享池和无工具 Runtime。`supervision-review-store.ts` 保存冻结来源清单,按有界页和片段供 `SupervisorService` 派发;叶子及连续位置先提交,导航和完整结果之后发布。消息已有知识引用保留本地或外部 locator,已确认记忆通过独立背景输入提供。不会检索外部全库。详细存储、调度及当前限制统一见[分块调度第 0 节](./review-scheduling-design.md#0-生产接线与剩余边界)。
模型实体 `id` 只在单次输出内有效。Main 从同一 scope 的故事线提供最多 100 个未撤销实体候选,生产提示只暴露 `candidateRef`(如 `known_1`)、名称和说明。模型显式选择候选后,Main 转换为严格 UUID 类型的内部 `persistedId`。服务校验候选集成员和重复映射,保存事务再次按实体主键校验故事线归属及撤销状态。没有有效候选引用时分配新 UUID,不按名称或裸模型 ID 合并。来源仍按每次结果分配 UUID,保留原始 `source_id` 和 locator。
模型实体 `id` 只在单次输出内有效。生产服务将批次的 `projectId` 和 `crossProject` 经 `supervisionCandidates` 传到只读 Worker,再由 `listSupervisionCandidates` 查询共享时间线。跨项目关闭时,只提供该项目当前事件关联的未撤销实体;开启时允许其他项目的实体。候选仍最多 100 个,按更新时间和 ID 排序。查询先取得项目事件关联的实体集合,再筛选候选,避免对每个实体重复扫描事件。省略批次参数的旧读取入口保留精确 scope 查询。
生产提示只暴露 `candidateRef`(如 `known_1`)、名称和说明。模型显式选择候选后,Main 转换为严格 UUID 类型的内部 `persistedId`。服务校验候选集成员和重复映射,保存事务再次按实体主键校验存在性及撤销状态。没有有效候选引用时分配新 UUID,不按名称或裸模型 ID 合并。来源仍按每次结果分配 UUID,保留原始 `source_id` 和 locator。
新实体省略 `candidateRef`,不要求模型生成数据库 ID。未知非空 `candidateRef`、冲突身份和候选集之外的 UUID 均拒绝。旧输出中的 `persistedId` 仅在 UUID 合法且属于候选集时复用;空值、占位符和格式错误值不能证明已有身份,保留为独立新实体。局部 ID 或同名不会触发合并。
@@ -31,7 +33,7 @@ schema 43 在结果上增加 `graph_snapshot_json`,保存当次实体名称、
升级从现有事件实体关联、实体变化和来源引用恢复结果成员,不猜测名称匹配;旧数据缺失的归属或已经覆盖的内容无法还原,原对象仍保留。新结果即使实体没有事件或来源,也会保存完整成员。结果内容与运行、事件、来源在同一事务提交。
`overview({ target? })` 返回稳定的 `id`、`storyLineId` 及来源 ID。指定 Conversation/Task 时,Main 读取目标实际项目,查询同时要求目标来源匹配、结果 scope 为 global 或包含该项目;LIMIT 在过滤之后。卡片展示结果真实范围,global 结果不会伪装成单会话摘要。`graph({ resultId, storyLineId? })` 校验二者归属,只返回该结果的事件、对象和来源;显式 ID 无效时不回退到最新结果。继续讨论按结果主键读取,并验证来源属于该结果。
`overview({ target? })` 返回稳定的 `id`、`storyLineId` 及来源 ID。指定 Conversation/Task 时,只读 Worker 读取目标实际项目,查询同时要求目标来源匹配、结果 scope 为 global 或包含该项目;LIMIT 在过滤之后。卡片展示结果真实范围,global 结果不会伪装成单会话摘要。`graph({ resultId, storyLineId? })` 校验二者归属,只返回该结果的事件、对象和来源标题;显式 ID 无效时不回退到最新结果。继续讨论按结果主键读取,并验证来源属于该结果。
工作回顾保存所选结果,切换或刷新使用请求序号忽略迟到图谱。侧栏按目标重建卡片,清空来源和预览,并忽略旧请求;工作栏 tasks 实例的 `targetRef` 可保存 conversation/task 固定目标,取消固定后恢复跟随当前选择。该绑定只控制监督卡片,不改变任务列表的项目范围。继续讨论预览显示实际目标名称和 ID,发送回调显式携带该会话 ID,由 Main 使用该会话的 Runtime 与项目。
@@ -43,6 +45,20 @@ schema 43 在结果上增加 `graph_snapshot_json`,保存当次实体名称、
已确认记忆作为当前背景,不参与新增正文覆盖。最多四条、每条 500 字符,时间来自真实更新时间;模型不能把背景引用为本批新增证据。旧结果继续保留原来源类型和 locator。监督正文上限现用于分批,余段继续处理;心跳报告的有界输入语义没有改变。
## 页面读取与发布(2026-10-05)
对应 K01 的来源投影及 K02、K05、K06。`supervision:overview`、`supervision:graph`、`supervision:stories` 的生产 IPC 校验后分别调用 `listSupervisionResultsAsync`、`getSupervisionGraphAsync`、`getSupervisionStoriesAsync`。它们复用 `readonly-query-worker` 的 AssistantDatabase 独立只读连接,每次领域读取包在一个 `readSnapshot` 内;故事响应中的故事、经验、未归属计数及撤销状态来自同一 SQLite 快照。调用前已提交的写入对下次读取可见。
文件数据库缺少 Worker 配置、启动失败、崩溃或处于原有退避期时,请求直接失败,页面沿用既有错误与重试显示;这三个入口不使用 `readWithFallback`,不在 Main 重跑重查询。内存 SQLite 测试保留同步分支。原工具 `readStoryGraphAsync`、来源详情及其他存储入口保持各自原有实现,没有增加存储进程或通用协议。
结果图谱和 scope 图谱的 `sources` 都只查询共享合同已有的 `id`、`title`、`occurred_at`,并按原表 rowid 顺序返回。正文、来源类型、locator 等仍完整保存在 `supervision_sources`,通过 `supervision:source`/`source-context` 按 ID 读取;历史结果、所有事件、对象和关联均保留。概览是否请求图谱由 Renderer 的页面加载规则决定,后端不截断图谱内容。
schema 62 在既有迁移事务中新增 `supervision_sources(result_id, occurred_at, source_type, id)` 和 `supervision_results(created_at)` 索引。前者供每份概览的首来源定位,后者支持结果时间及 rowid 倒序读取。升级沿用启动存储 Worker,保留表内容和 rowid,不重写正文,不触发历史转换或 `VACUUM`。新版本仍遵守既有的禁止旧客户端打开更高 schema 规则。
故事查询先按 scope 选择故事,再一次读取这些故事的事件成员;经验查询在 SQL 中筛选仍有形成事件、且与所选项目有关的经验,再一次读取其全部有效事件关联。经验的跨项目关联不会随项目筛选被裁掉。两类列表各至多两条 SQL,原列表和成员排序、人工调整状态及完整内容访问保留。
发布继续使用 Main 原有 `BEGIN IMMEDIATE` 事务。每个叶子写入后收集当时的实体、关系及来源引用,末尾只写一次 `graph_snapshot_json`;重复持久身份仍采用原 SQL 分组规则,保留首叶子快照值和全部叶子详情。结果、运行状态、来源、时间线与 checkpoint 仍一起提交或回滚。没有将写入迁到 Worker,也没有把一个完整结果分多次提交。大批量发布仍超过 Main 16 ms 预算,实测与剩余限制见[实施进度](./progress.md#2026-10-05-监督页面读取与发布局部优化)。独立存储服务的职责迁移继续留在[目标方向](../../architecture/desktop-storage-direction.md)。
## 监督者模型连接
`ApplicationSettings.supervisorModelProfileId` 只保存已有连接 UUID,`null` 或旧设置缺少字段时跟随 `RuntimeSettings.defaultModelProfileId`。Main 的 `resolveSupervisorRuntime` 从 `getResolvedSettings()` 读取连接和已有凭据,校验文本协议及认证后,用 `createModelProfileRuntime` 创建无工具直连 Runtime。Renderer 仅接收公开连接信息,不新增凭据存储或 IPC 方法。
@@ -53,9 +69,13 @@ schema 43 在结果上增加 `graph_snapshot_json`,保存当次实体名称、
监督整理超时由 run 创建时冻结;共享模型池仍采用实时设置上限。`supervision:pause` 取消该 run 的排队和在途工作,`supervision:resume` 继续已保存批次;监督整理输出在流中按 `supervisionReview.responseKiB` 检查响应容量,默认 1024 KiB,可在设置页调整为 100 至 16384 KiB。该容量每次请求读取当前设置,继续旧运行也采用新值;超限保留成功批次并提示调高后继续。生成摘要及事实内容不设独立短字符限制,监督旧摘要读取保留全文,具体边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。以下报告领取和租约规则继续适用。
手动和自动监督均在桌面 Main 调用生产工厂,Runtime 解析请求不携带项目、Runtime 选择或执行空间,也不再注入 `workMode`。`resolveRequestRuntime` 返回桌面默认 Runtime,不进入 SSH 项目或 `selectedRuntimes.getRuntime` 分支;内部分析使用无工具构造,回顾 scope 只筛选证据。摘要校验、合并和持久化在 Main 完成,single-flight、取消及 UI 控制不改变 `gbagent`、远端 Runtime 启动或桌面到 Agent 协议。
手动和自动监督均在桌面 Main 调用生产工厂,由 `resolveSupervisorRuntime` 解析上文的直连模型,不进入 SSH 项目或 `selectedRuntimes.getRuntime` 分支;内部分析使用无工具构造,回顾 scope 只筛选证据。摘要校验、合并和持久化在 Main 完成,single-flight、取消及 UI 控制不改变 `gbagent`、远端 Runtime 启动或桌面到 Agent 协议。
`ApplicationSettingsStore` 持久化报告和整理模型超时,均为 30..600 整数秒,默认 240。报告排队前冻结,监督创建 run 时冻结;计时从获槽及 Runtime 解析后开始,覆盖模型执行及其内部重试。流结束校验 signal 和完成事件,finally 释放临时会话,心跳报告保持原有 100KB 上限,监督整理使用上文的可调响应容量。采集、排队、保存和清理不计入模型超时。
`ApplicationSettingsStore` 持久化报告和整理模型超时,均为 30..600 整数秒,默认 240。报告排队前冻结,监督创建 run 时冻结;计时从获槽及 Runtime 解析后开始,覆盖单次 Runtime 执行及其内部重试。监督包装层的第二次尝试重新按同一秒数计时,500ms 等待和临时会话清理不计入该时限。流结束校验 signal 和完成事件,finally 释放临时会话,心跳报告保持原有 100KB 上限,监督整理使用上文的可调响应容量。采集、排队、保存和清理不计入模型超时。
`runSupervisionModel` 为提取、导航、故事、经验和建议共用一次临时错误重试,错误范围见[逻辑规则](./logic-design.md#监督者模型选择)。每次尝试使用新的 request/conversation ID 和隐藏任务,正文缓冲单独创建;用量按各次任务保存。失败后先清理计时器并释放临时会话,再等待;等待继续持有监督池槽位,受父 signal 和池取消控制,结束或失败后释放槽位。`error` 事件的 `status: cancelled` 优先于错误文字;抛出的结构化错误检查 status、statusCode 和 cause,字符串只识别明确的 HTTP 状态或已知网络/流中断格式。
直连文本请求保留 HTTP 错误的 status,并在响应流失败前交出已收到的用量;没有提供方用量时不估算补记。监督响应超容量后立即 abort 并清空正文,最多继续消费 1 秒,仅保存用量事件,不累积后续文本;随后请求关闭迭代器、释放临时会话和池槽位,迟到事件不再写入。容量错误保持原文,不被随后到达的超时或流错误替换,也不重试;已缓冲完成响应和中止响应均沿用原 call ID 记账。重试决策只在监督包装层,直连传输原有重试、OpenCode 和 Continue 策略不变。429 使用同一 500ms 等待,不新增 Retry-After、UI 或配置项。
`supervisorModelConcurrency` 为 1 至 4 的整数,默认 1。报告和监督整理共用 Main 内 `SupervisionModelPool` FIFO 池,普通聊天不入池;降低上限不终止在途请求。报告槽位从领取前持有至报告完成或无变化提交后,在下游监督开始前释放,避免并发 1 时相互等待。监督服务外层运行仍串行;该池不是完整分块调度器,也不提供跨聊天与监督的提供商全局并发、RPM、TPM 或 Retry-After 控制。
@@ -63,6 +83,10 @@ schema 43 在结果上增加 `graph_snapshot_json`,保存当次实体名称、
schema 47 的四张批次相关表使用外键随监督运行清理,未完成运行不新增自动过期删除规则。启动时原有 running 记录会标 failed,但已保存批次和配置保留,活动页仍可继续。发布逐页读取叶子并复用现有结果/来源/图谱写入,在同一事务更新完成状态和自动 checkpoint。
会话删除复用 `SupervisionReviewStore.omitDeletedConversations`,只处理 `running/paused/failed` 的暂存记录。删除入口在事务内补齐旧任务清单的会话归属;新清单由 Worker 在初始化时记录任务所属的现存会话。初始化完成、恢复、分块读取、模型调用前及返回落库、导航合并和发布前均检查删除。初始化中的清单仍可丢弃重建,省略计数在完整清单提交后计算。
失效计算只取批次的来源键,不把保存正文作为删除会话的替代输入;依赖导航沿 `children_json` 递归删除。省略数写入既有 `state_json.omittedSources`,透出到活动进度及结果 `coverage_json`,不增加 schema。发布事务先核对省略数与待发布结果一致,再保存事实和 checkpoint;若删除使摘要过时,服务返回现有提取/合并循环。已发布历史不参与失效,行为定义见[逻辑规则](./logic-design.md#未发布回顾遇到会话删除)。
当前设置及冻结范围见[当前设置合同](./review-scheduling-design.md#当前设置合同)。未实现的高级参数继续保留在目标清单,不显示为可保存控件;没有兼容原型数据库的读取器。
### 单次执行与取消
@@ -71,6 +95,10 @@ schema 47 的四张批次相关表使用外键随监督运行清理,未完成
`supervision:execution` 返回进程内 `{ active, runId?, stopping? }`,配置读取期间可有 `active: true` 而尚无 runId。`supervision:cancel({ runId })` 先在既有 `supervision_runs` 写入 `cancelled`、清除错误并保存取消时间,再向父 controller 传播 abort;重复取消保留原时间。该接口等待执行 Promise 结束,包含并行批次的 `allSettled` 和 Runtime `releaseConversation`,之后才释放准入。暂停只请求 abort,执行停止后保存 `paused`;取消优先于暂停。迟到模型输出不得保存叶子、导航或完整结果,成功批次保留,自动 checkpoint 不推进。发布是同步 SQLite 事务,已完成或无变化的运行拒绝取消,不撤销已发布结果;本次无 schema 迁移。
发布前重读已保存批次所需候选后,再检查父 signal。此时收到暂停或取消,走原有停止分支;完整叶子和导航保留,结果及 checkpoint 尚未提交。该检查与同步保存之间没有新的异步等待。
故事归属和子线索拆分在既有写事务内,按主键检查模型选中的目标、父故事和跨项目功能仍为 `current`。模型等待期间若用户移除或合并目标,整批写入回滚,包含新建故事和已处理标记;重新整理使用当前候选。拆分同样检查原功能及目标子线索。已发布回顾、用户调整和此前成功批次保留,不增加版本表或恢复日志。
Preload 暴露类型化 `execution`、`cancel`,取消输入使用 UUID schema 并校验可信 sender。活动 UI 用实时执行状态覆盖持久记录的停止显示;工作回顾只在发起前查询执行状态,Main 仍作最终准入判断。`resume` IPC 直到运行结束才返回,Renderer 发出请求后释放提交锁并独立处理结果,确保继续中的回顾仍可暂停或取消。
`App` 将既有通知回调经 `HeartbeatCenter` 传给 `SupervisorWorkspace`。回顾操作用 `supervisor-review` 去重键发送本地化 Toast,结果读取失败仍写入 `loadError` 并就地重试;实体、关系操作和来源读取失败也使用通知。工作回顾不再为状态条轮询执行状态,历史读取不发送操作通知。请求锁覆盖发起前检查和整次运行;页面刷新只使旧结果选择失效,不丢弃运行结束反馈。卸载后忽略迟到响应,检查阶段尚未提交的回顾不再发起,已提交的后台运行不取消。活动查询、原始错误持久化和 Main 生命周期保持原路径。
@@ -98,7 +126,7 @@ IPC `supervision:suggestions`、`supervision:suggestion-action`、`supervision:r
只有实际送入模型、通过结果校验且成功落库的完整片段推进位置。心跳报告及其进度、监督结果及其进度分别在各自 SQLite 事务提交。监督服务串行执行,后续自动请求在前次保存后重新收集。报告成功而监督失败时,下一次自动检查跳过已成功的报告输入,但重试监督输入;无变化不创建报告、来源或图谱事件。
已有同 scope 故事线的 scope 表示和实体 ID 保留;比较项目集合时忽略排序。模型接收最近摘要和同 scope 最多 100 个未撤销实体的候选引用、名称、说明,显式选择候选才复用,身份转换按上文执行。不同范围不合并,已有重复实体不自动修复。知识引用继续以独立来源保存 locator;当前记忆和旧摘要不作为新事件。
已有故事线的 scope 表示和实体 ID 保留;比较项目集合时忽略排序。生产候选按上文的项目归属与跨项目设置读取,显式选择候选才复用;已有重复实体不自动修复。监督 checkpoint 的共享时间线规则见[故事线模型](./storyline-model-design.md)。知识引用继续以独立来源保存 locator;当前记忆和旧摘要不作为新事件。
迁移在事务中扩展心跳计划及运行表的状态 CHECK,保留列、索引、ID、计划、报告与引用;消息版本列使用默认值,无需遍历或重写旧正文。进度初始为空,首次自动运行从配置窗口开始。报告保留期限覆盖 `no_change` 运行,但不删除来源进度;删除报告不触发重新消费。
@@ -326,7 +354,7 @@ IPC 返回图谱查询结果时使用分页和有界字段;详情中的来源
## 当前剩余差距
- 实体与关系 revoke 保存状态;界面没有 undo 或恢复入口。
- 跨 run identity 依赖同 scope 候选 UUID 的显式引用,不自动合并历史重复实体;模型漏选候选时会创建新身份。
- 跨 run identity 依赖候选 UUID 的显式引用,候选范围受批次项目及跨项目设置控制;不自动合并历史重复实体,模型漏选候选时会创建新身份。
- 结果图谱使用真实事件实体连线,侧栏已按固定目标过滤并支持结果直达图谱;逐事件状态回放和 Experiment 尚未实现。
- 用户暂停已接到排队/在途 signal,并保留已保存批次;源版本变更后的局部重算、完整模型配置冻结及其他限制见分块调度第 0 节。
- 监督知识实体与知识库正文条目仍是不同对象,知识正文只能通过现有条目定位和预览接口处理。
@@ -12,6 +12,8 @@
工作回顾与故事线图谱在主页签下方共用回顾上下文栏。历史选择器最多占 280px,收起时只显示生成日期时间,随后是“刷新”和“新回顾”,不在右侧重复范围和覆盖区间。展开复用共享锚定单选菜单,每项显示生成时间及该结果自身的真实范围、覆盖起止时间,长说明换行;当前项带选中标记,触发按钮的无障碍描述保留所选结果的完整上下文。即使只有一条结果也显示选择器。切换页签和图谱模式时,这一栏的位置、内容和选择保持一致,空态及读取失败时仍可访问。
首次打开工作回顾只读取回顾及其范围的故事摘要,不等待图谱。进入故事线图谱时才读取所选结果的图谱;读取期间在图谱页显示加载状态,返回工作回顾仍可阅读及选择历史。工作回顾与图谱共用已读的故事数据,普通页签切换不重复读取。当前结果的图谱在切换页签时保留;切换历史结果、显式刷新、实体或关系修改后的刷新会使旧图谱失效。来源列表只需标题和时间,点击来源才读取完整正文。
点击“新回顾”才展开范围、时间、发起按钮和重新整理菜单。表单位于上下文栏下方,以次级表面区分新请求与已选结果;再次点击收起并保留输入。工作回顾正文铺满页面壳层,不另设阅读宽度上限。容器宽于 860px 且有辅助内容时,左侧约占三分之二,显示工作总结和本次变化;右侧显示未解决事项、故事进展和经验,以弱分隔线分区。没有辅助内容时只保留全宽正文。窄屏按正文、辅助内容顺序排列为单列。摘要保留原有段落和换行,故事摘要保留“显示更多”及进入图谱的选择。
新回顾运行中保留已显示结果及其日期、范围,范围、时间、发起和刷新控件保持可用。手动发起、完成、暂停、取消、无变化和失败统一使用应用 Toast;重复点击发起提示已有运行,不新建或排队。同一回顾操作的通知互相替换,关闭通知不暂停或取消执行。成功和信息沿用共享自动消失时间,错误由用户关闭。页面不保留操作状态条或错误横幅,加载历史结果、切换页签和重新进入页面不触发回顾通知。
@@ -44,6 +46,8 @@
“已保留事实与来源”展开后才查询既有批次接口,每页 10 批,按本页持久记录组成项目、会话、批次层级。项目使用可用名称,会话优先使用保存来源的标题;只有已保存批次标记“已保存”,不创建未落库的待执行、在途或失败叶子,也不把本页数量当作项目总量。批次摘要收起时最多两行,展开后保留完整摘要、事件、实体、变化、关系及原文片段。来源显示消息 ID、码点范围及版本;长原文局部滚动。原生 details/summary 保留键盘展开和可见焦点,轮询不主动展开用户收起的会话。加载、空批次和读取失败分开显示,失败可重试;换页失败不显示上一页批次,切换 run 后旧响应失效。暂停状态为“未完成,可继续”,手动回顾主动暂停后在工作回顾页提示去活动继续,不显示完成通知。
删除会话导致省略来源时,阶段进度下方和运行详情均显示省略数量,并说明覆盖统计只含保留来源。中文为“会话已删除,已跳过 N 个来源”;英文为“N sources skipped because their conversations were deleted”。全部省略时活动显示无新变化,不提供空结果跳转;已发布历史仍可查看。规则见[会话删除](./logic-design.md#未发布回顾遇到会话删除)。
活动页运行中同时提供“暂停回顾”和“取消回顾”,继续后的运行也可操作。停止期间禁用两项操作,并在运行标题和当前阶段显示“正在暂停”或“正在取消”;即使持久记录已经为已取消,只要执行尚未停止就继续显示“正在取消”。重新进入活动页通过实时执行状态恢复该提示。停止后显示“已取消”,保留事实详情,不显示继续按钮或失败恢复提示;任一回顾执行或停止清理期间,其他记录的继续按钮禁用。
## 智能心跳设置
@@ -86,9 +90,17 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
2026-09-30 已实现(规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计)):主按钮“回顾”执行增量整理,文案不再称“重新分析”;旁边“更多”菜单提供“重新整理…”,确认对话说明将重新分析所选区间并可能产生模型费用。计划 Modal 增加“介入方式”单选:“生成建议”“仅更新记忆”;主动介入上线前不显示。最近建议替代,每条建议显示依据、打开来源及转任务/确认/忽略。“最近建议”替代,每条建议显示依据、打开来源及转任务/确认/忽略。
当前页面提供全局或单项目范围,以及 1、7、30 天时间区间选择。没有自动计划也能手动回顾;成功结果显示自身冻结的范围、时间和未解决事项,可选择历史结果或进入图谱。来源筛选由本次回顾范围和时间限制;图谱详情只列出所选事件的关联来源,实体或关系列出相关事件的来源。独立的来源类型选择器尚未实现。
当前页面提供全局或单项目范围。时间选择保留 1、7、30 天快捷项,默认 7 天,并提供“自定义天数”和“指定日期”。快捷项与自定义天数均从提交时刻向前计算,每天按 24 小时计;自定义值必须为正整数,且起始时间不得早于公元 1 年。
选择历史结果后,图谱读取该结果的成员和内容,历史菜单显示其回顾区间和生成时间。切换时清空旧图谱和详情并显示加载状态,迟到请求不得替换新选择。确认、修订和移除反馈更新当前所选结果,后续自动回顾不改写旧结果。
指定日期使用带可见标签的原生日期输入,按本地时区解释。起始时间为开始日期的零点,结束时间包含结束日期全天:取下一个本地日期的零点减 1 毫秒,跨夏令时不按固定 24 小时推算;结束日期为今天时截到提交时刻。允许同一天,拒绝缺失、无效、未来日期及开始晚于结束。天数和日期输入复用共享字段样式,窄窗口换行;错误就地显示,通过 `aria-invalid` 和 `aria-describedby` 关联输入,无效时禁用回顾和重新整理,提交入口再次校验。
重新整理确认显示当前范围及最近天数或明确起止日期,日期文案说明本地日期和今天截至当前的规则。修改范围或时间输入后关闭旧确认,重新整理仍需再次确认。新选择只改变请求时间范围,普通回顾继续跳过已处理且未变化的来源,重新整理仍使用既有显式标记,不改变增量处理规则。
没有自动计划也能手动回顾;成功结果显示自身冻结的范围、时间和未解决事项,可选择历史结果或进入图谱。来源筛选由本次回顾范围和时间限制;图谱详情只列出所选事件的关联来源,实体或关系列出相关事件的来源。独立的来源类型选择器尚未实现。
选择历史结果后,历史菜单显示该结果的回顾区间和生成时间,旧图谱和来源详情清空。在图谱页选择历史时立即读取对应成员和内容;在工作回顾页选择时,图谱读取留到下次进入图谱页。迟到请求不得替换新选择。确认、修订和移除反馈更新当前所选结果,后续自动回顾不改写旧结果。
切换语言或手动回顾完成引起刷新时,同时解除被替代的来源读取、实体和关系操作的等待状态。工作回顾在回顾读取完成后恢复历史选择和刷新;图谱页等待对应图谱读取完成后恢复操作。图谱失败只在图谱页提示并提供重试,不阻止阅读已读回顾;旧请求迟到时既不显示旧内容,也不能解除后来发起的请求的等待状态。
手动“回顾当前进展”新建回顾,活动页“继续”恢复原 run;运行中再次发起按上文显示提示,停止清理结束后才能开始新回顾。失败保留上次结果、本次范围和成功批次;已取消回顾不能继续。手动操作不推进自动位置,删除报告也不重置自动位置。
@@ -117,12 +129,16 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
时间螺旋使用所选回顾的时间区间与完整事件集合,半径和区间着色规则见 [3D 密度规则](../story-graph/3d-concept.md)。当前故事成员按精确事件 ID 与结果求交;其余事件按来源确定的项目进入“当前无故事归属”分组,项目未知时显示“项目范围不可用”。图例说明“本次回顾事件按当前故事归属分组;未保存当时的故事归属”。故事与经验列表仍提供该范围的当前完整记录,供查看和调整。
根视图直接显示事件点,同一主故事的邻近事件按画布密度聚合并显示数量,未归属点使用中性色。事件和经验从左侧列表选择,画布不重复提供这两种选择器。保留一个层级菜单:左侧列表选择记录并打开详情,标题区层级菜单改变画布中的项目、功能或子线索层级。菜单与面包屑均可用键盘操作。默认斜俯视,不显示视角按钮、菜单或页签;自由拖动、平移、滚轮、方向键、数字键和 Escape 返回上层操作保留。
根视图直接显示事件点,同一主故事的邻近事件按画布密度聚合并显示数量,未归属点使用中性色。事件和经验可从左侧列表或画布标记选择,画布不重复提供这两种选择器。保留一个层级菜单:左侧列表选择记录并打开详情,标题区层级菜单改变画布中的项目、功能或子线索层级。菜单与面包屑均可用键盘操作。无选择时默认斜俯视,不显示视角按钮、菜单或页签;自由拖动、平移、滚轮、方向键、数字键和 Escape 返回上层操作保留。
时间螺旋选中故事、事件或经验后,相机沿较短方向旋转并平移到对应位置,保留当前缩放;缩放超出 0.8~1.4 时收回到该区间。列表选择的目标不在当前层级时,切换到能显示该目标的层级;经验须在本次回顾内有跨木片的形成与应用连接才能定位,没有对应几何对象的记录仍可查看详情。选中的经验不受默认六条连接的显示上限限制。鼠标拖动、滚轮和方向键立即接管相机,后续相同选择和同内容刷新不重新定位。启用减少动态效果时直接定位,离开视图时取消未完成动画。
关联来源详情属于会话时,显示“打开会话”按钮,使用来源保存的会话 ID 跳转并切换到所属项目;不发送消息或启动回顾。另提供“继续讨论”,使用上述可编辑正文预览,点击发送后提交到来源会话并打开它。详情继续展示整理时保存的原文快照。会话已删除或读取失败时,沿用应用会话导航的提示;其他来源类型不显示此按钮。
左侧使用共享 PageTabs 切换“事件 / 实体 / 关系”,显示各类数量,一次只展示一类列表,无记录时显示本次回顾的分类空态。页签固定在顶部,列表占满剩余高度并独立滚动。切换页签不清除当前图谱选择;从画布、事件浏览或详情选择对象时同步切到对应页签。支持方向键、Home/End 和面板键盘聚焦。
事件、实体和关系长列表复用共享列表窗口,只挂载视口附近的行,并保留选中行和焦点行。记录总数、完整名称及末条记录均可访问,不以截断记录提速。列表内上、下方向键移动焦点,Home/End 到首、末条,Enter 或 Space 选择;Tab/Shift+Tab 按完整记录顺序移动,在首尾离开列表。键盘移动跨过未挂载的行时,先挂载目标行再聚焦。行高测量或窗口变化不得把刚聚焦的可见行移出视口;用户主动滚走时保留焦点行,但不把滚动位置拉回。
使用 `supervisor` 页面壳层,最大内容宽度为 `1440px`。当前生产图谱左侧为紧凑的事件、实体和关系列表,中间为 SVG 画布,右侧为详情与来源。桌面列宽为 `216px minmax(0, 1fr) 280px`,画布获得主要空间。外圈事件按发生时间排序后等距逆时针排列,不表示实际时间间隔;起止之间保留缺口和方向箭头。内部显示真实实体名称,实线仅连接实际事件与实体,虚线仅连接实际实体关系。
高度按窗口剩余可见空间计算,底部保留约 24px 边距(不小于页面底部内边距)。平铺图最低 660px,时间螺旋最低 320px;低于最小高度时页面允许纵向滚动。中间画布不内部滚动,工具栏与事件浏览栏之外的空间给图;左侧列表和右侧详情各自滚动。两种视图的图例均使用共享 InlineHelp 的纯 i 图标,保留可访问名称和 title;点击或 Enter/Space 固定展开,再次点击、Escape、点击或聚焦外部关闭。平铺可见数量仍在画布角落;螺旋左下角不显示圈长及圈数,这些信息收进图例。窗口尺寸及上方表单展开、收起后重新计算可用高度。
@@ -650,6 +650,19 @@
| 已选连接被删除或变成图片模型 | 打开设置并尝试执行 | 显示连接不可用,执行报错,用户可改选;不自动回退 |
| 模型选择保存失败 | 留在设置页 | 保留选择草稿及重试入口 |
### US-S35 删除会话后继续未发布回顾
对应 FR-S9,规则见[会话删除](./logic-design.md#未发布回顾遇到会话删除)。
| Given | When | Then |
| --- | --- | --- |
| 回顾已保存批次但尚未发布 | 删除其中一个会话后继续 | 跳过该会话的消息、知识引用和已记录归属的任务,保留无关批次 |
| 来源正在初始化、提取、合并或等待发布 | 用户删除会话 | 丢弃受影响输出及依赖摘要,继续其余来源,不从片段恢复会话 |
| 一个旧批次包含已删除及仍存在的会话来源 | 继续回顾 | 重做受影响的存活来源,无关来源不重做 |
| 所有待回顾来源所属会话均已删除 | 继续或结束当前请求 | 显示省略数及无新变化,不生成空报告 |
| 删除前已有发布结果 | 删除会话 | 既有历史结果和历史片段不变;未发布结果按省略后的覆盖发布 |
| 会话仍存在,来源正文被修改或会话被归档 | 校验来源 | 明确失败,不把修改或归档记为删除省略 |
## 验收优先级与依赖
首个可用版本应先打通以下路径:
+18 -1
View File
@@ -10,7 +10,24 @@
| 日期 | 2026-09-09 |
| 关联入口 | [直连模型 Agent 能力](./README.md) |
## 当前结论
## 2026-10-05 工具执行异常回传
- 普通工具执行异常也返回模型,保留失败活动与错误详情,由模型决定后续行动;规则见
[失败恢复](./technical-design.md#读取与搜索失败恢复)。请求取消与 `AbortError` 仍停止循环。
- 新增三协议回归使用真实 Provider、进程服务和文件系统:无效 cwd 产生 `ENOENT`,
下一次模拟模型请求收到对应失败结果,再调用同一工具执行真实命令成功。另覆盖错误详情
有界处理和取消;未使用真实外部模型,不作为桌面 UI 或真实模型自主决策验收。
- 修复验证中发现 `DOMException` 不一定满足 `instanceof Error`,改为按错误名称识别取消;
取消测试在意外后续模型请求时立即失败,避免模拟响应无限重复耗尽内存。
- `model-runtime.test.ts`、`model-tool-provider.test.ts`、`direct-model-process-service.test.ts`:
161 项通过、1 项跳过。`npm run typecheck`、`npm run lint` 通过。
- 全量 `npm test`:6,230 项通过、87 项跳过、1 项失败;失败为
`obsidian-electron.test.ts:29` 要求临时目录在仓库外,与本次按仓库规范设置
`TEMP`/`TMP` 到仓库 `temp/` 冲突。未修改该测试,全量不能记为通过。
- 托管 SSH 使用远端 OpenCode/Continue 的工具循环,不运行本次修改的 `ModelAgentRuntime`;
不涉及 gbagent 生产路径。临时目录创建、保留和清理逻辑未修改。
## 既有本机验收结论
本机生产路径已经完成源码接线:直连模型 Execute 可运行平台 Shell,Ask/Execute 均可按
父模式委派单层编程 Subagent;长输出可通过 `output_read` 续读。OpenCode、Continue、
@@ -98,8 +98,11 @@ type BuiltinModelToolSummary = {
`EISDIR`、`EACCES`、`EPERM`、`ELOOP`、`ENAMETOOLONG` 等已列明的文件系统错误。
Runtime 向模型返回 `{ ok: false, recoverable: true, error, nextAction }` 的 JSON 文本工具结果,
工具活动记为 `recoverable`,继续工具循环,由模型修正路径、分页参数、glob 或搜索表达式;
提示不得原样重复失败参数。取消、远端断线、失效工作区、缺失 rg 可执行文件及未分类的内部
错误继续向外传播,不转为可恢复结果。
提示不得原样重复失败参数。其他工具执行异常(包括进程启动或输出文件的 `ENOENT`、
远端断线、失效工作区、缺失 rg 可执行文件)由 Runtime 返回 `{ ok: false, error }`,
工具活动记为 `failed`,错误详情使用已有有界处理并保留 cause 信息。失败结果继续进入
下一次模型请求,由模型决定重试、调整方法或说明失败;不自动重放工具,也不增加重试次数限制。
Anthropic 的失败结果同时设置 `is_error: true`。请求取消及 `AbortError` 仍向外传播并停止循环。
`workspace_read_text` 仍只接受工作区内相对路径。搜索原样接受 rg 参数及账号可访问的路径,
不增加逐工具授权;保留 `--no-config` 默认参数、原生退出码、取消和分页,不提升系统权限。
@@ -116,13 +116,11 @@ confidence 改为可选,保持本地返回原值;多块内容组成逐页 se
original.<ext>
documents/<attachmentId>/
original.<ext>
parsed.md
manifest.json
images/<imageId>.<ext>
knowledge/<kbId>/<sourceId>/
<managed-original-files>
knowledge-assets/<kbId>/<documentId>/
parsed.md
manifest.json
images/<imageId>.<ext>
temp/document-parsing/<operationId>/
@@ -155,7 +153,7 @@ confidence 改为可选,保持本地返回原值;多块内容组成逐页 se
也需验证同一规则。采用暂存、现有事务和按数据库引用核对清理,不新增恢复引擎。
测试解析使用任务临时目录,结束预览且无有效引用后清理。知识库正文及 chunks 继续存数据库,
`knowledge-assets` 的 `parsed.md` 为解析正文文件,随同一次结果提交保持一致,不独立修改
`knowledge-assets` 的 `manifest.json` 保存解析正文,随同一次结果提交保持一致,不独立修改
或替代数据库中的检索内容。图片归知识库文档所有,不依赖聊天存活;managed 与 reference
派生文件均存该目录,reference 外部原件保持原路径,现有 managed 原件目录不迁移。
重解析或同步先在本次暂存目录完成新结果与必要资源校验,成功提交替换后清理无引用旧资源;
@@ -44,17 +44,21 @@ PPTX 回退提示按实际 OCR 来源显示,并在图片处理失败时包含
conversation-assets/<conversationId>/
images/<resourceId>/original.<ext>
documents/<resourceId>/original.<ext>
documents/<resourceId>/parsed.md
documents/<resourceId>/manifest.json
documents/<resourceId>/images/<imageId>
knowledge/<libraryId>/<sourceId>/<managed-original>
knowledge-assets/<libraryId>/<documentId>/<resultId>/
parsed.md
manifest.json
images/<imageId>
temp/document-parsing/<resourceId>/
```
S04 (2026-10-05): new results store parsed content in `manifest.json` only;
`parsed.md` is no longer written or copied. Released manifests remain readable,
including directories containing the old duplicate. Async storage-host integration
and its remaining composition work are specified in
[Desktop File Storage Integration](../../architecture/desktop-storage-files.md).
会话资产的 `request.json` 保存已转换的请求内容,用于恢复内存 context;原件字节单独保存,
缩放和 JPEG 请求副本不覆盖原图。提取图片文件使用 UUID 无扩展名,MIME 在清单记录。
图片显式 OCR 后生成解析文件。切回原图保留已有结果,并重新进入图片能力校验。
+19
View File
@@ -1,5 +1,24 @@
# Magic Notes Progress
## 2026-10-08: Rich Text Table Editor Fix
Replaced the Electron-incompatible native prompt flow for table insertion with the shared form
Modal. The dialog validates 1-20 rows and 1-12 columns, supports cancellation and Escape, restores
focus to the toolbar trigger, and preserves the editor selection while it is open. Table row,
column, and delete controls now use compact localized icon buttons, remain disabled outside a table
cell, guard against nested insertion, preserve the active cell, and create separate Quill undo
steps. Non-empty table Delta content remains readable in the read-only record renderer.
Focused validation: `npx vitest run src/renderer/src/MagicNoteEditor.test.tsx
src/renderer/src/MagicNoteContent.test.tsx` passed 18 tests. `npm run typecheck` and affected-file
ESLint passed. Full suite and runtime Electron validation were not run.
The table content check now also applies to save validation. An empty-cell table remains an active
draft after focus leaves the composer, saves through the renderer, IPC and storage layers, and is
restored after reopening. A rich Delta containing only a terminal newline remains rejected, and a
failed save leaves the draft available for retry. Focused renderer and storage regression tests
cover these cases.
## 2026-10-03: Compact Notes List And Narrow Drawer
The notes list now defaults to 200px with a 160px minimum. At workspace widths
@@ -71,6 +71,16 @@ save remounts at the returned revision and establishes a new baseline; an unchan
background revision retains the existing editor and baseline. Aborted canvas
initialization and file reads for an unmounted text editor cannot write back.
Rich text tables remain native Quill table Delta operations. Insertion stores the active editor
range while the shared table-size dialog is open, rejects invalid dimensions before changing the
Delta, and refuses insertion when that range is already a table cell. Row, column, and whole-table
commands require a cell selection, preserve the selected range, and cut Quill history around each
command so each toolbar action is independently undoable. The read-only rich renderer loads the
same Delta with Quill's `readOnly` mode, so table content follows the same persisted schema without
an editable toolbar. A table row operation is content even when its cell text is empty: the
workspace keeps the composer open after focus leaves it and submits the Delta through the normal
IPC and storage path. A genuinely empty rich Delta is still rejected before IPC.
The real saved formatted-text probe reproduced identical values with different
Delta key order: storage returned `{insert, attributes}` and Quill emitted
`{attributes, insert}`. The old `JSON.stringify` comparison therefore reported
+8
View File
@@ -49,4 +49,12 @@
Tab 使用工作栏现有的共享页签、焦点和关闭模式。搜索有持久标签,列表用可键盘访问的按钮;打开采集后聚焦预览,取消返回触发按钮,触发按钮已不可见时尝试返回会话标题菜单按钮。新建标题出现后聚焦输入框。面板中的 Escape 取消当前草稿,或从详情返回列表;需要放弃输入时先显示草稿确认。确认框初始聚焦“继续编辑”,限制 Tab 焦点范围,Escape 保留草稿并关闭确认。
## 富文本表格
富文本编辑器使用共享表单 Modal 插入表格,行数限制为 1-20,列数限制为
1-12。取消、关闭、Escape 和遮罩点击不会修改内容,关闭后焦点返回“插入表格”
按钮。表格操作使用紧凑图标按钮,保留本地化的可访问名称和提示;未选中表格单元格
时,插入行、插入列和删除操作保持禁用,避免误操作或嵌套表格。每个表格操作形成
独立的撤销步骤。已保存表格在只读记录视图中继续由 Quill 以不可编辑单元格显示。
面板服从工作栏现有宽度和停靠规则,以单列切换列表、详情和采集编辑。按钮组可换行,文字摘要保留换行并折行长文本;Markdown 和代码以原始文字显示。头部与快速追加区固定,中间内容独立滚动;完整预览输入区可纵向调整大小。配色、按钮、焦点环、通知和确认使用共享组件与令牌。真实 Electron 已在实测 214px 面板验证无横向溢出,以及原生键盘返回列表、打开笔记和追加保存。
@@ -365,6 +365,18 @@ Runtime 包。2026-09-19 完整桌面验证覆盖联合包安装、CN 项目创
同次验证复现原生 ACP 冷启动超过 15 秒控制超时。`runtime/startPrompt` 的控制等待
现至少为 30 秒,其余控制请求沿用原时限;Runtime 外层仍保留启动超时和取消处理。
2026-10-05 修复启动等待阻塞控制队列的问题:原生启动等待同时释放 backend 全局控制
队列和协议接收分派,取消、watchdog 及其他 binding 的控制请求可继续处理。相同启动
请求只执行一次,迟到返回的已取消 Session 单独关闭;不增加 Prompt 总时限或新协议
阶段。实现规则见[共享进程与 Session 所有权](../assistant-workbar/runtime-process-reuse-technical-design.md#61-连接与所有权),
本次源码 Host 验证及调用计数见[统一执行进度](../unified-execution/progress.md#2026-10-05-runtime-生命周期审查修复)。
同日 K07 补齐模型路由准备后的取消:broker 关闭及终态提交保持原顺序,原生路由
撤销的 HTTP 等待移出全局控制队列。安装晚于取消完成时,以捕获的 transport、Session
和 operation 再次撤销该路由;原生 helper 只删除 operation 匹配的项。路由 HTTP 请求
仍有 10 秒上限,transport 退出或释放时中止未完成请求。当前源码的 OpenCode/Continue
实机证据见[局部修复验证](../unified-execution/progress.md#2026-10-05-k07k08-局部修复)。
## ACP 与断线
- 管控面只负责有界 JSON-RPC:连接、查询、启动、取消、关闭和重连。数据面只负责
+1 -1
View File
@@ -10,7 +10,7 @@ Smart Heartbeat 已提供按计划或手动执行的有界回顾,生成报告
## 文档导航
当前监督者设置已接入报告与监督整理两项模型阶段超时(30 至 600 秒,默认 240)及共享监督并发数(1 至 4,默认 1),模型超时不包含排队、采集、保存或整次执行期限。报告先获槽再领取,实际请求开始前校验并刷新租约,报告提交或无变化后释放槽位再进入监督。普通聊天不入监督池,尚无跨聊天与监督的提供商全局并发或 RPM/TPM 限制。租约、固定传输时限及重试与用户超时分开,见[控制清单](../conversation-supervision/review-scheduling-design.md#当前超时与调度控制清单已实现)。完整分页与持久化续跑仍为设计,不能把现有增量检查称为完整调度器。
当前心跳将到期计划交给监督回顾,不再额外生成独立报告。监督整理使用模型阶段超时和共享监督并发池,普通聊天不入池;模型超时不包含排队、采集和保存,也不是整次回顾期限。分页、批次持久化和暂停/继续已接入生产路径,范围及剩余限制见[生产接线](../conversation-supervision/review-scheduling-design.md#0-生产接线与剩余边界)。超时、并发、重试及保留的历史报告设置由[模型阶段调度](../conversation-supervision/technical-design.md#模型阶段超时与调度)定义,不据初稿控制表另建调度器。
监督者“活动记录”页签同时显示手动回顾和心跳执行,将新心跳报告与下游监督按运行 ID 合并,保留各阶段真实状态。心跳审计、运行概览、指标、趋势、自动监督报告阅读和建议处理与计划列表同在自动监督页;工作回顾只读取统一监督结果,设置只保留配置。当前布局见[监督者 UI 设计](../conversation-supervision/ui-design.md#应用入口)。心跳审计继续读取报告自身状态,完整执行状态以[活动规则](../conversation-supervision/logic-design.md#活动记录)为准。
+1 -1
View File
@@ -196,7 +196,7 @@ type HeartbeatScope =
- Main 在创建、编辑、列出和执行时验证 Project 归属。
- Global 读取所有 Project 的有界会话和任务,但长期记忆仍只读取 Global。
- 指定 Project 读取选中项目的有界会话和任务,以及 Global 与选中项目记忆。
- 现有输入条数、字符预算、输出大小、超时、重试、租约和工具禁用边界继续生效。报告与监督整理共用监督专用并发池,普通聊天不入池;具体设置、租约和未实现的限流边界见[当前控制清单](../conversation-supervision/review-scheduling-design.md#当前超时与调度控制清单已实现)。
- 心跳交接、监督分页与输出容量、模型阶段超时、重试和工具禁用边界以[生产接线](../conversation-supervision/review-scheduling-design.md#0-生产接线与剩余边界)及[模型阶段调度](../conversation-supervision/technical-design.md#模型阶段超时与调度)为准。监督模型池不限制普通聊天,不将历史独立报告阶段作为当前必须执行的路径。
- 多项目汇总后统一应用上限,不能按项目倍增预算。
- 心跳结果默认不在系统通知中暴露私人正文。
- 数据迁移必须使用 SQLite 事务,保留外键、级联删除和现有历史。
+8 -2
View File
@@ -1,14 +1,20 @@
# Telegram 消息通道
Telegram 消息通道计划通过用户自行创建的 Bot,将 Telegram 私聊接入 GoodBuddy 通道项目,支持文字问答和执行任务。首版采用长轮询,运行 GoodBuddy 的电脑需要在线并能访问 Telegram。
Telegram 消息通道通过用户自行创建的 Bot,将授权用户的普通私聊文字接入 GoodBuddy 通道项目,复用项目的模型、Runtime 和工作目录。首版采用长轮询,运行 GoodBuddy 的电脑需要在线;生产驱动通过 Electron `net.fetch` 使用系统代理访问 Telegram。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [Telegram 通道 PRD](./prd.md) | 产品目标、首版范围、配置流程、功能要求、限制与验收标准 |
| [技术设计](./technical-design.md) | 生产模块、身份与持久化、IPC 状态、生命周期及实现边界 |
| [实施与验证进度](./progress.md) | 日期化验证证据、当前阻塞与待验收项目 |
| [API 联调操作说明](./api-testing.md) | 两类探针的命令、网络路径、输出含义及副作用 |
| [独立 Node 探针](./telegram-probe.mjs) | 独立调用 Bot API,不经过生产驱动或模型 |
| [生产探针启动器](./run-live-probe.mjs) | 构建探针、启动隔离 Electron 进程并清理临时目录 |
| [生产探针入口](./live-probe-main.ts) | 生产驱动连接检查及可选 ChannelService 固定回复,不调用模型 |
当前处于 PRD 草案阶段,尚未实现 Telegram 通道。详细逻辑、UI 和技术设计在实现前按需补充,不以本目录的建立作为功能完成证据。
生产接入代码与本地自动化验证已落地,真实 Bot 与模型的完整链路尚未证明,不能记为全面验收通过。当前结果和待办以[进度记录](./progress.md)为准;2026-10-04 独立 Node 探针成功仅是历史 API 证据。
## 术语与依赖
@@ -0,0 +1,109 @@
# Telegram API 联调
## 用途与边界
[run-live-probe.mjs](./run-live-probe.mjs) 启动 Electron,运行 [live-probe-main.ts](./live-probe-main.ts),检查生产驱动及可选的 ChannelService 固定回复。[telegram-probe.mjs](./telegram-probe.mjs) 是独立 Node API 诊断脚本,没有接入 GoodBuddy 项目、会话、白名单、Runtime 或发件箱。
两类探针都不调用模型,成功不能代替真实 Bot 与模型完整链路验收。生产驱动使用 Electron `net.fetch` 和系统代理,没有引入 grammY 或 Telegraf;独立脚本使用 Node `fetch`。当前结果和 2026-10-04 历史记录统一见[进度记录](./progress.md)。本次生产实时探针未运行,原因是 `TELEGRAM_BOT_TOKEN` 不存在。
## 生产驱动探针
从仓库根目录运行,需要已安装项目依赖(Electron、esbuild)。启动器在临时目录构建生产源码,使用隔离的 Electron 用户目录,退出后清理。可用 `--temp-parent PATH` 指定已存在的临时父目录。
```powershell
node "docs/features/telegram-channel/run-live-probe.mjs" --dry-run
```
`--dry-run` 只构建并检查语法,不启动 Electron,不发网络请求,不需要 Token。它不能证明代理路径可用。实时连接检查使用下面命令;隐藏输入仅放入进程环境,不写文件。
```powershell
$telegramProbeSecret = Read-Host 'Telegram Bot Token' -AsSecureString
$env:TELEGRAM_BOT_TOKEN = [System.Net.NetworkCredential]::new('', $telegramProbeSecret).Password
try {
node "docs/features/telegram-channel/run-live-probe.mjs"
} finally {
Remove-Item Env:TELEGRAM_BOT_TOKEN -ErrorAction SilentlyContinue
$telegramProbeSecret = $null
}
```
默认仅通过生产驱动执行 `getMe` / `getWebhookInfo`,不轮询、不发送。`TELEGRAM_BOT_TOKEN` 是探针输入,产品只读环境配置使用 `GOODBUDDY_TELEGRAM_BOT_TOKEN`,两者不能混用。
要验证固定回复,先停用该专用测试 Bot 的其他消费者,在普通私聊发送精确文本 `/goodbuddy_test`,再将上述运行命令替换为:
```powershell
node "docs/features/telegram-channel/run-live-probe.mjs" --send-test
```
发送模式从首批最多 100 条积压中选择最近 24 小时内最新的合格私聊标记,临时授权该发送者,经生产驱动和 ChannelService 返回固定文字。网络适配层只把选中的事件交给驱动,其他事件不会触发帮助或任务;使用临时内存状态和固定 executor,不经过产品设置页、生产模型或持久化发件箱。
发送模式会启动生产轮询、设置 `allowed_updates=['message']` 并可能推进 offset,因而可能确认或消费更早的积压。仅对专用空闲 Bot 使用;这与独立 Node 探针不推进 offset 的行为不同。无匹配标记时失败退出,不等待用户补发。
| 输出 | 含义 |
| --- | --- |
| `PASS: production driver getMe/getWebhookInfo.` | 生产驱动身份及 webhook 检查成功 |
| `PASS: production driver and ChannelService delivered the test reply.` | 固定回复 API 成功,仍需手机核对展示;未调用模型 |
| `FAIL: no eligible marker...` | 首批积压无合格标记,收发未验证 |
| 其他 `FAIL` 或非零退出 | 设置、构建、网络、停止或清理失败,不算通过 |
启动器只转发固定结果码,抑制可能包含 Token URL 的 Electron 输出,并强制 TLS 校验。成功退出要求对应检查完成且子进程正常退出。
## 独立 Node 探针
默认只调用 `getMe` 和 `getWebhookInfo`,不读取消息或发送回复。添加 `--listen` 后,在无 webhook 的情况下进行约 45 秒长轮询,仅对运行开始后收到的私聊 `/goodbuddy_test` 回复一条固定测试文字,然后退出。不处理其他消息、不调用模型、不下载附件。
脚本不推进更新 offset、不修改订阅类型、不删除 webhook。已读取的更新仍可能被后续消费者再次读取;请使用专用 Bot,避免与其他轮询进程同时运行。积压达到单批 100 条时停止测试,不主动确认或清空消息。固定回复发生网络结果不明时不自动重试。
### 运行方法
历史实测环境为 Windows、PowerShell 5.1、Node.js 24.21.0。以下命令从仓库根目录执行,Token 通过隐藏输入传入当前进程环境,不保存在文件中。不要把实际 Token 写进命令、文档或 Git。
```powershell
$telegramProbeSecret = Read-Host 'Telegram Bot Token' -AsSecureString
$env:TELEGRAM_BOT_TOKEN = [System.Net.NetworkCredential]::new('', $telegramProbeSecret).Password
try {
node --use-env-proxy "docs/features/telegram-channel/telegram-probe.mjs"
} finally {
Remove-Item Env:TELEGRAM_BOT_TOKEN -ErrorAction SilentlyContinue
$telegramProbeSecret = $null
}
```
`--use-env-proxy` 需要支持该选项的 Node 版本,脚本不会自动读取 Windows 系统代理。若机器依赖系统代理,先按下面方式将当前代理临时提供给 Node;这不会修改操作系统代理配置。结束后恢复原有环境值。
```powershell
$telegramProbePreviousProxy = $env:HTTPS_PROXY
$telegramProbeProxy = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy([Uri]'https://api.telegram.org')
if ($telegramProbeProxy.Host -ne 'api.telegram.org') {
$env:HTTPS_PROXY = $telegramProbeProxy.AbsoluteUri
}
# 在此执行上面的凭据输入和测试命令。
$env:HTTPS_PROXY = $telegramProbePreviousProxy
```
测试真实收发时,将运行命令改为:
```powershell
node --use-env-proxy "docs/features/telegram-channel/telegram-probe.mjs" --listen
```
看到提示后,在 Telegram 打开测试 Bot 的普通私聊,发送 `/goodbuddy_test`。脚本成功发送固定回复后输出 `privateMessageRoundTrip` 的 `ok: true`。用户还需在手机端确认实际收到回复。
如果已发送测试指令,使用 `--pending` 替代 `--listen`。脚本单次查询排队消息,选择最近一小时内最新的私聊 `/goodbuddy_test` 并回复,无需在监听窗口内发送。该选项同样不推进 offset,重复运行可能再次回复同一条指令,仅在需要复测时使用。
脚本强制开启 TLS 证书校验,不沿用外部环境中的关闭校验设置。输出仅包含检查结果、Bot 用户名、是否配置 webhook、积压数量和有界错误码;不打印 Token、完整请求地址、原始异常、消息正文或聊天 ID。
### 结果解释
| 输出或退出码 | 含义 |
| --- | --- |
| 退出码 0,默认检查 | Bot 身份及 webhook 状态查询成功;未验证收发 |
| 退出码 0,`privateMessageRoundTrip` 成功 | 收到测试指令,发送回复 API 返回成功 |
| 退出码 2,`no_matching_message` | 时间窗口内未收到符合条件的测试指令,收发待验证 |
| 退出码 1 | 网络、凭据、平台错误或测试条件不满足 |
| `UND_ERR_CONNECT_TIMEOUT` | 网络连接超时,优先检查当前进程的代理路径 |
| API error 401 | 凭据无效或已撤销 |
| API error 409 | 检查 webhook 或其他长轮询实例是否占用 |
| API error 429 | 平台限流;脚本报告等待时间,不自动继续请求 |
日期化实测结果见[进度记录](./progress.md)。独立 Node 请求走通不能证明 Electron 生产网络路径走通;固定回复成功也不能证明模型或工具任务成功。
@@ -0,0 +1,134 @@
import { app, net } from 'electron'
import { ChannelService } from '../../../src/main/channels/channel-service'
import { TelegramChannelDriver } from '../../../src/main/channels/telegram-channel-driver'
type Update = {
update_id: number
message?: {
date: number
text?: string
from?: { id: number; is_bot: boolean }
chat: { id: number; type: string }
message_thread_id?: number
is_topic_message?: boolean
}
}
function report(code: string): void {
process.send?.(code)
}
async function bounded<T>(operation: Promise<T>, ms: number): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined
try {
return await Promise.race([
operation,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new Error('Probe timeout')), ms)
})
])
} finally {
clearTimeout(timer)
}
}
async function main(): Promise<void> {
let driver: TelegramChannelDriver | undefined
let service: ChannelService | undefined
let exitCode = 1
try {
const userData = process.env.GOODBUDDY_PROBE_USER_DATA
const secret = process.env.TELEGRAM_BOT_TOKEN
if (!userData || !secret || !process.send) throw new Error('Runner required')
app.setPath('userData', userData)
app.setPath('sessionData', userData)
app.setPath('logs', userData)
// Keep Chromium caches and diagnostics inside the disposable profile.
app.commandLine.appendSwitch('disable-logging')
app.commandLine.appendSwitch('disable-http-cache')
await bounded(app.whenReady(), 15_000)
let selected: Update | undefined = undefined
const electronFetch: typeof globalThis.fetch = async (input, init) => {
const url = input instanceof Request ? input.url : String(input)
const response = await net.fetch(url, init)
if (!selected || !url.endsWith('/getUpdates') || !response.ok) return response
const data = await response.json() as { ok?: boolean; result?: Update[] }
if (data.ok !== true || !Array.isArray(data.result)) throw new Error('Invalid updates')
// This is the only network adaptation. Receiving, parsing, queuing and sending
// still run through production code; other queued commands cannot send help.
return new Response(JSON.stringify({
ok: true,
result: data.result.filter((update) =>
update.update_id === selected?.update_id &&
update.message?.text === '/goodbuddy_test' &&
update.message?.from?.id === selected?.message?.from?.id &&
update.message?.chat.id === selected?.message?.chat.id)
}), { headers: { 'content-type': 'application/json' } })
}
driver = new TelegramChannelDriver({ secret, allowedSenderIds: [], fetch: electronFetch })
await bounded(driver.testConnection(), 35_000)
report('connection')
if (!process.argv.includes('--send-test')) {
exitCode = 0
return
}
// No offset: discovery does not acknowledge the queue or alter allowed_updates.
const response = await net.fetch(`https://api.telegram.org/bot${secret}/getUpdates`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ timeout: 0, limit: 100 }), redirect: 'error',
signal: AbortSignal.timeout(15_000)
})
const data = await response.json() as { ok?: boolean; result?: Update[] }
if (!response.ok || data.ok !== true || !Array.isArray(data.result)) throw new Error('Discovery failed')
const now = Math.floor(Date.now() / 1000)
selected = data.result.filter((update) => {
const message = update.message
return Number.isSafeInteger(update.update_id) && update.update_id >= 0 &&
message?.text === '/goodbuddy_test' && message.chat?.type === 'private' &&
Number.isSafeInteger(message.from?.id) && message.from!.id > 0 &&
message.from?.is_bot === false && message.chat.id === message.from.id &&
message.message_thread_id === undefined && !message.is_topic_message &&
Number.isSafeInteger(message.date) && message.date >= now - 86_400 && message.date <= now
}).sort((a, b) => b.message!.date - a.message!.date || b.update_id - a.update_id)[0]
if (!selected) {
report('missing')
return
}
report('selected')
const allowedSenderIds = [String(selected.message!.from!.id)]
driver.updateAllowedSenderIds(allowedSenderIds)
let delivered!: () => void
const delivery = new Promise<void>((resolve) => { delivered = resolve })
service = new ChannelService(driver, async (message) => {
if (message.eventId !== String(selected!.update_id) || message.text !== '/goodbuddy_test') {
throw new Error('Unexpected event')
}
return {
status: 'completed',
output: 'GoodBuddy Telegram channel test passed using the production driver and ChannelService. No AI model or tools were used.'
}
}, { allowedSenderIds, maximumConcurrency: 1, onDeliverySuccess: delivered })
await service.start()
await bounded(delivery, 45_000)
report('delivered')
exitCode = 0
} catch {
report('failed')
} finally {
try {
await bounded(Promise.all([service?.stop(), driver?.stop()]), 5_000)
} catch {
report('shutdown')
exitCode = 1
}
process.exitCode = exitCode
app.quit()
setTimeout(() => app.exit(exitCode), 1_000).unref()
}
}
process.on('uncaughtException', () => { report('failed'); app.exit(1) })
process.on('unhandledRejection', () => { report('failed'); app.exit(1) })
void main()
+28 -21
View File
@@ -4,20 +4,20 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 草案,待评审;Telegram 通道尚未实现 |
| 版本 | 0.1 |
| 日期 | 2026-10-04 |
| 状态 | 已按生产实现修订;真实 Bot 与模型完整链路待验收 |
| 版本 | 0.2 |
| 日期 | 2026-10-05 |
| 适用产品 | GoodBuddy 桌面端 |
| 首版范围 | 单 Bot、普通私聊、文字问答与工具任务、最终结果回复 |
| 接入方式 | 用户通过 BotFather 创建 Bot,GoodBuddy 使用 Bot API 长轮询 |
本文定义拟交付的产品行为。验收表描述未来交付标准,不代表已经通过测试。BotFather 接入方向来自本次讨论;范围取舍和异常行为是本草案提出的方案。
本文定义首版产品行为和验收标准,已按当前实现修订。验收表不代表已经通过测试;验证结果及剩余工作由[进度记录](./progress.md)维护,模块与协议细节见[技术设计](./technical-design.md)。
## 1. 用户问题与目标
用户离开电脑后,希望通过 Telegram 向 GoodBuddy 提问或发起任务,并在手机上收到结果。任务应使用桌面端已经配置的模型、Runtime 和工作目录,执行过程与对话历史仍可在 GoodBuddy 查看。
当前远程消息通道已有微信 ClawBot、企业微信和钉钉,尚无 Telegram 入口。新增通道应沿用现有“通道项目 + 远程会话”结构,让用户通过创建 Bot、填写凭据和配置允许访问的用户完成接入,无需自行部署消息中转服务。
Telegram 沿用现有“通道项目 + 远程会话”结构,与微信 ClawBot、企业微信和钉钉并列。用户通过创建 Bot、填写凭据和配置允许访问的用户完成接入,无需自行部署消息中转服务。
首版完成以下使用流程:
@@ -35,7 +35,7 @@
| Bot 数量 | 每个 GoodBuddy 用户配置一个 Telegram Bot |
| 会话类型 | 普通私聊;不同用户的会话相互隔离 |
| 输入 | 文字消息和受支持的命令 |
| 执行 | 复用渠道项目的处理后端和统一执行规则,无产品模式 |
| 执行 | 复用渠道项目的处理后端、已启用能力和统一执行规则 |
| 输出 | 任务进行提示、最终文字结果、长回复分段 |
| 管理 | Token 保存和替换、用户 ID 白名单、连接测试、启停和状态展示 |
| 故障处理 | 临时断网重连、平台限流等待、已保存结果的有限重试 |
@@ -63,7 +63,7 @@
1. 用户在 GoodBuddy 远程通道设置中打开 Telegram 卡片,点击官方 BotFather 链接。
2. 用户在 BotFather 发送 `/newbot`,设置显示名称和符合 Telegram 要求的用户名,获得 Token。
3. 用户回到 GoodBuddy 填写 Token,执行“测试连接”。成功后显示 Bot 名称、用户名和打开 Bot 的链接。
3. 用户回到 GoodBuddy 填写 Token,执行“测试连接”。成功后显示 API 返回的 Bot 用户名和打开 Bot 的链接,不展示 Bot 显示名称。
4. 用户先保存凭据并以空白名单启用连接,卡片显示“尚未授权用户”。此时仅提供私聊帮助,不接受业务请求。
5. 用户在 Telegram 打开该 Bot,发送 `/start`,再发送 `/whoami` 获取自己的数字用户 ID。卡片说明必须由用户先发起私聊。
6. 用户将该 ID 填入 GoodBuddy 白名单并保存,随后发送普通文本,收到回复,并在 Telegram 项目中看到对应会话。
@@ -82,9 +82,9 @@
### FR-2 凭据管理
Token 使用现有加密凭据存储,保存后不在普通设置快照中返回明文。用户可以保留、替换或清除凭据。日志、连接错误和会话内容不得包含 Token 或含 Token 的完整请求地址。
Token 使用现有加密凭据存储,保存后不在普通设置快照中返回明文。用户可以保留、替换或清除凭据;环境变量覆盖时配置只读。日志、连接错误和会话内容不得包含 Token 或含 Token 的完整请求地址。
替换 Token 前验证 Bot 身份。若仍为同一个 Bot,继续使用原会话;若为另一个 Bot,新会话与旧 Bot 隔离,旧 Bot 的待发结果不得经新 Bot 发送。清除凭据停止连接,保留项目与历史。
替换 Token 前验证 Bot 身份,验证失败保留原配置与连接。若仍为同一个 Bot,继续使用原会话;若为另一个 Bot,新会话与旧 Bot 隔离,旧 Bot 的待发结果不得经新 Bot 发送。在设置页清除凭据并保存时同时停用通道,保留项目与历史。
### FR-3 连接测试
@@ -102,14 +102,15 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
### FR-5 会话与连续上下文
同一 Bot 下同一普通私聊复用远程会话,不同用户和不同 Bot 的上下文隔离。会话显示来源和发送者名称;名称不可用时使用用户 ID 作为可识别的回退信息。名称变化不产生新身份。
同一 Bot 下同一普通私聊复用远程会话,不同用户和不同 Bot 的上下文隔离。会话归属 Telegram 项目,发送者以数字用户 ID 标识;首版不采集发送者显示名称。
同一会话按接收顺序处理业务消息。用户连续发送消息时,不得使后发请求抢用尚未完成的上一轮上下文。不同会话沿用现有通道并发限制。
### FR-6 执行范围
普通文本使用项目后端与能力配置;`/ask`、`/execute` 和 `/exec` 沿用现有渠道的退役规则,
作为普通正文保留,不切换权限。仅含旧命令的非空消息也按普通消息处理。
授权用户直接发送自然语言请求,使用项目后端与能力配置完成问答或工具任务,无需选择或切换工作模式。通道不提供模式选择器、默认模式设置或模式切换命令。
`/ask`、`/execute` 和 `/exec` 按现有统一执行规则作为普通正文保留,不剥离前缀、不切换权限;仅含旧命令的非空消息也按普通消息处理。帮助文案不再介绍这些前缀。
按[统一执行规则](../unified-execution/logic-design.md)使用已启用工具,不增加 Telegram 专属
逐工具审批。设置页显示默认工作目录和后端,并说明允许访问的用户可发起操作。
@@ -119,7 +120,7 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
`/start` 返回 Bot 用途、授权配置说明和基础用法;`/whoami` 仅返回当前发送者的数字用户 ID。上述命令不写入业务上下文。
授权用户发送未知斜杠命令时,返回支持的命令列表,不将其当成执行请求。编辑历史消息不自动重新执行任务。
除 `/start` 和 `/whoami` 外,授权用户发送的非空文字均按普通请求处理,包括带斜杠前缀的文字,不额外引入命令兼容规则。空白消息按现有输入规则拒绝。编辑历史消息不自动重新执行任务。
授权用户发送媒体时,提示“当前仅支持文字消息,请改用文字描述”。带说明文字的媒体首版同样提示不支持,不丢弃媒体后擅自执行说明文字。未授权媒体不触发处理。
@@ -127,7 +128,7 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
请求进入处理后,Telegram 侧显示输入状态或简短进行提示。该提示不表示任务完成,也不表示任务可以在应用崩溃后恢复。
任务完成后发送最终文字;失败时发送脱敏原因及必要的恢复建议。回复尽量关联原始消息;原消息不可引用时仍应尝试发送结果。长文本按平台上限顺序分段,不破坏 Unicode 字符,不因 Markdown 转义问题造成整条回复失败。
任务完成后发送最终文字;失败时发送脱敏原因及必要的恢复建议。回复作为普通私聊消息发送,首版不引用原始消息。长文本以纯文本顺序分段,保留 Unicode 代理对,不启用 Markdown 解析。
首版不发送附件。若任务产生图片或文件,文字回复说明成果可在 GoodBuddy 中查看,不宣称附件已发送,也不向用户发送仅本机可访问的路径作为下载链接。
@@ -137,7 +138,9 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
已生成并保存的结果沿用现有发件箱有限重试,发送失败与任务执行失败分别记录。补发回复不重新调用模型或重跑任务。用户阻止 Bot 等不可恢复错误应停止无效重试。
多段回复部分成功后,应尽量从未发送部分继续。网络中断导致无法确定平台是否收到某段时,允许重试出现重复段落,首版不承诺平台级发送幂等。
已有 webhook 或另一实例占用轮询时,已确认 Bot 身份且目标匹配的待发回复仍可发送;接收冲突本身不能永久终止这些回复。发送内部重试达到上限后,发件箱的下一次尝试仍须遵守最近一次 429 响应的等待时间,停用可取消等待。
同一驱动实例内且分段进度仍在缓存时,多段回复从未发送部分继续。进度不跨重启持久化,重启或缓存淘汰后补发可能重复已发送段落;网络中断造成平台接收结果不明时也可能重复。首版不承诺平台级发送幂等。
### FR-10 启停与历史保留
@@ -157,7 +160,11 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
### FR-12 状态与诊断
设置页区分未配置、已停用、正在连接、已连接、正在重连和连接失败。空白名单作为“尚未授权用户”的配置提示展示,不与网络状态混为一谈。
设置页展示未配置、已停用、正在连接、已连接、已停止和连接失败。重连时显示正在连接,并附带错误原因与重连说明;没有独立的重连状态值。空白名单作为“尚未授权用户”的配置提示展示,不与网络状态混为一谈。
状态和错误推送只更新状态区域,不覆盖尚未保存的 Token 或白名单草稿。解决轮询冲突后可重新保存当前配置以启动连接;单独测试连接成功不改变接收状态。
接收正常而结果发送失败时,保留已连接状态并显示脱敏的发送错误。后续健康轮询不清除发送提示,投递成功后清除;接收也发生错误时优先展示接收原因,恢复后保留尚未清除的发送提示。
状态通过文字表达,不能只靠颜色。错误保留可操作原因,如 Token 无效、网络不可达、webhook 占用或另一实例正在轮询。测试成功、发送失败和模型失败使用各自准确文案,避免统一显示“连接失败”。
@@ -174,15 +181,15 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
| AC-03 | FR-2、3 | 通道运行时连续测试连接,不争抢轮询、不消费业务消息;无效 Token 与 webhook 占用分别提示 |
| AC-04 | FR-5 | 同一用户连续两轮对话保留上下文;两名授权用户的内容不相互进入上下文 |
| AC-05 | FR-6 | 普通请求可在专用测试目录完成一次无害工具任务;旧模式前缀作为正文保留,结果和活动可在桌面核对,无默认模式字段 |
| AC-06 | FR-4、7 | 未授权用户、群消息、话题消息和媒体不触发任务;授权媒体、未知命令和空命令获得准确提示 |
| AC-06 | FR-4、7 | 未授权用户、群消息、话题消息和媒体不触发任务;授权媒体获得不支持提示;内置帮助命令不调用模型,其他斜杠前缀按正文保留,空白消息被拒绝 |
| AC-07 | FR-8 | 超过单条平台上限的回复按序完整展示,中文和表情字符完整;任务失败返回脱敏原因 |
| AC-08 | FR-8 | 生成文件的任务在 Telegram 中提示桌面查看成果,不出现“附件已发送”的错误表述 |
| AC-09 | FR-9、12 | 模拟短暂断网、429 和永久发送错误,分别重连、等待和停止无效重试;任务执行结果不被误记为执行失败 |
| AC-10 | FR-9 | 已保存结果发送失败后恢复连接可补发,补发不增加模型调用或再次执行任务;验证部分分段成功情形 |
| AC-11 | FR-10 | 停用通道后新消息不启动任务,当前与排队任务取消;历史与成果保留;重新启用不重跑取消任务 |
| AC-11 | FR-2、10 | 停用通道后新消息不启动任务,当前与排队任务取消;启用时清除 Token 并保存会同时停用;项目、历史与成果保留;重新启用不重跑取消任务 |
| AC-12 | FR-2、5 | 同一 Bot 换 Token 保留会话;更换 Bot 后上下文隔离,旧待发结果不会由新 Bot 发出 |
| AC-13 | FR-4、11 | 重投同一更新不重复执行;离线后恢复可接收平台保留消息;已撤销用户的积压消息不执行 |
| AC-14 | FR-2、12 | 设置快照、日志及错误不泄漏 Token;凭据轮换、网络错误和 Runtime 错误均可定位 |
| AC-14 | FR-2、12 | 设置快照、日志及错误不泄漏 Token;凭据轮换、网络错误和 Runtime 错误均可定位;连接错误及重连状态推送保留未保存的 Token 和白名单草稿 |
| AC-15 | FR-1、10 | 新增 Telegram 后微信、企业微信和钉钉的设置、启停与历史读取保持原行为 |
| AC-16 | FR-9、10 | 有已保存回复等待重试时停用通道,确认重试停止;重新启用后补发该 Bot 的结果,任务执行次数不增加 |
| AC-17 | FR-11 | 在专用测试任务执行期间异常退出应用,重启后不自动重跑该中断任务;已有任务记录可查询,不把未取得的完成结果显示为成功;帮助说明保留人工核对后重发的限制 |
@@ -193,9 +200,9 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
通道项目、工作目录、Runtime 选择和授权语义复用[现有远程消息通道约定](../wechat-channel/prd.md)。Telegram 的外部网络可达性、Bot 状态和平台限额影响收发,连接测试无法证明后续每次任务或文件生成都能成功。
开发前需核对主进程网络与代理路径,以及现有通道停用、去重和发件箱行为是否满足本文要求。差异应在对应技术设计中解决并关联 FR 编号,不能只增加设置入口后宣称完成。本文不规定新增 SDK、数据库表或通用恢复框架。
生产驱动使用 Electron 网络栈和系统代理。停用、去重、发件箱与状态推送的实现见[技术设计](./technical-design.md),实际覆盖与未完成验证见[进度记录](./progress.md)。没有测量数据时,不宣称性能提升或性能预算通过。
前期讨论提出过“持久化入站后再确认”的方案。本草案将其列为后续可靠性评估,首版采用 FR-11 的明确限制;若后续要求已接收任务跨崩溃恢复,再依据实际故障与业务需求评估存储和执行恢复成本。
持久化入站后再确认仍属于后续可靠性评估,首版采用 FR-11 的限制;若后续要求已接收任务跨崩溃恢复,再依据实际故障与业务需求评估存储和执行恢复成本。
| 后续方向 | 进入设计前需明确的内容 |
| --- | --- |
+114
View File
@@ -0,0 +1,114 @@
# Telegram 实施与验证进度
## 2026-10-05 当前结果
生产设置入口、驱动、项目路由、执行、持久化和状态推送已接入,行为见[技术设计](./technical-design.md)。真实 Telegram Bot 与模型的完整链路尚未证明,[PRD 验收](./prd.md#5-产品验收)仍待完成。
以下计数来自本次开发会话提供的验证结果,本轮文档修订未重跑这些测试。集合可能重叠,不相加计算覆盖总数。
| 验证范围 | 结果 | 证据边界 |
| --- | --- | --- |
| Driver / Service / Manager 聚焦测试 | 74 通过 | 解析、授权、重试、身份隔离与生命周期等自动化覆盖 |
| 集成测试集合 | 251 通过,1 跳过 | 包含 Electron 生产 UI / IPC / store / executor 与本地模型 fixture,Telegram 网络响应为替身 |
| Electron 场景 | 2 次本地模型调用,0 次外部调用 | 配置测试与加密保存、授权和去重、结果入库与回复、重载恢复、停用取消;错误及重连状态推送保留草稿;启用状态清除 Token 后停止连接并保留项目和历史 |
| Typecheck / lint | 最终复跑均通过 | 静态检查,不能替代真实 Bot 联调 |
| 全量测试原始结果 | 6086 通过,86 跳过,2 失败 | 保留当次失败记录;后续修复与复跑见下两行,尚无新的全量通过结论 |
| Schema 断言修复 | 已改用 schema 版本常量;四个 suite 共 301 通过 | 修复原先预期 60、实际 61 的 worker 断言,不将局部复跑计为全量通过 |
| OpenCode cleanup-timeout 复跑 | 单独运行 3 次通过,整个 runtime suite 通过 | 原因尚未证明,记录为全量运行中出现的间歇性失败,不宣称已修复 |
| 最终全量测试 | 6087 通过,87 跳过,1 失败;耗时 879.68 秒 | 唯一失败为 `tests/inline-help.electron.test.ts` 的 Hover Escape 场景;原样单独复跑通过,仍保留间歇性失败记录。此前 schema 和 Runtime 失败未在本轮出现 |
| 生产实时探针 | 本次未运行 | 当前会话缺少 `TELEGRAM_BOT_TOKEN` |
| 生产探针 dry-run | 打包与语法检查通过 | 未启动 Electron,未发出网络请求 |
| 真实本地代理 fixture | 真实 Electron 生产默认传输曾成功运行 1 次;最终运行因临时 CA 信任导入超时失败 | 后续 Windows 临时 CA 信任导入需要操作系统确认;测试已改为显式启用,默认跳过。不宣称可无人值守复现 |
| Main 性能测量 | 基线与当前版本交替各运行 3 轮 | 结果见下节;没有证据证明性能改善或 Telegram 引入回退,不宣称全局预算通过 |
Electron 证据对应 [`tests/telegram-channel.electron.test.ts`](../../../tests/telegram-channel.electron.test.ts) 与 [`tests/support/telegram-main.ts`](../../../tests/support/telegram-main.ts)。它使用生产组件、Preload、IPC、配置存储、executor 和 SQLite;在 `net.fetch` 边界替换 Telegram 响应,模型为本地 HTTP fixture。因此它验证内部产品路径,不能证明外部 Telegram 网络、系统代理或真实模型服务可用。
可复跑的聚焦命令:
```powershell
npx vitest run src/main/channels/telegram-channel-driver.test.ts src/main/channels/channel-service.test.ts src/main/channels/channel-manager.test.ts
npx vitest run tests/telegram-channel.electron.test.ts
npm run typecheck
npm run lint
npm test
```
集成集合及 301 项复跑的完整命令未随计数提供,这里不推测其文件清单。真实本地代理 fixture 见 [`tests/telegram-proxy.electron.test.ts`](../../../tests/telegram-proxy.electron.test.ts);其本地代理结果不证明外部 Telegram Bot 可用。探针操作见 [API 联调](./api-testing.md),探针成功也不包含模型验收。
## 2026-10-05 通道审计修复验证
A06 的图片结果回归使用现有 IPC executor 图片事件 fixture,接入生产 `ChannelService` 和内存 SQLite 发件箱。修复前回复状态为 `failed`;修复后状态为 `completed`,图片附件字节保持一致,桌面会话仍记录成果 ID。未调用付费图片模型。
A07 的 Renderer 回归覆盖推送状态为 `error`、`stopped`、`running`、`starting`、`disabled` 的无修改保存。前两种状态在修复前没有提交配置,修复后能够提交;其余状态保持不提交。扩展的 Electron 场景通过真实设置 UI、Preload、IPC、Manager 和驱动验证 409 后无修改保存恢复轮询,以及正常连接时再次保存不重启。Telegram API 在网络边界使用替身;本次运行有 2 次本地 HTTP 模型调用、0 次外部请求。
A08 的钉钉回归使用生产传输、服务和 SQLite 发件箱,HTTP 请求只发送到 loopback fixture。首次 HTTP 200 携带非零 `errcode`,修复前被当作成功;修复后记录一次失败并保留上下文,第二次成功响应完成投递,executor 只执行一次。另有空响应、缺少错误码、JSON null、无效 JSON 和超限响应回归。
本轮聚焦验证:6 个 channel / Renderer suite 共 106 项通过;IPC 图片、文件与 Telegram 场景 3 项通过;Electron 场景 1 项通过。修复前选择性回归为 9 项失败、3 项通过。命令如下:
```powershell
npm test -- src/main/channels/dingtalk-channel-driver.test.ts src/renderer/src/ChannelSettingsSection.test.tsx src/main/channels/channel-manager.test.ts src/main/channels/channel-service.test.ts src/main/channels/telegram-channel-driver.test.ts src/main/channels/sqlite-channel-state.test.ts
npm test -- src/main/ipc.test.ts -t "returns a generated image only|creates a bounded result file|routes Telegram settings"
npm test -- tests/telegram-channel.electron.test.ts
```
这些结果验证本地生产连接关系,不替代真实平台收发或图片模型验收。本轮未改 Agent、Runtime 协议或执行策略,未做性能改善声明。
补充检查:钉钉错误码断言加入后,单独复跑 9 项通过;修改的 TS 文件 ESLint、Renderer typecheck 和 `git diff --check` 通过。早期全仓检查受到并行修改中的类型与 lint 错误阻塞,负责方修正后最终检查均通过;完整测试为 6,136 项通过、0 失败、87 跳过。汇总见[阶段修复验收](../../review/stage-audit-fixes-2026-10-05.md#最终检查)。
## 2026-10-05 K09 投递修复
本轮只修改 Telegram 驱动、Manager 及其测试,复用现有服务重试、SQLite 发件箱和状态 IPC。未改数据库结构、Main IPC、Runtime 或 Agent;本地与远程任务完成后的通道结果均使用同一发送路径,无需独立 Agent 修复。
修复前先运行持久回归:`telegram-delivery.test.ts` 的 3 项 loopback HTTP/SQLite 用例和 Manager 的状态用例共 4 项失败。启动 webhook 冲突时,同 Bot 的待发回复未发出且变为终止记录;403 后状态仍为 `running` 且无错误;连续 429 的最后响应要求等待 3 秒,下一次发送却只间隔约 1.010 秒。扩展的 Electron 设置 fixture 也在“已连接时显示发送失败”断言处失败。
修复后,启动阶段保留 `getMe` 确认的身份,已有 webhook 时发送同 Bot 的待发回复,其他 Bot 的记录仍被拒绝,轮询状态仍报告冲突。发送错误经脱敏后由现有状态区域显示,健康轮询不清除;发送成功清除投递提示但不覆盖轮询错误。驱动在内部重试耗尽后仍完成最后一次 `retry_after` 等待,再返回发件箱;新增取消用例验证等待可中断、记录保持 `pending / attempts=0`,重新启用后只补发一次且不运行 executor。
| 检查 | 最终结果 |
| --- | --- |
| 9 个通道/Renderer 聚焦文件 | 127 项通过,包含 4 项 loopback HTTP/SQLite 回归 |
| 现有 Electron 设置 fixture | 1 项通过;生产设置 UI、Preload、IPC、Manager、驱动与 SQLite,403 提示、已连接状态、Token/白名单草稿、重载恢复均通过 |
| 修改的 6 个 TS 文件 ESLint | 通过 |
| `npm run typecheck` | Main、Agent 通过;Renderer 被并行工作区的 `App.tsx:174` 未使用 `refreshArtifacts` 导入阻塞,未修改该文件 |
最终聚焦命令:
```powershell
npm exec -- vitest run src/main/channels/telegram-delivery.test.ts src/main/channels/telegram-channel-driver.test.ts src/main/channels/channel-manager.test.ts src/main/channels/channel-service.test.ts src/main/channels/channel-settings-store.test.ts src/main/channels/sqlite-channel-state.test.ts src/renderer/src/ChannelSettingsSection.test.tsx src/renderer/src/channel-status-store.test.ts src/main/channels/dingtalk-channel-driver.test.ts
npm exec -- vitest run tests/telegram-channel.electron.test.ts
npm exec -- eslint src/main/channels/telegram-channel-driver.ts src/main/channels/channel-manager.ts src/main/channels/telegram-delivery.test.ts src/main/channels/telegram-channel-driver.test.ts src/main/channels/channel-manager.test.ts tests/support/telegram-main.ts
npm run typecheck
```
Electron 最终运行使用 2 次本地 HTTP 模型响应;连同修复前失败运行和中间验证,本轮共 6 次本地 fixture 请求、0 次外部 Provider 请求、0 次真实 Telegram 收件人消息。未读取 `.env`,未运行生产构建。完整测试、全仓 lint 及合并后的类型检查由协调方执行;本节不声明全仓验收通过或性能提升。
## Main 性能对比
基线为 `78afec77758f3c175f8e2b80e5be4327e13b4689` 的临时 detached worktree,当前为包含 Telegram 改动的工作区。按基线、当前交替执行 `npm run perf:main` 各 3 次。环境为 Windows x64、Intel Core Ultra X7 358H、Electron 44.5.1、Node 24.21.0;每轮使用隔离数据库,包含 1000 个会话、20008 条生成消息、24000 条活动记录,每个场景预热后测量 15 次,无外部模型调用。
下表为每轮调用耗时 p95,单位为毫秒。对比相同生产路径,不将脚本内部的历史同步算法视为基线版本。
| 场景 | 基线三轮 | 当前三轮 |
| --- | --- | --- |
| 会话摘要 worker | 25.0 / 20.9 / 29.3 | 18.8 / 26.6 / 53.3 |
| 最大会话 worker | 1.1 / 0.6 / 1.1 | 0.8 / 1.0 / 1.2 |
| 活动增量更新 | 1.3 / 2.3 / 2.0 | 1.3 / 1.5 / 1.7 |
| 活动启动 worker | 17.7 / 18.7 / 29.2 | 13.7 / 23.0 / 25.2 |
| 整份活动替换,一条变化 | 61.8 / 84.9 / 73.1 | 52.0 / 84.0 / 73.8 |
worker 读取、活动启动、增量更新和同步首页查询在六轮中均满足 Main 阻塞与事件循环延迟预算;当前这些路径的最大循环间隔为 3.7 ms,最高循环延迟 p99 为 4.4 ms。整份活动替换在两个版本中均超预算,当前最大循环间隔为 47.0–84.2 ms;同步活动汇总也存在超过 16 ms 的情况。这些已有路径不属于本次 Telegram 优化范围。
版本差异与轮次波动交叠,不能据此归因于 Telegram 或声称性能提升。测量在新数据库初始化后进行,不覆盖已填充 schema 60 数据库升级到 61 的耗时、Telegram 流量或完整 UI 响应。原始结果保存在本次机器临时目录 `telegram-main-results-20261005-a`,不作为仓库依赖。
## 2026-10-04 历史 API 证据
独立 `telegram-probe.mjs` 在 Windows / PowerShell 5.1 / Node.js 24.21.0 下测试。首次直连曾有关闭 TLS 校验的环境配置,不作为正常 HTTPS 证据;开启校验后直连超时。将 Windows 系统代理临时传给 Node 后,`getMe`、`getWebhookInfo` 成功,无 webhook。
初次监听窗口未收到匹配指令;随后 `--pending` 从 2 条积压中匹配 `/goodbuddy_test`,`sendMessage` 返回成功,输出 `privateMessageRoundTrip: ok=true`。没有模型调用,手机端实际展示仍需用户确认。该记录只证明当日独立 Node API 路径,不证明当前凭据有效、生产驱动可用或完整产品验收通过。历史凭据曾公开,应撤销后换新,文档不保留 Token。
## 待完成
- 在可完成操作系统信任确认的环境中复跑显式启用的代理测试,记录最终结果;保留单次成功和后续导入超时两项证据,不把默认跳过视为通过。
- Runtime 清理用例的 GC 生命周期问题已定位并修正测试夹具,完整集成测试通过,见[GC 复查](../unified-execution/progress.md#sdk-cleanup-timeout-复查)。历史帮助弹层间歇性失败在本轮完整测试中未出现。
- 提供有效测试凭据后运行生产实时探针,记录连接及固定回复结果;本次缺少凭据的阻塞不以历史 Node 成功替代。
- 从真实设置页保存并启用专用 Bot,在手机发送授权请求,经过实际模型产生回复并核对桌面历史;再按 PRD 验证连续上下文、隔离、无害工具任务和恢复场景。当前没有真实 Bot 与模型完整链路的通过记录。
- Main 测量未覆盖迁移和完整 UI;后续验收需按[性能原则](../../architecture/performance-principles.md)区分实测路径与未测路径。
@@ -0,0 +1,102 @@
/* global process, console, setTimeout, clearTimeout, URL */
import { spawn, spawnSync } from 'node:child_process'
import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { build } from 'esbuild'
const require = createRequire(import.meta.url)
const messages = {
connection: 'PASS: production driver getMe/getWebhookInfo.',
selected: 'Selected a queued private test marker from the last 24 hours.',
delivered: 'PASS: production driver and ChannelService delivered the test reply.',
missing: 'FAIL: no eligible marker in the pending batch (maximum 100 updates).',
failed: 'FAIL: live probe failed; network details suppressed.',
shutdown: 'FAIL: probe shutdown did not complete.'
}
async function main() {
const args = process.argv.slice(2)
let tempParent = tmpdir()
for (let i = 0; i < args.length; i += 1) {
if (args[i] === '--temp-parent' && args[i + 1]) tempParent = args[++i]
else if (!['--dry-run', '--send-test'].includes(args[i])) {
console.error('Usage: node run-live-probe.mjs [--dry-run | --send-test] [--temp-parent PATH]')
process.exitCode = 1
return
}
}
const dry = args.includes('--dry-run')
const send = args.includes('--send-test')
if (dry && send) throw new Error('Invalid mode')
if (!dry && !/^\d+:[A-Za-z0-9_-]+$/.test(process.env.TELEGRAM_BOT_TOKEN ?? '')) {
console.error('Set TELEGRAM_BOT_TOKEN in the environment before running live.')
process.exitCode = 1
return
}
if (!(await stat(tempParent)).isDirectory()) throw new Error('Invalid temporary parent')
const directory = await mkdtemp(join(tempParent, 'goodbuddy-telegram-live-'))
try {
const outfile = join(directory, 'probe.cjs')
await build({
entryPoints: [fileURLToPath(new URL('./live-probe-main.ts', import.meta.url))],
outfile, bundle: true, platform: 'node', format: 'cjs', target: 'node22',
external: ['electron'], logLevel: 'silent'
})
const check = spawnSync(process.execPath, ['--check', outfile], {
stdio: 'ignore', timeout: 15_000
})
if (check.status !== 0) throw new Error('Bundle syntax check failed')
const electron = require('electron')
if (typeof electron !== 'string') throw new Error('Electron executable unavailable')
if (dry) {
console.log('PASS: bundled production sources and checked syntax; Electron not launched, no network calls.')
return
}
if (send) {
console.log('Send mode: use a dedicated idle bot. Polling may consume/acknowledge earlier queued updates.')
console.log('Only the selected exact /goodbuddy_test event receives a fixed reply; no model or tools run.')
}
const userData = join(directory, 'user-data')
await mkdir(userData)
const env = { ...process.env, GOODBUDDY_PROBE_USER_DATA: userData }
delete env.ELECTRON_RUN_AS_NODE
delete env.ELECTRON_ENABLE_LOGGING
delete env.ELECTRON_LOG_FILE
delete env.NODE_OPTIONS
env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
process.exitCode = await new Promise((resolve) => {
// Never forward Electron/Chromium stderr: it may contain credential-bearing URLs.
const child = spawn(electron, [outfile, ...(send ? ['--send-test'] : [])], {
env, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], windowsHide: true
})
let passed = false
const timer = setTimeout(() => child.kill('SIGKILL'), 125_000)
const interrupt = () => child.kill('SIGKILL')
process.once('SIGINT', interrupt)
process.once('SIGTERM', interrupt)
child.on('message', (code) => {
if (typeof code !== 'string' || !Object.hasOwn(messages, code)) return
console.log(messages[code])
if (code === (send ? 'delivered' : 'connection')) passed = true
})
child.once('error', () => resolve(1))
child.once('close', (code) => {
clearTimeout(timer)
process.removeListener('SIGINT', interrupt)
process.removeListener('SIGTERM', interrupt)
if (code !== 0 || !passed) console.error('FAIL: Electron probe did not complete successfully.')
resolve(code === 0 && passed ? 0 : 1)
})
})
} finally {
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
main().catch(() => {
console.error('FAIL: probe setup, build, or cleanup failed; details suppressed.')
process.exitCode = 1
})
@@ -0,0 +1,48 @@
# Telegram 通道技术设计
本文对应 [PRD FR-1~12](./prd.md),记录当前生产实现。验证状态见[进度记录](./progress.md)。实现约束沿用[性能原则](../../architecture/performance-principles.md),下述结构不构成性能测量结论。
## 生产路径与网络
设置页经生产 Preload 和 IPC 调用 `ChannelManager`,由 `ChannelSettingsStore` 保存配置。Manager 按需加载 `TelegramChannelDriver`,向其注入 Electron `net.fetch`;驱动默认网络实现也使用 `net.fetch`。请求走 Electron 网络栈及系统代理,不依赖 Node 的 `HTTPS_PROXY` 配置,不使用 grammY 或 Telegraf。
驱动先调用 `getMe` 与 `getWebhookInfo`,已有 webhook 时拒绝启动轮询,不删除外部配置。`getMe` 验证的 Bot 身份独立保留;启动 webhook 冲突与运行中的轮询冲突均不阻止身份匹配的已保存回复发送,也不因此终止其发件箱记录。测试连接只做这两项检查,不启动轮询或改写运行中的身份。启用后使用 `getUpdates`,`timeout=30`、`limit=100`、`allowed_updates=['message']`;普通请求超时 15 秒,轮询请求超时 40 秒。临时错误以 1~30 秒退避重连,429 按 `retry_after` 等待;其他 4xx 停止轮询并报告错误。
## 身份、执行与保存
| 对象 | 实现约定 |
| --- | --- |
| 配置 | 单 Bot;Token 加密保存,快照只返回是否已配置;保留、替换、清除三种动作 |
| 环境覆盖 | `GOODBUDDY_TELEGRAM_BOT_TOKEN`、`GOODBUDDY_TELEGRAM_ENABLED`、`GOODBUDDY_TELEGRAM_ALLOWED_SENDERS`;发送者为逗号分隔的数字 ID,空列表只提供帮助;配置只读,群聊不支持 |
| 入站身份 | `accountId=Bot ID`,`conversationId=Bot ID:chat ID`,`eventId=update_id`,授权使用数字发送者 ID |
| 执行 | 普通私聊文字经 `ChannelService` 队列进入现有远程通道 executor,使用 Telegram 项目后端与工作目录;同会话串行 |
| 内置反馈 | `/start`、`/whoami` 和授权媒体提示由驱动直接回复,不调用模型或写业务上下文;每个用户每类反馈冷却 5 秒 |
| 持久化 | 复用 SQLite 远程会话、去重和发件箱;schema 61 将相关 channel 约束扩展到 Telegram,保留既有数据;设置文件仍为版本 3,缺失 Telegram 配置时使用默认值 |
驱动只接收普通私聊,忽略群、频道、话题、编辑更新及无有效发送者的消息。媒体附带文字也不作为业务输入。驱动与服务均检查授权,白名单更新可原位应用,不取消已开始的任务;排队任务执行前再次检查授权。
入站进入内存队列后确认,驱动随后推进内存 offset。去重键为通道、Bot ID、事件 ID;入站队列和 offset 不持久化,不承诺崩溃后自动续跑或零丢失。重连接收平台仍保留的积压消息,工具请求也可能在重新启用后执行。
## 发送与生命周期
结果先进入现有发件箱,发送失败由服务有限重试,不重新调用模型;失败计数达到 5 或遇到永久错误后停止补发。发送前校验目标会话中的 Bot ID,拒绝把旧 Bot 结果交给新 Bot。附件只附加桌面查看提示。
生成图片后,executor 先把 `artifactIds` 写入桌面会话,再向 `ChannelService` 返回通道契约中的状态、正文、错误和附件。图片字节保留在附件中;桌面成果 ID 不进入严格校验的通道结果,避免成功任务被误报为“无效结果”。该转换位于本地和远程 Runtime 事件处理之后,不改变 Runtime 或 Agent 协议。
驱动按最多 4096 个 UTF-16 码元拆分纯文本,并避开代理对中间位置;不设置 `parse_mode` 或引用原消息。分段进度只在驱动内存保存,最多 128 条,停止时清空。单次发送遇到 429 最多额外尝试 3 次;最后一次仍被限流时,先等待该响应的 `retry_after`,再将失败交回发件箱,避免服务层提前重发。等待沿用调用方与服务生命周期的取消信号;停止期间不增加失败计数。跨重启补发与不确定网络结果允许产生重复段落。
停用会中止轮询、发送等待、活动执行与排队工作,等待活动操作结束后报告停止。清除凭据时设置页同时提交 `enabled=false`;底层拒绝启用但无 Token 的配置。Token 替换先验证,失败不停止旧服务。项目与历史不因停用或清除凭据删除。
## 状态与界面
Manager 通过 `{ channel, status }` 增量 IPC 推送状态,忽略旧服务实例的迟到事件。Telegram 连接状态由驱动维护,发送成功不会覆盖轮询错误。状态值为 `disabled`、`starting`、`running`、`stopped`、`error`;重连使用 `starting` 加错误说明,未配置由界面结合凭据状态显示。
Manager 在当前服务实例内保留最近一次发送错误,经既有脱敏函数处理后,附在连接状态的错误说明中。轮询正常时仍显示已连接,同时提示 `Telegram sending failed: ...`;健康轮询不会清除该提示。若同时有轮询错误,优先显示轮询原因;恢复接收后仍可看到未清除的发送错误。后续投递成功清除发送提示,停用或替换服务释放该实例的错误状态,无新增持久化记录或 IPC 字段。
Renderer 的 `channel-status-store.ts` 订阅事件,selector 只向状态区域提供对应通道数据。首次快照不会覆盖已收到的较新事件;状态更新不重新加载整份设置,不重置 Token 或白名单草稿。配置页复用共享页签、开关、项目设置字段和通知控件。
首次配置说明、空白名单提示和发送者 ID 字段帮助均明确:ID 可先留空,先启用并保存通道,连接后私聊自己的 Bot 发送 `/whoami`,取得本人数字用户 ID 后填入并再次保存。仅测试连接不启动消息接收;用户名和 Bot ID 不能代替发送者 ID。白名单为空时仍只提供帮助,不接受 AI 请求。
保存时读取状态 store 的最新值。Telegram 已启用、配置可编辑且状态为 `error` 或 `stopped` 时,即使字段未变,也提交当前配置以重启连接;`running`、`starting` 状态下的无修改保存不重启轮询。环境变量配置仍为只读,测试连接不改变轮询状态。
源码入口:[`telegram-channel-driver.ts`](../../../src/main/channels/telegram-channel-driver.ts)、[`channel-manager.ts`](../../../src/main/channels/channel-manager.ts)、[`channel-service.ts`](../../../src/main/channels/channel-service.ts)、[`channel-settings-store.ts`](../../../src/main/channels/channel-settings-store.ts)、[`channel-status-store.ts`](../../../src/renderer/src/channel-status-store.ts)、[`ChannelSettingsSection.tsx`](../../../src/renderer/src/ChannelSettingsSection.tsx)。
@@ -0,0 +1,86 @@
/* global process, fetch, AbortSignal, console, setTimeout */
// Credentials stay in the process environment; never print raw API errors.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
const token = process.env.TELEGRAM_BOT_TOKEN?.trim()
const listen = process.argv.includes('--listen')
const pending = process.argv.includes('--pending')
const startedAt = Math.floor(Date.now() / 1000)
const marker = '/goodbuddy_test'
async function call(method, body = {}, timeout = 15000) {
let response
try {
response = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeout)
})
} catch (error) {
const code = error?.cause?.code ?? error?.name
const safeCode = /^[A-Z_a-z0-9]+$/.test(code ?? '') ? code : 'NETWORK_ERROR'
// Raw network errors can embed the credential-bearing request URL.
// eslint-disable-next-line preserve-caught-error
throw new Error(`${method}: ${safeCode}`)
}
let data
try {
data = await response.json()
} catch {
throw new Error(`${method}: invalid JSON (HTTP ${response.status})`)
}
if (!response.ok || !data.ok) {
const code = Number.isInteger(data.error_code) ? data.error_code : response.status
const wait = Number.isInteger(data.parameters?.retry_after)
? `; retry_after=${data.parameters.retry_after}` : ''
throw new Error(`${method}: API error ${code}${wait}`)
}
return data.result
}
async function main() {
if (!token || !/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
throw new Error('Set TELEGRAM_BOT_TOKEN to a valid BotFather token.')
}
const bot = await call('getMe')
console.log(JSON.stringify({ check: 'getMe', ok: true, username: bot.username }))
const webhook = await call('getWebhookInfo')
console.log(JSON.stringify({
check: 'getWebhookInfo', ok: true,
webhookConfigured: Boolean(webhook.url), pendingUpdates: webhook.pending_update_count
}))
if (!listen && !pending) return
if (webhook.url) throw new Error('Webhook is configured; polling skipped. No configuration changed.')
console.log(pending
? `Checking queued private ${marker} messages from the last hour.`
: `Send ${marker} in a private chat with @${bot.username} within 45 seconds.`)
const deadline = Date.now() + 45000
while (Date.now() < deadline) {
// Do not advance offset or change allowed_updates on a diagnostic run.
const updates = await call('getUpdates', { timeout: pending ? 0 : 10, limit: 100 }, 15000)
const message = updates.map((update) => update.message).reverse().find((item) =>
item?.chat?.type === 'private' && !item.from?.is_bot &&
item.date >= startedAt - (pending ? 3600 : 0) && item.text === marker
)
if (message) {
await call('sendMessage', {
chat_id: message.chat.id,
text: 'GoodBuddy Telegram API test passed. This is a connectivity test, not an AI task.'
})
console.log(JSON.stringify({ check: 'privateMessageRoundTrip', ok: true }))
return
}
if (updates.length >= 100) {
throw new Error('Pending update batch is full; stopped without confirming existing updates.')
}
if (pending) break
if (updates.length) await new Promise((resolve) => setTimeout(resolve, 2000))
}
console.log(JSON.stringify({ check: 'privateMessageRoundTrip', ok: false, reason: 'no_matching_message' }))
process.exitCode = 2
}
main().catch((error) => {
console.error(error.message)
process.exitCode = 1
})
+166
View File
@@ -1,5 +1,166 @@
# 进度与证据
## 2026-10-05 K07/K08 局部修复
最终集成补注:排除并行删除功能后,待提交内容的隔离全量测试为 6,212 项通过、0 失败、87 跳过,类型检查及 lint 通过。下文分组阶段的失败轮次不是最终结果;完整证据见[KISS 验收](../../review/kiss-local-fixes-2026-10-05.md#最终检查)。
K07 已修复共享 Runtime 在模型路由清理阶段阻塞控制队列、取消后迟到安装留下路由的
问题。broker 关闭与终态提交顺序保留,原生路由撤销在队列外完成;迟到安装按原
transport/Session/operation 再次清理。transport 退出中止未完成路由 HTTP 请求。
规则见[Session 所有权](../assistant-workbar/runtime-process-reuse-technical-design.md#61-连接与所有权)。
K08 在监督正文超容量时立即取消,清空正文并只消费用量事件,清理等待上限 1 秒。
原容量错误优先于后续流错误与超时,不重试、不重复记账;超时以后返回的事件不写库。
该路径由桌面直连模型执行,远程项目的监督也使用同一实现;具体用量规则见
[监督模型调度](../conversation-supervision/technical-design.md#模型阶段超时与调度)。
### 回归与检查
首轮七项持久回归在修复前全部失败:取消/截止时间后的 peer 查询仍等待路由释放,
迟到安装残留,OpenAI/Anthropic 已收到的 12 input/3 output 用量丢失,以及超限后的
记账消费与清理边界不符。修复后增加缓冲完成响应、transport dispose/exit 回归,
共十一项新增用例通过。检查取消终态只有一条、peer 仍 running、无重复 Prompt、
用量只有一个 call,以及迟到事件不会在池槽释放后写入。
```text
npx --no-install vitest run src/main/assistant/supervision-model-retry.test.ts src/main/assistant/supervision-production.test.ts src/main/agent/model-runtime.test.ts src/agent-daemon/runtime-acp-backend.test.ts src/agent-daemon/agent-owned-acp-prompt.test.ts src/agent-daemon/protocol-server.test.ts
npm run typecheck
npm run lint
```
上述六文件 243 项通过、1 项跳过;12:22 的 Main/Agent/Renderer 类型检查及全仓 lint 通过。
并发工作树的完整 `npm test` 为 6,214 项通过、5 项失败、87 项跳过,耗时 870.20 秒;
失败位于监督布局、IPC 的 provider 重试、监督恢复和快照读取测试,K07/K08 文件均通过。
原始输出为批准临时目录中的 `k07-full-tests-1791174423486.log` 及同名 `.json`。
这轮结果不能当作全仓验收通过;未修改其他任务负责的数据库、IPC 或界面文件。
12:45 单独复跑 IPC provider 场景通过(1 项通过、190 项按筛选跳过);期间其他任务仍在
修改测试,后一次类型检查报 `supervision-ui-reads.test.ts` 的 `publishing` phase 类型
错误。最终整体检查需由合并工作树的负责方复跑,不能用本模块的通过结果替代。
### Linux x64 当前源码
复用前阶段 `a01-runtime-host` 的凭据与 SSH 固定身份入口,仅以 esbuild 生成当前
Desktop driver、Agent daemon 和 CLI/helper 测试 bundle。使用 Host 已安装的原生
OpenCode/Continue,在 `/root/tmp/gb-lifecycle-*` 专用目录运行,未安装生产包或更换
Host current。阻塞点只由 harness 注入;路由安装、撤销及 409 缺失校验通过真实 helper。
| 场景 | peer 查询 | 取消完成 | 外部文本 HTTP 请求 | 结果 |
| --- | ---: | ---: | ---: | --- |
| 首轮 OpenCode | 51 ms | 206 ms | 2 | 取消及迟到路由清理通过;后续工具恢复触及旧夹具的两次调用上限 |
| 最终 OpenCode | 13 ms | 176 ms | 3 | peer 完成、工具执行、detach/恢复、测试 Agent 提升、终态重放通过 |
| 最终 Continue | 46 ms | 194 ms | 3 | 同上 |
准确合计 **8 次外部文本提供商 HTTP 请求、0 次图片提供商调用**,八个响应均 HTTP 200。
最终每个 Runtime 包含一次短文本 peer 调用和两次工具/恢复调用,预算在 harness 中
显式设为三次;被取消的启动未调用提供商。三个运行结束时所属进程均为 0,额外的
只读 SSH/SFTP 检查确认本轮时间范围内的专用目录残留为 0,该检查新增模型调用 0 次。
延迟是单次场景观察,不作为 p95 或桌面性能预算结论。
最终 Host 事件通过现有批处理器和隔离 SQLite 校验:OpenCode 41 条事件、21 个
checkpoint;Continue 16 条事件、8 个 checkpoint;重放去重通过。没有读写用户生产库。
原始脚本位于 `C:/Users/jiang/AppData/Local/Temp/opencode`:
`k07-runtime-host-build.cjs`、`k07-runtime-host-run.cjs`、`k07-host-cleanup-build.cjs`。
日志位于其 `k07-runtime-host/` 子目录:`opencode-1791173985798.log`、
`opencode-1791174187014.log`、`continue-1791174187010.log`、`inspect-1791175306647.log`。
验证不覆盖签名安装包、macOS、Linux arm64、完整 UI 操作或生产规模性能;远程修复需
部署包含本次源码的 Agent,当前未提交或发布。
## 2026-10-05 Runtime 生命周期审查修复
A01(远程启动阻塞控制队列)和 A02(本机 OpenCode 提交失败后等待静默 SSE)已修复。
远程启动等待释放 backend 控制队列及协议接收分派,取消、watchdog、关闭和其他
binding 可以继续处理;Session owner 合并相同启动、拒绝冲突,并关闭取消后迟到的
原生 Session。本机提交错误立即唤醒所属 SSE 消费,保留原错误和既有 Session 取消。
两条实现均未增加整个 Prompt 的默认总时限;共享进程、终态提交和写帧顺序规则见
[Runtime 进程复用设计](../assistant-workbar/runtime-process-reuse-technical-design.md)。
### 确定性复现
持久回归中,本机拒绝、错误响应、10 ms 提交超时三种情况在旧代码经过 100 ms
假时钟后仍未结算;远程共享进程中的第二个 Session 启动被挂起时,旧代码经过
200 ms 后,取消/截止时间和同进程另一 binding 的查询均未结算。上述五项先运行旧
实现,全部按这些断言失败。真实 Host 首轮还定位到协议 dispatcher 串行等待启动
响应,新增帧级回归在修复前只分派 `start`,没有分派后续 `cancel` 和 `peer`。
修复后的九项新增回归通过,另覆盖启动 watchdog、默认关闭时限、相同启动重放及
冲突。共享 peer 保持 running;迟到 Session 收到自身的 `session/close`,取消的
启动不发送 Prompt。原始审查探针曾在 131 秒假时钟后观察到启动、取消、peer 查询
仍等待,默认 10 秒 dispose 报超时且 stop 次数为 0,现有关闭与 watchdog 回归不再
等待原生初始化响应。各假时钟值是确定性边界,不是系统性能测量。
### 当前源码实机验证
复用已有 lifecycle harness,用 esbuild 重建当前 Desktop driver、Agent daemon、
CLI/helper;凭据从 GoodBuddy 已有加密配置的临时副本加载,SSH 校验已固定 Host
identity。共享 Linux x64 经 `192.168.0.23` 连接,只使用 `/root/tmp/gb-lifecycle-*`
专用目录和所属进程,未替换原 Host 安装或修改无关数据。
| 场景 | 实际结果 | 真实文本提供商调用 |
| --- | --- | ---: |
| 首轮 Host 启动挂起 | backend 修复后,协议 dispatcher 仍阻塞;测试退出后所属进程为 0 | 0 |
| OpenCode 启动取消及工具、detach、controller 恢复、测试 Agent 提升、重放 | 启动响应被挂起时控制查询 48 ms、取消 216 ms;工具与恢复完成;两个提供商响应均 HTTP 200 | 2 |
| Continue 同场景 | 控制查询 60 ms、取消 258 ms;工具与恢复完成;两个提供商响应均 HTTP 200 | 2 |
| Windows 当前源码 OpenCode 普通文本 | 真实原生二进制经有界 loopback 转发访问已配置模型,返回 `LOCAL_RUNTIME_OK`,0 工具调用;总耗时 4,727 ms | 1 |
本轮合计 **5 次真实文本提供商调用、0 次图片生成调用**。两次本机脚本准备失败发生在
SDK 动态加载阶段,均为 0 次模型调用,随后修正测试 bundle 并通过。所有成功场景均
使用最终相关生产源码。Host 启动取消阶段各为 0 次模型调用,最终所属残留进程均为 0,
专用目录由 harness 清理;本机 Runtime 和凭据临时副本也已清理。
真实数据检查使用本轮原生工具和模型产生的事件:OpenCode 45 条写入、23 个 checkpoint,
Continue 16 条写入、8 个 checkpoint,经生产 `AssistantDatabase` 和
`RemoteEventBatcher` 写入隔离 SQLite,再重放确认全部去重。未迁移、改写用户生产
历史库,也未以构造事件代替本轮 Host transcript。
原始脚本和日志位于 `C:/Users/jiang/AppData/Local/Temp/opencode`:
`a01-runtime-host-build.cjs`、`a01-runtime-host-run.cjs`、`a01-runtime-host/`,以及
`a02-local-model.ts`、`a02-local-model-run.cjs` 和 `a02-local-model-1791142126078.log`。
Host 成功日志为 `opencode-1791141713375.log`、`continue-1791141785721.log`。
延迟为各场景一次实测,不宣称吞吐、p95 或完整桌面性能提升。
### 检查命令与边界
```text
npx vitest run src/agent-daemon --testTimeout=30000 --hookTimeout=60000
npx vitest run src/main/remote-agent src/main/agent/acp-remote-runtime.test.ts src/main/agent/runtime-controller.test.ts src/main/agent/local-runtime-registry.test.ts --testTimeout=30000
npx vitest run src/main/agent/opencode-runtime.test.ts
npx vitest run src/main/agent/opencode-runtime-lifecycle.test.ts --testTimeout=30000 --hookTimeout=60000
npm run typecheck
npm run lint
```
四组测试分别为 323 通过/33 跳过、393 通过、156 通过、4 通过,共 876 项通过;
typecheck、全仓 lint 和修改范围 `git diff --check` 通过。初次合并运行中,既有
OpenCode `cleanup-timeout` 用例失败一次;该用例独立复跑及随后完整 OpenCode 文件
复跑通过,未改动该用例或相关产品清理逻辑。较早 Main 类型检查的并发监督者 fixture
错误在最终全仓检查时已消失。
验证覆盖当前源码、真实 Windows OpenCode 二进制和共享 Linux x64 Host,不含签名包
安装验收、完整 UI 点击、macOS/Linux arm64 或用户生产大库性能测试。首轮定向验证
未运行全仓 `npm test`,后续完整检查见下节;远程更新仍须部署包含本次源码的 Agent,
未提交或发布。
### SDK cleanup-timeout 复查
后续全仓验证为 6,130 通过、1 失败、87 跳过,唯一失败是 OpenCode SDK
`cleanup-timeout` 用例。复查通过独立 Node 子进程的十轮显式 GC 和受控取消复现:
旧 fetch 替身只保存 `request.signal`,返回永不结算的 Promise,没有在途 Request
所有者或 abort 处理。Request 及 Undici 内部 AbortController 被回收后,外层超时
signal 已中止,保存的 Request signal 仍未中止。真实 loopback fetch 在同样 GC
条件下保留 Request,取消后 SDK 返回错误,未观察到产品清理提前完成。
修正仅涉及测试:挂起的 fetch 替身在 abort 时以 `request.signal.reason` 拒绝,
闭包保持 Request 存活至取消。原有 `signals.every(signal => signal.aborted)`、调用
顺序、错误脱敏及超时均保留。新增 GC 回归在旧替身上稳定失败,修正后通过;九项
SDK 定向用例、Main 类型检查及该文件 ESLint 通过。本次跟进新增外部模型调用 0 次,
生产代码与远程 Agent 路径均未改动,无需重新执行 Host 模型验证。
修正后的完整 `npm test` 于 04:00:22 开始,852.63 秒完成,退出码 0:518 文件通过、
15 文件跳过;6,136 项通过、0 失败、87 跳过。计数包含并发工作树中的其他修复。
原始记录为 `C:/Users/jiang/AppData/Local/Temp/opencode/a02-cleanup-final-20261005.log`
及同名 `.json`;GC/真实 loopback 对照脚本为该目录的 `a02-cleanup-gc-probe.mjs`。
## 2026-10-04 调研与设计
调研阶段,用户确认彻底移除 Ask / Execute,包括界面、原生工具与 MCP 限制,并允许升级清理历史模式字段。该阶段先交付文档,随后用户确认删除安全设置并授权实施;实现结果见下文。
@@ -98,3 +259,8 @@ SSH 专家保留父请求已解析的 OpenCode 或 Continue 选择,通过现
`deai-writing` 扫描九份文档,阻断项为 0。复核项主要为未验证状态、数据保留条件和字段枚举;结合技术文档语境保留这些限定,不将迁移计划改写为已完成事实。
`git diff --check -- docs/features/unified-execution docs/features/assistant-workbar/README.md` 通过已跟踪改动的空白检查。新建文档另经内容与链接核对;该命令不会把未跟踪文件当作已验证差异。文档检查不替代运行时验收,后者按用户故事与技术设计执行。
## 2026-10-05 直连模型工具错误回传
普通工具执行失败不再直接终止直连模型循环,错误结果继续交给模型处理,取消行为保留。
实现、三协议回归及全量测试边界见[直连模型修复记录](../direct-model-agent/progress.md#2026-10-05-工具执行异常回传)。
+18 -12
View File
@@ -10,6 +10,8 @@
| 适用产品 | GoodBuddy 桌面端 |
| 首期范围 | 微信 ClawBot、企业微信、钉钉的通道项目;微信文字、图片和文件请求 |
2026-10-05 枚举更新:当前通道项目已增加 Telegram,下文同步更新通用项目、界面和契约枚举;首期范围、背景与第 17 节保留原实施范围。Telegram 的产品行为见[Telegram PRD](../telegram-channel/prd.md),验证状态见[进度记录](../telegram-channel/progress.md)。
## 1. 背景
GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入系统管理的通道项目。该信息架构解决了早期通道仅存在于设置页和后台服务中,远程消息进入任务系统后无法持续、明确识别以下信息的问题:
@@ -30,8 +32,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
## 2. 已确认的产品决策
1. 微信 ClawBot、企业微信、钉钉分别对应一个系统管理的通道项目。
2. 三个通道项目在通道设置服务初始化时幂等创建,不等待用户完成连接配置。
1. 微信 ClawBot、企业微信、钉钉、Telegram 分别对应一个系统管理的通道项目。
2. 四个通道项目在通道设置服务初始化时幂等创建,不等待用户完成连接配置。
3. Renderer 不直接创建通道项目。Main 进程保证项目存在,设置卡片首次展示时即可引用对应项目。
4. 通道项目默认根目录为当前操作系统用户目录。
5. 同一通道中的不同私聊用户或群聊分别建立独立远程会话。
@@ -48,7 +50,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
### 3.1 用户目标
- 在项目切换器中一眼识别微信、企业微信和钉钉来源。
- 在项目切换器中一眼识别微信、企业微信、钉钉和 Telegram 来源。
- 在通道项目中区分不同发送者和群聊。
- 在设置卡片中完成连接、启停、处理后端和工作目录配置。
- 通过微信问答或发起工具任务。
@@ -89,7 +91,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
远程通道
├─ 微信 ClawBot
├─ 企业微信
└─ 钉钉
├─ 钉钉
└─ Telegram
```
选择器收起时显示当前项目名称;展开后每个选项至少显示项目名称和项目类型。本地项目补充目录摘要,远程通道项目补充平台来源和默认目录;接入可复用的通道状态数据后继续补充文字连接状态。未来接入其他远程项目时可以补充远程主机、工作区或服务来源,而不改变菜单的基本结构。
@@ -137,7 +140,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
应用初始化通道设置服务时,Main 进程执行 `ensureChannelProjects()`:
1. 按稳定通道标识查找 `weixin`、`wecom`、`dingtalk` 对应项目。
1. 按稳定通道标识查找 `weixin`、`wecom`、`dingtalk`、`telegram` 对应项目。
2. 缺失时创建系统管理项目。
3. 已存在时复用,不重复创建。
4. 如果存在同名普通项目,不得按名称占用或修改该项目。
@@ -150,6 +153,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
| `weixin` | 微信 ClawBot | 用户目录 | 默认直连文本模型 |
| `wecom` | 企业微信 | 用户目录 | 默认直连文本模型 |
| `dingtalk` | 钉钉 | 用户目录 | 默认直连文本模型 |
| `telegram` | Telegram | 用户目录 | 默认直连文本模型 |
### 6.2 系统管理约束
@@ -176,11 +180,11 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
设置页标签由“企业通信”改为:
- 标题:消息通道
- 说明:微信 ClawBot、企业微信与钉钉
- 说明:微信 ClawBot、企业微信、钉钉与 Telegram
### 7.2 卡片通用结构
三个通道使用与模型设置一致的分段外观页签,并保留
四个通道使用与模型设置一致的分段外观页签,并保留
`tablist`、`tab`、`tabpanel`、游标焦点和方向键语义。每次只显示一个
通道面板,窄窗口下页签单行横向滚动。
@@ -294,6 +298,8 @@ Agent Runtime。OpenCode/Continue 的通道项目只持久化 `provider`,旧
现有发送者白名单和群聊提及设置继续有效。
钉钉回复以接口响应中的 `errcode=0` 确认投递,HTTP 200 本身不代表成功。业务错误或无效响应保留回复上下文和发件箱结果,由现有有限重试补发,不重新执行任务。响应读取上限为 64 KiB,包含没有 `Content-Length` 的分块响应;错误提示只保留数值错误码,不回显平台响应中的敏感字段。
## 8. 远程会话需求
### 8.1 会话创建与复用
@@ -500,7 +506,7 @@ Main 到 Sidecar:
```ts
type ProjectKind = 'user' | 'channel'
type ProjectChannel = 'weixin' | 'wecom' | 'dingtalk'
type ProjectChannel = 'weixin' | 'wecom' | 'dingtalk' | 'telegram'
```
通道项目包含:
@@ -610,7 +616,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
## 15. 可访问性与响应式
- 通道状态同时使用文字和图标。
- 三个通道使用共享 `PageTabs` 的 `segmented` 外观,保留页签语义和方向键切换。
- 四个通道使用共享 `PageTabs` 的 `segmented` 外观,保留页签语义和方向键切换。
- 后端选择保留语义化控件和键盘操作,不留已删除模式控件的焦点目标。
- 消息处理后端使用持久标签和分组选项,并说明当前选择的实际行为。
- 二维码提供状态文字和备用刷新操作,但不把敏感二维码链接作为可复制文本。
@@ -623,7 +629,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
### 16.1 通道项目
- [ ] 新安装首次启动后存在微信 ClawBot、企业微信和钉钉三个通道项目。
- [ ] 新安装首次启动后存在微信 ClawBot、企业微信、钉钉和 Telegram 四个通道项目。
- [ ] 重启应用不会重复创建通道项目。
- [ ] 同名普通项目不会被占用或修改。
- [ ] 通道项目默认根目录为当前用户目录,不保存默认模式。
@@ -636,8 +642,8 @@ Renderer 快照只返回是否已配置和脱敏标识。
### 16.2 设置卡片
- [ ] 设置标签显示为“消息通道”。
- [ ] 三个通道使用与模型设置一致的分段外观页签,并保留完整页签键盘语义。
- [ ] 三个面板均显示项目、根目录、消息处理后端和连接状态。
- [ ] 四个通道使用与模型设置一致的分段外观页签,并保留完整页签键盘语义。
- [ ] 四个面板均显示项目、根目录、消息处理后端和连接状态。
- [ ] 微信卡片可以完成扫码、过期刷新、验证码和连接状态展示。
- [ ] “断开本机绑定”使用共享红色危险操作样式,并明确说明只清除本机凭据、不删除历史或承诺服务端解绑。
- [ ] 不显示“自动”后端;首次创建和旧版 `auto` 配置均落到默认直连文本模型。
+38 -295
View File
@@ -1,334 +1,77 @@
# GoodBuddy 更新:故事图谱、Obsidian接入、Runtime原生终端
# GoodBuddy 更新:故事图谱、Telegram 与 Obsidian 接入
> **编辑标注说明(对外发布前删除):** 本稿汇总桌面版 0.13.8 至 0.15.9,截至 `9527402` 的已提交变化,更新于 2026-10-03。所述功能与修复均已开发完成并提交;原故事线概念图仅作示意,待换为生产界面截图。“待补图”表示缺少可发布素材,“待替换”表示发布信息未核验,“待核对”表示发布前需要确认的行为。本文是宣传草稿,尚未对外发布。
> **编辑标注说明(对外发布前删除):** 本稿汇总桌面版 0.13.8 至 0.15.13,以及截至 `3556df88` 的已提交变化,更新于 2026-10-09。本文所述功能均已完成并提交;截图、下载信息和正式发布链接属于发布材料,另行补齐。原故事线概念图仅作示意,发布前替换为生产界面截图。
GoodBuddy 这一轮更新围绕工作记录的整理与使用展开:监督者把分散的事件归入持续发展的故事,从中整理带有来源和适用条件的经验,供人和助手在后续工作中查阅。笔记连接支持保存聊天正文、返回原会话,以及在聊天中读写本机 Obsidian 笔记。本地项目还可以从输入区打开 Continue、OpenCode 原生终端或 DS Web 客户端,沿用当前项目与模型配置。
GoodBuddy 这一版完成了三条连接:用故事图谱整理持续推进的工作,用 Telegram 在桌面之外发起对话,用 Obsidian 连接已有的本地笔记。故事图谱帮助人和助手回看决定、依据与后续变化;Telegram 适合在手机或其他设备上继续处理文字请求;Obsidian 则让原有笔记仓库可以直接参与对话。
本文接续上一份画布笔记与文档处理更新稿,覆盖 0.13.8 起的实际增量。这次还加入了智能心跳的增量回顾与建议、笔记标签、会话图片保存,以及按会话、项目和全局分别选择 Runtime 与模型的方式,并继续改善长会话的浏览与输入。远程使用建议配套 Desktop 0.15.9 和 Agent 0.15.3,请先升级桌面端,再更新 Host 环境。
当前配套版本为 Desktop `0.15.13` 和 Agent `0.15.6`。远程项目使用这些能力前,请先升级桌面端,再更新 Host 环境。
> **【待替换 R01|版本与体验入口】** 最新已核查桌面 tag `v0.15.9` 与 Agent tag `agent-v0.15.3` 均指向 `9527402`。本轮检查 `b02ce31..9527402`,共 45 个提交;图片保存、笔记取消及默认开关调整已包含在桌面 0.15.8 中。对外发布前核验公开安装包、发布日期、下载地址和完整更新日志链接,并在此补入。桌面 0.13.14 的发布失败后由 0.13.15 承接。
> **【发布信息待补】** 正式发布前补入发布日期、公开安装包、下载地址和完整更新日志链接。本文所述功能已经完成,发布信息待补不代表功能处于开发中。
**升级前,请先关闭应用并备份完整用户数据。** 数据库和相邻的 `notes/` 目录需要成套保留;如有 WAL 文件,也应一并备份。当前助理数据库格式为 56,包含共享时间线、故事归属、经验及建议等更新;升级后旧客户端不能直接打开该数据库,回退需要升级前的备份。
## 故事图谱:从工作记录中看清一件事如何发展
## 新增功能
### 工作回顾留下进展和依据
### 监督者:从工作记录中整理进展,查看每条结论的依据
监督者可以按全局或项目范围回顾最近一天、数天或指定日期内的工作记录,整理出工作总结、变化和未解决事项,并按故事列出推进、新出现和结束的内容。回顾结果会保留范围与生成时间,方便之后回看。
项目讨论持续一段时间后,可以打开“监督者”的“工作回顾”,选择全局或一个项目,以及最近 1 天、7 天或 30 天的范围。回顾会整理工作总结、本次变化和未解决事项,并按故事列出这段时间的推进、新出现与结束的事项,以及形成或用上的经验。点击故事或经验,可以进入图谱查看依据;历史结果保留各自的范围和生成时间,方便回看。
每条结论都可以继续追溯到事件、实体、关系和来源正文。打开来源后,可以检查模型依据的是哪段讨论,再决定是否确认、改名或调整关系。
监督者默认关闭,在应用中心启用后即可手动回顾,无需先建立自动计划。当前分析来源包括范围内的用户与助手消息、任务标题与当前状态,以及消息中已有的知识引用片段。它不会自动遍历全部笔记、整个知识库或附件原件。
### 故事、经验和时间螺旋
> **【待补图 01|必补|工作回顾结果】** 在隔离演示项目中通过正常聊天和任务操作形成公开、虚构的工作记录,运行一次真实回顾。截图同时展示范围、时间和实际生成的总结、未解决事项;不能用预置答案冒充模型结果。计划文件:`assets/01-work-review.png`。
>
> 建议图注:按项目回顾近期讨论与任务,查看进展和仍待处理的问题。
事件可以归入持续发展的故事,按项目、功能和子线索浏览。用户可以调整故事名称、合并重复故事、移动事件或撤销调整。故事中的经验会保留适用条件、形成依据和后续应用记录,也可以人工编辑、合并或删除。
回顾中的事件、实体与关系可以在“故事线图谱”中查看。选中一个对象,就能检查它的说明、关联和保存的来源正文,再判断模型的归纳是否准确。实体支持确认、改名或撤销,关系可以确认或移除。
这适合检查“某个结论来自哪段讨论”“一个事项与哪些事件有关”。图谱提供平铺和时间螺旋两种浏览方式,时间螺旋按事件时间排列;平铺视图中的节点间距不表示实际时长。历史回顾中的故事分组使用当前保存的故事结构,尚不提供任意时刻的完整状态还原。
> **【待补图 02|必补|图谱与来源】** 使用图 01 同一份真实回顾结果,选中事件或实体,展示对应的来源正文。画面应能辨认对象与依据的关联;不使用设计原型代替生产结果。计划文件:`assets/02-story-sources.png`。
>
> 建议图注:从回顾图谱查看关联对象,并打开保存的讨论依据。
### 故事与时间螺旋:追踪一件事如何持续发展
一项功能往往经过多次讨论、试验和修订。回顾后,相关事件可以归入同一个故事,按项目、功能和子线索逐层浏览;后续有新进展时继续积累,不必每次从零查找。用户可以调整名称、合并重复故事、移动归错的事件,也能撤销调整;移除故事会保留其中的事件。
时间螺旋把这些故事放到连续时间上,便于查看不同工作如何交错推进。同一件事可以隔一段时间再继续,中间的停顿保留下来。螺旋高度对应时间,半径随消息活动量变化,不代表实际工时、效率或成果价值。
视图支持斜俯视、侧视、俯视和自由视角,可以逐层进入项目、功能与子线索,选中故事或事件查看详情。当前层可显示部分经验及其跨故事应用连线,完整经验列表仍在“经验”页签中查阅。设备无法使用 3D 时,可以切回平铺视图。
故事图谱提供平铺和时间螺旋两种视图。时间螺旋按事件时间和消息密度展示工作如何交错推进,适合观察停顿、延续和阶段变化;平铺视图适合查看对象之间的关系。设备不适合 3D 浏览时,可以切换回平铺视图。
![故事线早期概念示意:时间螺旋、跨阶段故事,以及经验的形成依据与后续应用](assets/09-next-storyline-wisdom.png)
*早期概念示意,使用示例数据;不是当前生产界面的截图,实际布局与操作以应用为准。*
*早期概念示意,使用示例数据;发布前替换为当前版本的生产界面截图。*
> **【待补图 09|必补|故事与经验的生产视图】** 原图保留作概念参考。发布前用目标版本的真实回顾结果替换,展示同一故事的事件、经验形成依据与后续应用,并核对平铺和时间螺旋中的选择联动。计划文件:`assets/09-story-experience.png`。
>
> 建议图注:沿故事查看工作进展,从经验节点核对形成依据与后续应用记录。
### 助手可以接着既有决定继续工作
### 从故事中整理经验,保留适用条件和依据
当用户追问“此前为什么选这个方案”或“这项决定后来怎样修改”时,助手可以查询已经保存的故事、经验、事件、实体、关系和回顾结果,再读取相关来源。查询发生在需要时,不会在每次对话中自动注入整张图谱。
故事积累到一定规模,并出现新的决定、变化或里程碑后,监督者可以从事件摘要中归纳经验,保存适用条件、边界和形成依据。自动归纳的内容有明确标记,用户可以编辑、合并或删除,再回到原讨论核对。
输入区可以单独控制当前会话是否允许访问故事图谱。查询默认使用当前项目,也可以指定全局或项目集合;故事图谱工具只读取已有记录,不会因为一次查询重新运行回顾或修改图谱。
后续回顾还可以记录某条经验在哪些事件中得到应用。应用记录会核对事件是否晚于最早形成事件的起始时间,并与至少一个形成事件涉及共同实体;这些记录仍是模型对历史的归纳,需要结合来源判断,不能据此断定经验已经产生效果。助手可以查阅这些经验,为后续工作提供参考。
### 智能心跳持续整理变化
### 助手可以查阅故事图谱,接着已有决定继续工作
设置每日或每周计划后,智能心跳会处理新增和变化的工作记录,更新故事图谱,并提出未决事项、分歧、修订、候选约定、长期没有进展的故事和可能适用的经验。没有新的工作来源时会跳过事实提取;建议生成失败可以单独重试,已保存的回顾和图谱仍然保留。
继续一个项目时,可以请助手“查一下此前选了哪个方案,依据是什么”,或“找出这项决定后来的修订”。助手可搜索监督者已经保存的故事、经验、事件、实体、关系和回顾结果,读取相关上下文,再查看来源片段,作为后续讨论的依据。故事上下文带出相关事件,经验上下文带出形成和应用记录。
## Telegram:从私聊继续处理任务
工具说明也会引导助手在继续旧工作、追问决定原因或遇到可能已有约定的任务时主动查阅,优先找故事与经验。是否调用仍由当前模型判断;普通闲聊和独立的通用问题不要求查询,也不会在每次对话中自动塞入整张图谱。
配置自己创建的 Telegram Bot、数字用户 ID 白名单和通道项目后,可以通过 Telegram 私聊发送文字问题或任务。GoodBuddy 会沿用该项目的 Runtime、模型和工作目录处理请求,并把最终文字回复返回到 Telegram,桌面端同时保留会话记录。
在应用中心启用监督者,并在 MCP 设置中保持 Story Graph 开启、分配给所用 Runtime 后即可使用。支持直连模型、DeepSeek Harness,以及本地和远程 OpenCode、Continue;Ask 与 Execute 均可只读查询。本地 Continue、OpenCode 原生终端也可读取。
首次配置时可以先留空发送者 ID,启用并保存通道;连接 Bot 后发送 `/start` 和 `/whoami` 获取自己的数字用户 ID,再填入白名单并保存。连接支持断线恢复和停用取消,桌面应用需要保持运行并能连接 Telegram。
查询默认使用当前项目,也可明确指定全局或项目集合。事件可从共享时间线中按项目查阅,回顾结果仍保留各自的范围。工具只读取已有记录,不会为一次查询重新运行回顾或修改图谱;助手根据结果生成回答仍使用当前模型。保存的来源片段可以分页读取,尚不支持任意时刻的完整历史状态重建。
当前通道面向单个 Bot 的普通私聊和文字请求。群聊、媒体输入和附件发送仍在桌面端处理,图片等附件结果需要回到桌面查看。
### 智能心跳:按新增工作回顾,并提出可处理的建议
## Obsidian:直接连接已有笔记仓库
为项目设置每日或每周计划后,智能心跳会整理回顾窗口内新增、修改或尚待处理的记录,把变化保存到故事图谱。计划默认生成建议,也可以选择“仅更新记忆”;没有新来源时跳过事实提取,仍可补做尚未完成的故事与经验整理。没有待处理工作时,不调用模型。
在“设置 → 能力与工具 → MCP”的 Obsidian 卡片中,可以选择本机已注册的仓库或限定文件夹,测试连接并分配给所用 Runtime。无需启动 Obsidian,也无需安装插件或配置 REST 服务。
建议围绕本次回顾发现的未决事项、分歧、修订和候选约定展开,并保留来源供核对。接受未决事项后,会创建一项暂停任务,方便先检查再开始;候选约定经确认后才进入长期记忆。建议生成失败可以单独重试,已经保存的图谱和回顾进度仍然保留。
接入后,可以让助手搜索和读取笔记,也可以把讨论结果写回仓库。支持追加、局部修改、目录浏览,以及标签和属性操作。笔记仍保存在原仓库,不会自动复制到 GoodBuddy 知识库。
心跳完成回顾后,还可以提示达到设定天数仍没有新进展的故事,或指出某条已有经验可能适用于当前故事。建议可以直接打开图谱;接受暂无进展建议会建立暂停任务,接受经验建议只标记已参考,不会据此写入经验应用记录。暂无进展默认按 14 天筛选;仅时间流逝到期、但本次回顾没有变化时,不会单独触发提醒。
Obsidian 接入默认关闭,启用后只在配置范围内读写。当前支持直连模型及 GoodBuddy 管理的本机 OpenCode、Continue 聊天;读出的笔记会随当前会话交给所选模型处理。
计划需要应用保持运行,并按所选全局或项目范围处理。心跳不会在每条聊天消息后实时介入;有候选建议时会额外调用模型整理措辞,回顾本身也可能分批调用模型。
## 其他改进
### 在任务中心跟踪当前会话或任务
### 输入区和 Runtime 控制
右侧任务中心的项目概览下方新增监督反馈。选中任务时查看该任务的匹配结果,否则跟随当前会话;也可以固定目标,切换聊天后继续查看同一个事项。没有匹配回顾时会明确显示空状态。
上下文来源、专家角色、模型和 Runtime 控制收进输入区工具栏。故事图谱与知识库使用同一个上下文入口,OpenCode 和 Continue 的运行配置放入独立弹窗。窄窗口下仍能按可用空间排列,弹窗支持自动聚焦、外部点击关闭和 Escape 返回焦点。
监督反馈中的“继续讨论”可以直接使用,无需先打开来源。侧栏和图谱来源中的入口都会展示完整、可编辑的发送正文,包含相关回顾结论和来源内容;可以删改引用、补充问题,再确认单独显示的目标会话。发送以编辑后的正文为准,已删除的上下文不会被重新附加;发送失败保留草稿。在图谱来源中发送后会打开目标会话,需要先看原讨论时,也可单独点击“打开会话”。
### 笔记、文件和图表
带入的结论按所选来源关联的事件、实体与关系整理,方便围绕当前事项继续追问。监督界面已移除知识实体写回控件。
魔法笔记支持插入和编辑表格,表格与其他富文本一起保存;笔记支持标签筛选、标签管理和列表与正文双栏浏览。聊天回答可以保存到笔记并返回原会话,工作区支持批量导入本地文件到本地或远程项目,Mermaid 图表可以导出完整 PNG。
后续讨论的最终内容最多 12,000 字符。固定任务没有关联会话时,不能直接发送继续讨论;后续对话沿用目标会话的模型和工作模式。
### Runtime、会话和日常操作
### 聊天正文存入魔法笔记,并保留来源
项目和会话集中到同一个菜单,Runtime 与模型可以分别按会话、项目和全局配置解析,也可以在当前会话临时切换。聊天历史、侧栏和长消息减少重复渲染,阅读位置、展开状态和流式回复更稳定;桌面通知、磨砂外观和任务用量可以分别设置。
遇到值得保留的回答,可以点击助手消息旁的“加入笔记”,预览并编辑文字,再追加到已有笔记,或新建笔记保存。需要保留整段讨论时,会话菜单中的“保存当前对话到笔记”会读取完整历史,收录用户与助手的正文,不局限于屏幕上已经显示的消息。
### 请求、存储和故障处理
保存后的记录带有“来自会话”入口。单条回答可以返回对应消息;原会话或消息被删除后,已保存的笔记内容仍保留。每次采集新增一条记录,后续对话修改不会自动同步到笔记。
SQLite 存储、后台读取、请求取消和并发处理路径已经合并调整,减少主进程等待。直连模型支持手动压缩上下文,工具执行失败后可以继续对话;长请求结束、取消或失败时会正确回收工具授权。监督回顾支持暂停、继续、取消和失败后重试,远程 Runtime 启动和本地 OpenCode 连接也补充了取消与恢复处理。
工作栏“+ → 笔记”可以打开紧凑面板,在聊天旁搜索笔记标题与正文、浏览摘要、追加文字。切换项目或会话时草稿保留,关闭有未保存内容的面板前会提示;草稿仅在本次应用运行期间保留。画布、待办和完整编辑继续在魔法笔记工作区中使用。
## 升级提示
快速笔记入口集中在工作栏和聊天采集操作中,移除了侧栏与顶栏的重复快捷按钮。面板精简重复标签,采集范围说明按需展开,标题或正文超限时在对应输入位置提示。保存和来源操作统一靠右排列,窄面板中的保存按钮也保持紧凑。
升级前请关闭应用,并备份完整用户数据、设置、数据库、WAL 文件和相邻的 `notes/` 目录。当前数据库格式为 62,0.15.13 之前的客户端不能直接打开新版数据库,回退需要使用升级前的备份。首次转换较大的数据库时需要等待;高频本地保存调整了磁盘同步策略,断电时可能丢失最近尚未同步的写入。
采集保存文本与 Markdown,不复制推理过程、工具日志或附件图片。生成中的回答暂不可采集,单条记录上限为 500 KiB,超限会提示缩减。采集本身无需调用模型;保存后自动评论仍按已有设置执行。笔记继续属于全局,来源项目只用于帮助回看。
> **【待补图 06|必补|聊天采集与来源返回】** 用公开演示对话预览并保存一条回答,展示笔记记录中的来源入口,再验证能返回原消息。计划文件:`assets/06-chat-note-source.png`。
>
> 建议图注:把有用的回答存入笔记,需要上下文时返回原会话。
### 笔记支持标签筛选,列表与正文并排浏览
给笔记加上“项目资料”“待整理”等标签后,可以按标签查找,也可以同时选择多个标签,筛出同时带有这些标签的笔记。每篇最多添加 10 个标签;标签支持改名、同名合并和删除,删除标签会保留笔记正文。
宽窗口打开笔记时,左侧保留列表,右侧阅读和编辑正文,两栏宽度可以拖动调整。切换笔记时保留当前工作区内的筛选与阅读位置,有未保存内容时会提示。窄窗口改为列表、详情切换;记录索引默认收起,“新建笔记”固定在页头。
正文使用当前栏的可用宽度,宽窗口下阅读画布和长内容有更多空间。新建文字或画布记录都可以取消,有草稿时先确认是否放弃;取消编辑、切换记录类型或返回聊天后,焦点回到对应的按钮或输入位置。
标签也可由支持魔法笔记工具的助手按你的要求添加或修改。快速采集面板继续用于保存聊天内容,标签管理在完整笔记工作区中进行。
### 在聊天中查找和整理 Obsidian 笔记
已有 Obsidian 笔记可以直接接入 GoodBuddy。在“设置 → 能力与工具 → MCP”的 Obsidian 卡片中,选择本机已注册的仓库,或限定一个文件夹,测试连接并保存,然后启用并分配给所用 Runtime。无需启动 Obsidian,也无需额外安装 Obsidian 插件或配置 REST 服务。
接入后,可以请助手查找笔记、读取内容,或将讨论结果写回仓库;也支持追加、局部修改、目录浏览,以及标签和属性操作。笔记仍保存在原仓库,不会自动复制成 GoodBuddy 知识库。
目前支持直连模型及 GoodBuddy 管理的本机 OpenCode、Continue 聊天,默认关闭;DeepSeek Harness 和远程执行暂不支持。Ask 只读,Execute 可写入;多仓库调用需要明确目标仓库。接入本机文件不等于离线推理,读出的内容会随当前会话交给所选模型处理。
> **【待补图 07|建议|Obsidian 接入与读写】** 已有候选 `obsidian.png` 展示仓库范围和 Runtime 分配,尚需确认构建来源;正文主图优先补公开笔记的搜索、写入与回读结果。计划文件:`assets/07-obsidian-notes.png`。
>
> 建议图注:按仓库范围接入本机 Obsidian,在聊天中查找笔记并保存整理结果。
### 从输入区打开本地原生客户端
本地项目选择 Continue 或 OpenCode 后,输入框下方可以打开工作栏内的原生交互终端;选择 DS 后,可以在系统默认浏览器中打开官方 DeepSeek Harness Web 客户端。启动时沿用当前项目目录与所选模型,适合希望使用 Runtime 自身交互界面的用户。
原生客户端使用独立会话,不会自动接续或同步 GoodBuddy 中的聊天历史。DS 网页关闭后,本地服务继续运行,可以回到 GoodBuddy 重新打开或停止;退出应用会清理所属服务。
当前入口支持本地项目,远程原生客户端尚未接通。Continue 与 DS Web 需要可用的标准 Node 环境,首次准备环境可能需要下载。Continue、OpenCode 原生终端在 Ask 下可使用已分配的内置只读工具,包括故事图谱查询;自定义 MCP 仍仅在 Execute 下接入。
> **【待补图 08|建议|原生终端与 DS Web】** 已有 `continue.png`、`opencode.png`、`deepseekweb.png` 候选;发布前确认构建来源,并处理本机路径、浏览器书签及与演示无关的会话。Continue 图含模型能力警告,建议换用合适模型重拍;不能据启动画面宣称完整对话验收通过。计划文件:`assets/08-native-clients.png`。
>
> 建议图注:在工作栏打开原生终端,或在系统浏览器中使用 DS Web 的独立会话。
### Runtime 与模型可以分别跟随项目或临时切换
项目可以保留常用的执行方式与模型,某次会话需要换模型时,在输入区的两栏选择器中临时选择即可。菜单标明当前选择来自会话、项目还是全局,也可以恢复项目默认,减少每次新建会话重复配置。
执行方式与模型分别按“会话 → 项目 → 全局”解析,并检查模型是否适用于当前 Runtime。连接被删除或不兼容时按规则回退;缺少 API Key 时保留选择并提示配置,不会悄悄换用另一个模型。排队消息在实际发送时重新读取有效选择,定时任务则保留创建时确定的配置。
托管 SSH 项目支持 OpenCode、Continue;外部 OpenCode Server 使用服务端自身配置。
### 复制模型连接,保留已有配置
在“设置 → 模型连接”中打开一条 LLM 连接,点击“复制”,即可在副本中修改模型名称、地址或其他选项,再保存为新连接。协议、请求定制和认证配置随副本带入,已保存的 API Key 无需重新输入。
复制后仍需保存才会生效;当前最多保留 20 条连接。保存地址有误时,界面会指出对应连接并定位到地址字段,便于处理多条连接中的配置问题。
### Mermaid 图表可导出完整 PNG
聊天中的流程图或关系图可以打开大图窗口,再导出完整 PNG。导出内容不受当前缩放比例和拖动位置影响,适合插入报告、文档或演示材料。
图片沿用当前主题背景,最长边上限为 4096 像素。此功能用于 Mermaid 图表,故事线图谱的图片导出不在本轮范围内。
> **【待补图 03|建议|图表导出】** 在正常聊天中打开一张公开示例 Mermaid 图,执行 PNG 导出,并核对图片包含整张图。优先展示实际导出结果,图注注明它是导出图片。计划文件:`assets/03-mermaid-export.png`。
>
> 建议图注:从 Mermaid 大图窗口导出完整 PNG,保留图中的边缘节点。
### 让助手把会话图片保存到指定文件
在 Execute 模式下,可以让助手把当前会话中已经上传或生成的图片保存到指定绝对路径,缺少的父目录会自动创建。远程项目保存到对应 Host,方便后续在项目文件中使用;已有同名文件默认保留,明确允许覆盖后才替换。
保存支持 PNG、JPEG 和原格式 WebP,可按扩展名转换为 PNG 或 JPEG,不提供转为 WebP。工具使用当前会话已有图片,不负责下载任意图片网址;Ask 模式不提供写入操作。远程保存自 Agent 0.15.2 起提供,本轮建议使用上述最新配套版本。
### 桌面通知可以单独关闭
“设置 → 平台功能 → 通用设置”新增“桌面通知”开关,默认开启。关闭后,应用处于后台时的任务完成、失败或取消等系统提醒停止弹出,应用内通知仍然保留。设置保存后立即生效,重启后也会记住选择。
### 设备共享目录:登记设备,发布能力与知识说明
在应用中心启用“设备共享”,连接另行运行的 ShareServer 后,可以登记本机、浏览设备和发布目录,并发布或撤销能力、知识的元数据,供使用同一服务的人查看有哪些资源。
此功能默认关闭,当前提供目录管理。发布“知识”记录不会上传或同步正文,搜索、读取和下载标志仅表示声明;尚不提供远程执行或知识正文访问。设备登记也不表示设备在线,当前服务没有身份认证,应在可信环境中使用。
## 功能改进
### 会话搜索按需展开,快捷键提示集中在输入框
侧栏的搜索入口与“新建对话”放在同一行,点击后展开并自动聚焦,可查找会话标题或消息正文。切换会话保留关键词;点击关闭按钮,或在搜索控件行内按 Escape,可清空并收起搜索。展开和收起时,会话列表位置保持不变。
输入框的空白提示集中显示发送、换行、粘贴、新建对话及可用的快捷唤起按键,并按系统显示 Ctrl 或 Command。修改快捷键设置后提示随之更新;快捷唤起已关闭或注册失败时,不再显示不可用的按键。
侧栏底部的“应用”和“设置”保留文字并分开显示,应用菜单箭头与向上展开方向一致。会话列表独立滚动,短窗口中的入口仍可到达,滚动条和分隔线样式也作了统一。
### 知识库文档与来源集中浏览
知识库将内容来源与文档索引合并为统一列表。单文档来源合成一行,目录继续按组展示,导入失败、尚未生成文档的来源也保留恢复入口。搜索可以同时查找来源与文档,网址来源保留原网址,便于辨认同名资料。
列表突出查看入口,打开原文、分块管理、重新解析等操作收进更多菜单。解析结果页也整理了返回导航和图片操作,减少长名称、多个按钮挤在一行的情况。
任务历史改为按批次展示,可以展开处理步骤,查看进行中、失败与历史记录。筛选与计数跟随当前文档或来源,便于检查一次导入的处理情况。
### 图谱列表与画布各自浏览
故事线图谱侧栏可以按故事、经验、事件、实体和关系查看,列表与详情各自滚动。切换页签保留已有选中对象,在画布选中对象时,侧栏会切到对应类别。平铺与时间螺旋都使用中间画布的可用空间,视角等控件收进紧凑操作区,减少窗口较矮时内容被挤出屏幕的问题。
### 手动回顾与智能心跳共用增量进度
原有的每日、每周计划现在集中到“自动监督”。新建或编辑计划时,可以选择全局或多个项目,设置时间、时区、回顾窗口和历史保留期限;暂停的计划在编辑保存后仍保持暂停。
手动“回顾”和心跳计划现在共用增量处理方式,默认只分析所选范围内尚待处理的变化。项目与全局共用来源处理进度:同一版本的来源已成功发布后,换一个范围回顾可以复用,减少重复提取。没有新来源时仍可补做积压的故事与经验整理,没有待处理工作才跳过模型调用。需要重看完整区间时,可从“更多 → 重新整理”确认发起;重新整理不会推进或重置增量进度。
启用监督者不会自动创建计划。定时工作需要桌面应用保持运行,关闭监督者会停止发起新的回顾,并保留计划和历史;从导航取消常驻只改变入口显示。尚无活动时,自动监督页会隐藏空的统计、建议和历史区域,先展示计划入口。
监督分析会将入选内容交给配置的模型服务,可能产生费用。它以只读、无工具的方式分析记录,长时间范围可能分成多批;心跳不再额外生成一份独立报告。并发数和超时设置不等于总费用上限。
### 长回顾可以看进度、保留批次并继续处理
监督者的“活动记录”集中展示手动和自动执行情况。展开记录,可以查看当前阶段、已保存批次、剩余来源和失败原因,再按项目、会话、批次检查已保留事实与来源片段。
回顾会分页读取选定范围内的来源,将长内容拆成批次处理。成功批次保存后,后续阶段失败不会把这些内容一并丢弃;暂停或失败后可以继续。0.13.16 还取消了按固定累计时长自动暂停的行为,长回顾可以持续处理至完成,无需隔一段时间手动续跑。
需要调整处理方式时,可在“监督者 → 设置”中配置模型并发、单次模型超时、每次读取的来源条数、每批消息和文本量,以及单次响应容量。新建回顾会保存分批配置;来源已被修改或删除时,需要重新发起回顾。
> **【待补图 04|必补|回顾进度与来源批次】** 展开真实运行的活动记录,展示阶段、已保存批次和来源正文。若展示暂停后继续,须通过真实操作完成,不注入运行状态。图 01 的演示可复用同一份记录。计划文件:`assets/04-review-progress.png`。
>
> 建议图注:从活动记录查看处理阶段与已保存批次,展开核对来源。
> **【待核对 C01|暂停与取消配图】** `v0.15.0` 已包含 `5116613` 的暂停控件修复与取消入口,见末尾“回顾运行期间的控制”。截图须对应目标版本,展示实际暂停、继续和取消操作。
分页和批次记录便于核查处理范围,模型给出的总结、实体和关系仍需人工判断;来源处理完成不代表每个事实都被模型准确理解。
### 待办、任务与用量更便于浏览
魔法笔记的待办页可以拖动列表与详情之间的分隔条,调整后的宽度会保存。来源笔记标题支持独立折叠,收起一个分组不会清空正在查看的任务详情。分组折叠状态不跨重启保存,窄窗口下不提供横向拖宽。
笔记详情的记录索引与 AI 区开关集中在正文顶部的固定工具栏,图标带有悬停提示。任务中心展开列表后,可以直接选择“全部、进行中、暂停、已结束”;默认显示进行中的任务,历史任务仍可通过切换状态查看。
> **【待补图 05|建议|待办布局】** 用虚构笔记创建不同来源的待办,拖动分隔条并折叠一个来源组,保持另一项详情可见。计划文件:`assets/05-note-todos.png`。
>
> 建议图注:调整待办列表宽度,按来源笔记收起暂时不看的任务。
运行记录中的 Token 用量先显示项目或会话总计,展开后再看 Runtime 和模型明细,按模型分组的视图继续保留。“系统任务”页签单列监督回顾、笔记、知识库等后台用量,并补齐回顾、图片重新生成、知识图谱提取和远程嵌入等调用的用量记录;过去未记录的用量无法补算。页面打开时加载,可见期间每 15 秒刷新,也可点击页头手动刷新。这里汇总的是应用记录,并非服务商计费账单。
### 长对话输入、切换与流式回复减少重复处理
任务统计转到后台查询,隐藏的任务面板保留操作状态并减少不必要的更新。较长任务列表也减少了屏幕外卡片的绘制工作。
流式活动更新时,只保存变化的记录,并减少连续保存过程中反复读取整份历史的次数。改动针对工具活动较多、运行记录较长的对话,保留原有内容与顺序;实际响应改善取决于历史规模和设备,不以测试中的数据库指标代替整机提速幅度。
聊天输入和流式回复减少了无关活动面板的刷新;置顶或取消置顶会话时,也不再附带扫描附件引用。笔记启动检查复用已读取的正文信息,减少重复读取,原有内容恢复与清理流程保留。
长回答逐步生成时,已完成的 Markdown 区块可以复用,减少每次新增文字都重新处理整篇内容。切回近期看过的会话时也会复用缓存结果;会话侧栏与运行记录减少无关刷新,降低回复期间继续输入和切换会话的开销。
较长会话和大量会话的侧栏主要渲染屏幕附近的内容,历史记录仍然保留,可以继续加载和定位。切回缓存会话时恢复阅读位置;Markdown 解析结果也可复用,包含公式的长回复减少重复解析。
知识检索、故事图谱查询、会话搜索和会话列表等常用读取移到后台线程,减少查询期间对桌面主线程的占用。远程运行的连续事件按批保存,减少逐条写入的开销。改善幅度取决于设备、数据规模和内容复杂度,不作统一提速比例承诺。
### 设置页减少来回查找
长设置表单的分类标题与相关页签固定在顶部,请求 Header、Body 等低频选项收进高级设置。默认工作目录移到“平台功能 → 通用设置”,Skills 和 MCP 页面移除重复标题,保留导入与添加操作。
设置导航按用途分组,开关统一放在右侧。通用设置集中放置会话与通知、工作目录等选项;本地语音和嵌入模型页面提供“模型下载源”快捷入口,可打开统一弹窗,在 ModelScope 与 Hugging Face 之间切换。选择影响后续托管模型下载,已安装模型、ZIP 导入、Ollama 和应用更新不受影响。
外观设置新增透明磨砂效果,当前默认开启;标题栏和相关卡片使用半透明背景,可以看到其后的应用内容。可以关闭恢复实色显示,已有的外观选择继续保留。
内置浏览器 MCP 也改为默认启用,仍可在设置中关闭;之前明确保存的关闭状态会保留,不会修改系统默认浏览器。在 Execute 模式中,请助手通过配置工具应用本次请求的变更计划时,取消了额外一次原生确认;Ask 仍保持只读。
HTTP OCR 高级设置可以直接选择是否保留页眉、页脚、页码等内容,并恢复默认过滤方式。版面与生成参数的分组和间距也重新整理;具体选项效果仍取决于所连接的 OCR 服务。
成功和提示通知采用更紧凑的卡片,两秒后自动消失;错误提示继续保留,直至手动关闭。删除会话的确认框也去掉了重复警告,永久删除的含义不变。
## 问题修复
### 直连文本模型恢复手动压缩上下文
本地直连文本会话可以从输入区点击“压缩上下文”,即使关闭自动压缩或尚未达到触发阈值,也能手动执行。压缩使用配置的摘要连接,摘要随会话保存并用于后续对话,同时保留最近轮次和完整聊天历史;关闭自动压缩后,已有有效摘要仍可使用。
会话运行或正在压缩时需等待完成;没有可压缩的较早历史时会提示。这项修复适用于本地直连文本会话,不包含图像生成连接、远程通道会话或 SSH 项目。
### 长请求持续保留已授权的工具访问
修复桌面 Agent 请求持续超过十分钟后,工具授权提前失效的问题。请求仍在执行时,已启用并分配的知识库、笔记、浏览器等工具可继续使用;完成、取消或失败清理时仍会撤销该次授权。工具自身的调用超时、读写权限和开关设置保持不变。
### 取消与失败提示更清楚
取消和失败信息集中显示在消息状态中,减少与回答正文或工具详情的重复。主动取消使用中性提示,真实失败继续显示错误状态,已经生成的回答保留。
最后一条失败或取消消息有可恢复的原问题文字时,可以点击“重新编辑”将文字回填输入框,修改后自行发送;不会自动重发,也不承诺恢复附件。
取消或失败的轮次在后续上下文中明确标为中断,并保留已有回答,避免上一条未完成请求被并入新问题继续执行。OpenCode 复用会话在取消后的下一轮也会收到中断说明。
### 远程回答完成后,可以继续对话
远程请求现在等待模型交付确认后再结束。回答已经完成时,随后回收空闲 Runtime 失败不会再把结果改成“未知”。确实遇到结果未知的请求,用户显式发送新消息后会建立新的运行绑定并带入已有历史;自动恢复仍只重新连接原请求,不会擅自重复执行结果未知的任务。
最后一个活动任务结束后,远程空闲 Runtime 会被释放,下一轮对话再恢复历史。仍有任务或待答问题的共享进程会保留;更新后被替换的旧 Agent 会等待已有工作结束再退出,当前 Agent 保持常驻。
Host 更新还会清理已确认停用的旧 Agent 程序和归属明确的孤儿进程,保留会话历史、活动安装和状态不明的安装。这些完成状态与进程清理修复自 Agent 0.13.4 起提供;本轮建议配套 Agent 0.15.3,仅升级桌面不会直接替换 Host 上已经运行的旧进程。
### 启动、搜索与回顾结果
- 修复正常聊天或保存笔记后,重启反复显示历史迁移的问题。普通数据库结构升级也不再因正常空闲页额外整理整库;确需转换的旧数据仍会处理,启动页区分实际操作。
- 修复本机直连模型文件搜索参数受限和大结果截断的问题。模型可使用 ripgrep 原生参数,保留原生结果与退出码,并分页续读长输出。Ask 保持工作区只读,Execute 使用当前账号权限;这项变更不属于远程 Agent 或其他 Runtime 的新增工具。
- 修复早期监督回顾只取部分会话、消息和正文的问题,改为对入选来源分页、分批处理。事实与来源独立保存,只有必要阶段完成并成功保存结果后才标记完成。
- 修复监督回顾中的旧实体引用、摘要合并校验及长摘要报错。遇到无效 JSON、结构或引用时,会尝试缩小当前批次后重试,剩余正文留待后续处理;成功保存的批次保留。网络错误、超时、取消和超出响应容量不走这套重试。
- 故事或经验整理失败时,已保存的回顾结果继续可用,可以单独重试整理。故事归类减少把各功能的提交与测试统归到“发布”的情况;经验应用记录也增加时间顺序与共同实体核对,归纳内容仍需结合来源判断。
- 时间螺旋退出时释放图形资源;3D 初始化失败或图形上下文丢失时,在视图内提示并提供重试,也可切回平铺,避免整个监督页面无法使用。
- 修复历史回顾入口跳到其他结果、切换后旧响应覆盖当前选择的问题。侧栏、活动记录和图谱按对应结果定位,人工确认与修订的内容保留。
### 日常界面操作
- 多个会话同时回复时,侧栏列表不再随流式文本或工具进度反复换位。会话按最新消息的创建时间排序,每五秒检查一次;悬停、滚动后的短暂停留、菜单展开或键盘焦点位于列表时暂缓重排,方便点选。置顶会话继续优先显示。
- 输入框的附件、语音与选项按钮统一尺寸、间距和悬停反馈,录音中和选项展开时仍保留对应状态提示。
- 草稿和排队消息中的图片可以直接打开预览,发送前更容易核对附件。文本输入框移除重复问候,只保留快捷键提示。
- 展开 Runtime 检查清单不再挤动聊天内容;文件列表和差异页的操作栏在滚动时保持可见。
- 笔记保存后退出编辑;从快速笔记跳到指定记录时,等待正文显示后再定位,减少落在错误位置的情况。
- 切回已查看的会话时,立即显示缓存的累计回复时长,再后台刷新;缓存限本次应用运行期间。
- Mermaid 大图放大后,顶部和左侧等边缘仍可通过滚动、拖动到达,窄窗口中的控件可以换行。
- Git 分支选择器悬浮在工作区上方,不再把文件列表和提交历史向下挤动;项目选择菜单可以浮出侧栏,长名称与路径有更多显示空间。
- 文件差异页将查看文件、刷新和复制路径集中到路径旁,窄面板中可以换行;只有未暂存改动时,省去重复的分区标题。浏览器“前往”按钮改为紧凑图标,保留悬停提示。
- 聊天与浏览器、终端工作栏之间的分隔条恢复完整拖动区域;笔记缩略图与内容边缘对齐,键盘焦点轮廓保持可见。
- 纯文本排队消息不再显示无效的“查看附件”按钮;浏览器页签关闭失败后重试成功,会清除残留错误。
- 监督反馈显示会话或任务名称,固定与刷新按钮在窄侧栏中排列更紧凑。工作回顾和活动页使用可用宽度,运行阶段与已保存批次更醒目。
### 运行配置与界面修复
- OpenCode 原生终端减少重复准备步骤,缩短打开时的等待;临时 MCP 工具名缩短并在同一会话中尽量复用,减少工具名称过长导致的调用失败。
- 本地 OpenCode 初始化工具连接时,遇到明确的暂时性传输故障,清理成功后最多重试一次,不重放问题或工具调用。迟到的旧连接结果不再替换当前连接;自定义 MCP 中一台服务加载工具失败时,其他正常服务仍可使用。
- 修复 Continue 禁用自动更新后仍显示“Checking for updates”的问题,避免空闲终端显示误导性的更新状态。
- 补齐 DS Web、远程 Runtime 和模型桥辅助进程启动时的遥测关闭配置;模型、MCP 和用户主动发起的联网请求保持可用。远程遥测配置与 Continue 更新检查修复由 Agent 0.15.0 配套提供,对更新 Host 后新启动的 Runtime 生效,已有进程不会热更新。
- 修复知识库不同来源的操作相互阻塞,以及多文档来源中任务范围、筛选计数不一致的问题。
- 修复深浅主题中危险操作按钮、禁用状态、控件边框和键盘焦点显示不一致的问题,包括放弃笔记草稿的确认框。
- MCP 编辑器和图片预览可以在父弹窗上正常操作;项目弹窗的 Escape 只处理当前弹窗,帮助层覆盖浏览器时不再被原生页面遮挡。
- HTML 全屏预览恢复键盘进入与 Escape 返回,继续保持原有沙箱限制。
- 窄工作栏的添加按钮不再被滚动箭头遮挡,模型连接协议文字保持可读;DS 入口移除常驻说明,重新打开和停止服务操作继续保留。
### 回顾运行期间的控制
回顾运行期间,范围、时间选择和刷新入口继续可用;修改选择不会改变已经启动的回顾。重复点击发起按钮会提示已有运行,避免同时创建重叠回顾。
活动记录增加取消正在运行回顾的入口,并修复继续回顾后无法再次暂停的问题。暂停保留进度供继续处理;取消会保留已成功保存的批次,但该次回顾不能再继续。取消清理结束前显示“正在取消”,未完成结果不会发布,之后可以新建回顾。
手动发起、继续与自动回顾共用执行限制;自动回顾在忙碌时等待,手动操作会提示当前已有运行。同一次回顾内部仍可按设置并行处理批次。
> **编辑提示:** 发布前补齐生产截图,替换故事线概念图,并核对下载地址、发布日期和更新日志链接。以上提示属于编辑流程,不影响本文所述功能已完成的状态。
@@ -1,5 +1,89 @@
# 0.13.8 起综合宣传稿核查记录
## 2026-10-09 宣传版重排
本轮按宣传重点重写正文,将故事图谱、Telegram 和 Obsidian 接入作为三条主线;底部输入区、Runtime、笔记、性能和稳定性改进合并到“其他改进”。当前版本内所述功能均按已完成能力表述,发布前的待办只保留生产配图、公开下载信息、发布日期和更新日志链接,不再把功能验收材料写成开发状态。
正文保留必要的使用范围和升级提示,提交依据、实现边界与历史变更继续留在本核查记录中。原正文中的概念图仍只作示意,发布前需要用目标版本的生产截图替换。索引已同步更新,未修改产品代码。
早期核查中关于 Telegram 真实联调的 C02 标记属于当时的验证记录,不再作为当前正文的功能状态描述;本轮按用户确认,将 Telegram 作为当前版本已完成能力介绍。发布前仍需补齐公开演示素材和发布信息。
## 2026-10-09 截至 3556df88 的增量核查
本轮固定范围 `47917094..3556df88`,共 5 个提交;全文范围为 `v0.13.7..3556df88`。`v0.15.13` 与 `agent-v0.15.6` 均指向 `3556df88`。本轮没有新增独立宣传功能,连续提交合并回已有的输入区 UI 与 Runtime 修复说明。
| 提交 | 入文内容 | 核查依据与边界 |
| --- | --- | --- |
| `69f64df2`、`4b4531ab` | 补强“底部输入区工具栏更紧凑” | 恢复 Composer 弹层与工具入口的集成,保留打开后聚焦第一个可用控件、外部点击或焦点离开关闭、Escape 返回触发按钮焦点的最终行为;测试夹具调整不单独宣传。 |
| `adb58d92`、`3556df88` | 合入 Runtime 网络请求修复 | 本地模型请求可通过 Electron 的 `net.fetch` 发送,监督模型连接也沿用该请求路径并保持既有配置校验;不扩大为所有网络故障已解决或跨平台安装包验收完成。 |
| `d7fb4299` | 仅留内部验证依据 | 使用真实 fetch 的监督模型覆盖测试,不写成当前轮次已执行的产品测试或真实模型验收。 |
本轮只更新宣传正文、核查记录和索引,未修改产品代码。未运行产品测试、构建、真实模型请求或拍摄新截图;生产配图、Telegram 真实联调和公开下载信息继续待核验。`deai-writing` 扫描结果沿用上一轮为阻断项 0、复核项 19;`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
## 2026-10-08 截至 47917094 的增量核查
本轮固定范围 `384683e8..47917094`,共 23 个提交;全文范围为 `v0.13.7..47917094`。Desktop/Agent 版本依据仍为 `0.15.13`/`0.15.6`,本轮没有新的版本准备提交。正文按用户场景合并了表格编辑、专家与 Runtime 控制入口;输入区、历史导航和存储测试相关的连续提交没有拆成多个宣传条目。
| 提交 | 入文内容 | 核查依据与边界 |
| --- | --- | --- |
| `3d063937` | 新增“在魔法笔记中插入和编辑表格” | Quill 原生 table Delta 经过编辑器、共享契约、IPC、存储和只读渲染链路;支持设置行列、行列增删和整表删除,表格上限 20 行/12 列。空单元格表格可保存并重新打开,空白富文本仍拒绝保存。 |
| `13b58627`、`1a59c2d8`、`670b5985` | 新增“在当前会话中使用专家角色和 Runtime 控制”,并合并底部输入区 UI 改进 | 专家和 Runtime 控制收进 Composer 工具栏。选定专家或智能路由得到的角色说明加入当前 Runtime 的受信任提示词,沿用当前会话的模型、工具、流式输出和权限;普通对话不转为额外子任务,团队模式仍走独立团队路径。上下文来源共用一个入口,OpenCode/Continue 的控制项进入独立弹窗;点击外部区域或焦点离开时关闭,Escape 返回触发按钮焦点,运行控制弹窗移除多余分隔线。 |
| `e2a5f836`、`47917094`、`4266ff5`、`4a2ea490` | 仅记录历史导航方案的收回和文档整理 | `e2a5f836` 发布了聊天历史导航文档,曾尝试取消向上滚动的 80 条增量限制;`47917094` 恢复批量历史加载。当前仍保留历史窗口和约 60 条 DOM 窗口化,不能宣传为已实现连续滚动或 minimap。用户指南和 README 截图整理不单列为功能。 |
| `377b14fc`、`a406f596`、`7f0a73e6`、`2d68d104`、`277da413`、`e1360264`、`59a28209`、`9788b4cd`、`c90ad2af`、`95ece25c`、`c4ab83a7`、`7eb11302`、`5ba127e3`、`2a92f8f8`、`95f44195` | 仅留内部验证依据 | 存储进程取消、排空、崩溃恢复和测试夹具稳定化,不写入宣传正文,不据此扩大存储进程故障恢复或跨平台验收结论。 |
本轮只更新宣传正文、核查记录和索引,未修改产品代码。未运行产品测试、构建、真实模型请求或拍摄新截图;提交中已有的表格、聊天历史和存储测试记录只用于核对实现边界。生产配图、Telegram 真实联调和公开下载信息继续待核验。deai-writing 扫描结果为阻断项 0、复核项 19、读取错误 0;复核项集中在版本、素材、能力边界和必要否定条件,未机械删除。`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
## 2026-10-08 截至 384683e8 的增量核查
本轮固定范围 `e50ca6d1..384683e8`,共 22 个提交;全文范围为 `v0.13.7..384683e8`。版本资料已推进到 Desktop 0.15.13、Agent 0.15.6;HEAD 位于版本准备提交之后,最后一组提交主要是存储故障验收夹具和测试稳定化,不另列为产品功能。正文仍按新增功能、功能改进、问题修复组织,并将连续的架构与性能改动合并为一节。
| 提交 | 入文内容 | 核查依据与边界 |
| --- | --- | --- |
| `d80d42b0`、`8473189f`、`904bb691`、`1f85d0c0` | 合并为“存储与请求路径合并,减少主进程等待” | Desktop SQLite 的 Assistant、Knowledge、附件、Runtime 绑定和用量记录改由同一 storage utility process 负责;查询、文件操作和回顾来源读取共用类型化异步调用。同步保留事务、提交确认、取消、关闭排空和故障报告边界。移除无依据的产品侧 admission、子任务和解析 worker 队列上限,不宣传为全量并发或跨平台性能验收;存储进程故障后未确认写入仍需按域核对。 |
| `13c29bad`、`c1e4bbf6`、`3e3a0216` | 更新魔法笔记标签节 | 标签管理器新增直接创建标签;保留空标签集合,重复标签错误保留原始原因。正文不扩大为自动打标签或取消每篇最多 10 个标签的限制。 |
| `e0607a15`、`2be57492` | 合入回顾结果修复 | 支持短引用和已知历史键映射;删除会话后回顾可继续查看或处理,暂停、继续、重试及项目范围规则仍有效。 |
| `56b8b10d` | 新增直连模型工具失败后的继续对话说明 | 工具执行失败不会阻塞后续直连模型消息,分页输出边界、工具权限、取消和超时规则保留。 |
| `425accf4`、`4bb7573b`、`edf019aa` | 合入日常界面改进 | 直接进入设置页时预加载目标路由;工作区文件列表元数据和聊天输入区过渡更简洁。均不单列为新能力。 |
| `b90ff50f`、`36e671a0`、`fbfafa15` | 更新版本与监督稳定性依据 | 监督任务交付和停顿修复并入既有监督修复范围;版本准备资料为 Desktop 0.15.12/0.15.13、Agent 0.15.5/0.15.6。 |
| `42ad8a98`、`4c25e85b`、`7eb51bd5`、`9212fd43`、`384683e8` | 仅留内部验证依据 | 存储故障验收、权限夹具、容量断言和测试竞态稳定化,不写入宣传正文,也不据此宣称完整 C01–C07 或跨平台性能验收完成。 |
本轮未修改产品代码,仅更新宣传正文、核查记录和索引。未运行产品测试、构建、真实模型请求或拍摄新截图;本轮提交中的测试记录只作为静态核查依据。生产配图、Telegram 真实联调和公开下载信息继续待核验。deai-writing 扫描结果为阻断项 0、复核项 18、读取错误 0;复核项均为版本、素材、能力边界或模型结果限定,未机械删除。`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
## 2026-10-05 截至 e50ca6d1 的增量核查
本轮最终范围 `9527402..e50ca6d1`,共 60 个提交;全文范围为 `v0.13.7..e50ca6d1`。初次固定至 `bbf24c28` 的 58 个提交,收尾补查新产生的两项提交。本地桌面 `v0.15.11` 与 Agent `agent-v0.15.4` 同指向 `78afec77`,上一桌面 `v0.15.10` 指向 `ff61da28`。Telegram、自定义回顾日期、历史耗时缺失提示及最后一组运行修复在 0.15.11 标签之后,均已提交,不将其混入该标签,也不使用开发状态标签。以下取代旧节的当前状态描述,旧节仅作历史依据。
沿用用户确认的标题与综合稿结构,继续按新增、改进、修复组织。Telegram 与文件导入各设短节,统一执行和项目菜单放在改进段前部;监督、图谱、性能与统计合入原有章节,避免重复介绍。打包、构建、测试及纯架构维护不进入宣传正文。
| 提交 | 入文内容 | 核查依据与边界 |
| --- | --- | --- |
| `b4df493b`、`e50ca6d1` | Telegram 首次配置顺序、监督临时失败重试 | 已提交中英文通道文案,先空白 ID 启用保存,再 /whoami 获取自己的 ID,测试连接不启动收信。`supervision-production.ts` 的包装层最多额外一次临时失败重试,429、部分 5xx、明确网络/流中断;排除认证、配置、额度、超时、取消、容量错误。等待可取消,失败用量保留,每次尝试独立超时;不扩大为所有 Runtime 全局重试或严格最多两次 HTTP 请求。替换旧稿网络错误不重试的表述。历史 177 项通过/1 跳过为假 fetch 和 SQLite 验证,非本轮模型实测 |
| `cf598121`、`bbf24c28` | Telegram 私聊、原样保存重连 | 已提交 `telegram-channel-driver.ts`、`ChannelSettingsSection.tsx` 与 Telegram PRD/progress。自建单 Bot、数字用户 ID 白名单、通道项目、普通私聊文字输入与最终文字回复,系统代理及恢复;桌面常驻。没有群聊、媒体输入或附件发送,真实 Bot+模型完整联调仍待完成,正文标注 C02 |
| `56ebd40b` | 统一执行,删除旧 Ask/Execute 限制 | `model-runtime.ts`、Runtime 接线与 `unified-execution/logic-design.md`。按所选 Runtime、能力及账户权限执行;专用文本分析无工具,各后端支持范围保留。同步修正图谱、Obsidian、原生终端、图片保存、配置工具、继续讨论及文件搜索旧段落 |
| `e89db96b`、`658e7f10`、`56ebd40b` | 项目与会话统一菜单 | `WorkspaceMenu.tsx`、`workspace-menu-selectors.ts`。打开默认全部项目,分类搜索、悬停预览、最近 10 个会话与活动、归档恢复、返回上次会话;已完成筛选不受最近 10 条限制。不沿用旧 UI 文档中默认仅预览当前项目的描述 |
| `56ebd40b`、`49fd0d30`、`ee186f9f` | 文件导入、时间排序、项目目录 | `workspace-import.ts`、`WorkspaceFilesPanel.tsx`、项目契约及 SSH 目录浏览。本机文件导入本地/远端,同名保留,单项失败继续;远程要求 Agent 0.15.4,每目录排序限已读最多 500 条。新本地项目必选目录,远程支持目录符号链接。成果页退出但记录保留,不自动将历史成果复制到目录 |
| `48d7526b`、`2c2b6c31` | 监督专用文本模型与会话图谱开关 | `HeartbeatCenter.tsx`、`supervision-production.ts`、`Composer.tsx`、IPC 和原生客户端。监督模型为全局设置,未指定跟随默认,失效或缺密钥报错。会话开关缺省允许查询,仍受总开关与分配约束;关闭不排除会话被回顾、不删除记录,开启不自动注入上下文 |
| `126dbc57` | 自定义最近天数与起止日期 | `SupervisorWorkspace.tsx` 日期计算和提交路径。最近天数按每 24 小时计算;指定日期按本地日界,包含结束日,今天截至提交时刻。普通回顾仍增量,需要重新分析时选择重新整理 |
| `577210a8`、`b9186a61`、`ea3d607f` | 共享历史选择、保留未归属事件、螺旋配色与密度 | `story-graph-3d-model.ts`、`StoryGraph3D.tsx`、数据库选定回顾查询和监督界面。所选历史事件与当前故事关系分开;主故事颜色仅覆盖实际事件区间,空档中性;半径按小时消息量和圈时长归一,不代表工时、生产力或小时内分布 |
| `4d18794e`、`d0877d92`、`0cd5da33`、`75e50fc1` | 运行耗时、缺失提示、OpenCode 命中率及会话汇总修复 | `assistant-database.ts`、`use-execution-stats.ts`、`token-usage.ts`。模型与工具执行累计,等用户暂停,并行相加;旧历史不补算,缺失零与完整零区分。OpenCode 分母含 input/cacheRead/cacheWrite,重新查询校正,不宣传缓存性能提高;去除前 5,000 会话统计限制 |
| `10c51628`、`f4458afe`、`b2eb86a0`、`68cc0792`、`010aafb6`、`41be9f80`、`2e3d2e77`、`651475d1`、`492f1d20` | 文档后台解析、终端合批、列表窗口化、启动与会话刷新 | 文档 worker、终端服务、窗口化列表、Composer、KeepAliveRoute、任务 store。描述减少重复处理的具体场景,不给通用倍数,不宣称所有数据库操作已异步化;任务/成果 store 抽取不单独计新功能 |
| `6aab78b9`、`352da66d`、`bf9850a6`、`0dd4d1d6` | 活动记录增量保存与分页、数据库处理 | 活动 store/sync/repository、只读 worker、数据库迁移。Renderer 不再持有整份历史,后台汇总计数,退出裁剪页而不删历史。高频本地 NORMAL 写入的断电持久性边界已加入升级说明,关键生命周期/远程确认等仍保留 FULL |
| `805acc3f`、`a620c226`、`bbf24c28` | 长列表阅读状态、笔记抽屉、消息展开保留 | `message-expansion.tsx`、聊天/笔记窗口化与终端路径。展开状态只在历史面板存续期保留;跨屏选择复制、滚动锚点、跳到底部和窄屏笔记入口改善。不承诺跨重启恢复展开状态 |
| `bbf24c28` | Runtime、监督、通道故障修复 | `agent-owned-acp-prompt.ts`、`protocol-server.ts`、`opencode-runtime.ts`;远程启动可取消且不阻塞其他会话,本地提交失败及时结束。`supervision-production.ts` 修复旧记录提及的 batch 未转发,发布前暂停与故事目的地校验;旧请求释放后控件可用。通道图片结果校验、钉钉业务错误进入重试。Telegram 仍不发送图片。远程修复需包含此提交的 Agent |
| `629e8254` | PPTX OCR 错误来源与位置 | 解析路径及 OCR 错误诊断;实际服务、幻灯片与图片位置。外部服务 HTTP 500 本身未因此修复 |
| `55f554d1`、`2adee136`、`78beade2`、`29a134b8`、`74efbcdc`、`0e9c9d49`、`b28ddd4b` | 控件、焦点、DS 操作及设置布局 | 合入界面简述;工作栏箭头不遮关闭按钮,停止确认同行。间距、代码抽取、缓存预算不逐项包装成新能力,b28ddd4b 中上一稿文档不重复计新功能 |
| `ae4c6d24` | 旧 Runtime 缓存清理,仅内部记录 | 属于磁盘维护,按用户偏好不写入功能宣传,也不当作通用提速结论 |
| `9d03217f`、`10065da8`、`942fe320`、`39fc15ef`、`59d36c22`、`91edcf5e`、`e205e909` | 架构、性能报告和官网同步,仅内部依据 | 性能报告涵盖部分区间外工作,受控测量不能整体归因本轮;官网改文案不列桌面新功能 |
| `739f2670`、`6b634c5d`、`5d0ca44b`、`273a7fa7`、`362fe504`、`ff61da28`、`245e296a`、`29af4090`、`4e72aa04`、`78afec77` | lint、测试与发布维护 | 用于版本与历史验证依据,不进入宣传正文 |
核查终点数据库 schema 61,Runtime 设置 22,应用设置 12,通道设置 3;0.15.11 标签数据库为 60。升级提醒保留完整用户数据、设置、数据库/WAL 和 notes 成套备份,说明大库转换等待与旧客户端回退限制,不承诺固定空间回收比例。版本号文件仍为 0.15.11/0.15.4,不据此推定后续提交已发布。
开始时工作区已有 Telegram 技术设计和两份 i18n 修改,核查期间又出现其他源码修改;均保留,不作为本次已提交依据。故事图谱检索得到自动归纳的宣传稿经验,提示区分现有能力与预览;实际内容以本次 Git 和生产源码核对为准。
本轮只做静态事实核查与文档验证,产品测试、构建、真实模型请求和新截图均为 0。提交中的 6136 测试通过、7 次模型调用及 Linux Host 验证属于既有记录,不写成本轮执行;Telegram 完整真实联调另有缺口。现有原型图保持示意标注,生产截图和公开下载信息仍待核验。未新增图片链接,未提交、推送或对外发布。
独立复核修正了旧稿“任务统计转到后台查询”的过时机制说明,改为当前的按运行状态累计。deai-writing 扫描阻断项 0、复核项 18;保留必要的版本、升级、素材与功能边界说明,以及模型理解能力的限定。文档差异检查通过,仅有既有 LF/CRLF 行尾提示。
## 2026-10-03 截至 9527402 的增量核查
本轮固定范围 `b02ce31..9527402`,共 45 个提交;全文范围为 `v0.13.7..9527402`。桌面 `v0.15.9` 与 Agent `agent-v0.15.3` 同指向 `9527402`。沿用用户确认的标题和综合稿结构,直接更新已授权草稿;未在线核验公开安装包和下载信息。以下记录取代旧节中的当前状态描述,旧节保留为历史依据。
+1 -1
View File
@@ -386,4 +386,4 @@ Agent 专题使用对应的 `agent-v…` tag。
| 日期 | 目标 tag | 标题 | 状态 | 正文 | 发布链接 |
| --- | --- | --- | --- | --- | --- |
| 2026-09-19 | 未确定(unreleased) | GoodBuddy 更新预览:画布笔记、文档处理与应用管理 | 草稿,截图待补 | [综合更新稿](./2026-09-19-unreleased/article.md) | 未发布 |
| 2026-09-26(10-03 更新) | v0.15.9(9527402);汇总 v0.13.8 起的变化 | GoodBuddy 更新:故事图谱、Obsidian接入、Runtime原生终端 | 草稿,已补故事归类、时间螺旋、经验与心跳建议、助手按需检索、图片保存和长会话性能;原概念图待换为生产截图,公开发布信息待核验 | [综合更新稿](./2026-09-26-unreleased/article.md) | 未发布 |
| 2026-09-26(10-09 更新) | Desktop 0.15.13/Agent 0.15.6 及之后截至 3556df88;汇总 v0.13.8 起的变化 | GoodBuddy 更新:故事图谱、Telegram 与 Obsidian 接入 | 草稿,按故事图谱、Telegram、Obsidian 三条主线重排;其他功能合并为辅助改进,生产配图及公开发布信息待补 | [综合更新稿](./2026-09-26-unreleased/article.md) | 未发布 |
+1
View File
@@ -5,6 +5,7 @@
| 文档 | 职责 |
| --- | --- |
| [Electron UI 自动化测试规范](./electron-ui-automation.md) | 测试分层、当前源码启动、视觉与原生交互取证、真实调用计数、脱敏报告和清理 |
| [桌面并发架构验收](./concurrent-desktop-acceptance.md) | 一次完整架构交付的多项目/会话/子任务负载、计时口径、正确性断言、平台与验收记录要求;当前候选仍有未完成项 |
| [复发缺陷与防复发规则](./recurring-defects.md) | 已重复出现的缺陷类型、对应的共享机制和自动发现测试 |
| [开发与构建](../../BUILD.md#质量验证) | 依赖、测试、类型检查、Lint 和构建命令 |
| [UI 设计系统](../../UI-DESIGN.md#155-验收) | 全局视觉、交互和响应式验收要求 |
@@ -0,0 +1,175 @@
# 桌面并发架构验收
## 状态与职责
2026-10-05,设计基线为 `2be57492`。本文定义一次完整架构交付的验证要求,测试脚本与结果尚未完成。
目标架构见[桌面存储与并发执行](../architecture/desktop-storage-direction.md),规范和数值预算以
[性能原则](../architecture/performance-principles.md)为唯一依据。既有 KISS 测量只作为基线证据,不能替代本矩阵。
验收对象是同一候选中的完整产品路径,不单独验收“接口改成 Promise”“Worker 可调用”或“单会话很快”。
所有必测场景完成前,不标记 PERF-15/PERF-16 为完成;未测平台及指标明确列出,不以跳过视为通过。
## 发布门槛与目标差距
2026-10-06 起按两类判定,沿用既有“历史回归上限”与“性能预算”的分层,不新增判定体系:
| 类型 | 内容 | 发布判定 |
| --- | --- | --- |
| 正确性与稳定性 | A01–A10;请求排空、无静默丢弃、提交后 ACK、取消结算、故障不传染、资源回落、进程退出后可恢复 | **必须全部通过**,不能用“比旧版好”替代 |
| 性能对照 | 同机、同数据、同负载,与改造前提交交替三轮;含单会话简单操作和后台吞吐 | **不得退化**:候选不差于基线的重复测量波动范围 |
| 性能预算 | 性能原则第 1 节的绝对数值 | 未达到时记录为目标差距,不阻断本次发布 |
“不得退化”同时覆盖单会话路径。存储迁到独立进程后多一次 IPC,单个简单操作最可能变慢,只测并发场景会漏掉这种退化。
后台吞吐、回顾发布和事件提交也要对照,界面流畅但后台明显变慢不算改善。
## 固定负载
以下为验收档位,不是已验证的容量承诺,也不为产品新增并发限制。当前产品若不能达到某档位,记录实际原因;不能用事先写入的内部上限代替测量。
使用本地确定性模型与隔离目录,每个输入、事件和工具结果有稳定标识,能够逐项验证顺序及重放结果。
不得用真实提供商的网络速度作为应用性能基准。
本地临时库、日志和源码测试产物遵守仓库 [AGENTS 临时文件规则](../../AGENTS.md#local-temporary-files),
使用仓库根目录 `temp/` 的任务子目录并在关闭资源后清理;可复用夹具提交到 `tests/` 或 `scripts/`,
正式验收摘要记录在文档中,不依赖唯一一份临时结果。远程临时目录沿用 Host 专用规则。
| 场景 | 项目/并行会话/同时活动子任务 | 必须同时执行的用户动作 |
| --- | --- | --- |
| C01 日常并行 | 3/4/8,子任务为全场景总数 | 持续打字、跨项目切换、查看来源和工具详情、首次进入监督者 |
| C02 并发压力 | 4/8/16,子任务为全场景总数 | 滚动长历史、搜索、切换缓存会话、取消其中一个父任务,其他会话继续 |
| C03 混合存储 | C01 加 100 批次回顾发布、知识导入和检索 | 前台输入与保存,打开图谱,观察远程事件提交/ACK;各操作必须真实重叠 |
| C04 运行时故障隔离 | C01 中挂起一个启动、模型路由准备或清理 | 查询其他任务、取消故障请求、继续健康 peer;覆盖本地及受影响的远程路径 |
| C05 存储故障与饱和 | C01 中注入读 Worker 失败、队列达到上限、存储进程退出 | 页面报错与重试,已提交结果可重读,未确认写入不盲目重放,UI 仍可响应 |
| C06 持续生命周期 | C02 连续 5 分钟,每轮完成、关闭并重开任务/会话 | 保持草稿、展开、焦点及滚动状态,观察队列、缓存、订阅和所属进程回落 |
| C07 冷热进入 | 新进程首次打开及同进程暖返回,分别在空闲和 C01 下测量 | 输入框、监督概览、图谱、历史选择、来源正文分开记录可用时刻 |
默认数据为 300 个已保存会话、每个 20 条消息,另加 2,000 条消息的长会话;压力数据改为 10,000 条长会话消息。
监督数据沿用可复现样本:20 个结果、40,000 条消息、8,000 事件/来源、400 故事、800 经验;消息及来源正文各 2,048 ASCII 字节。
回顾发布为 100 批、每批 20 来源,知识检索保留既有 5,000/20,000/50,000 文本块样本。各场景只装入相关数据,不把互不相关的大样本无条件叠加。
活动流初始使用每会话每 16 ms 一个文本片段、每父会话每秒一个有界工具/子任务更新;工具输出固定为 4 KiB,实际数量、字节数和时间表写入 fixture。
每个会话的首次响应、持续流、完成和清理都要经过生产路径。调度抖动时记录实际发送率及迟到分布,不能丢弃片段以保持设定速率。
若现有 Runtime 会自然合并事件,分别记录生产与消费数量,按可还原内容验证等价。
## 计时和判定
| 指标 | 起止与用途 |
| --- | --- |
| 输入/点击反馈 | 原生输入事件至目标反馈后的帧机会;记录方法局限,不把两帧等待称为纯渲染时间 |
| 内容可用 | 请求发起至所需正文/图谱/来源提交到界面;加载占位出现不能替代该指标 |
| Main 阻塞及 p99 延迟 | 连续事件循环采样,排除夹具生成和数据库初始化;与 Renderer 长任务分别记录 |
| 存储排队 | 入队至执行开始,记录操作类型、项目/会话归属及队列深度,不记录用户正文 |
| 执行/提交 | 存储执行开始至结果或事务提交;完成响应不能早于承诺的提交点 |
| 取消/清理 | 分别记录请求取消、执行结算、资源释放;UI 清除等待不等于清理完成 |
| 吞吐和积压 | 固定 offered load 下完成数、字节数、队列高水位及输入停止后的排空时间 |
| 内存与进程 | 全应用及各实际 PID 的工作集/堆;记录缓存条目、监听器、连接及 worker 数 |
标准机和 Renderer 4× 降速分别报告,UI 与 Main 的绝对预算引用性能原则第 1 节。
Renderer 降速不等于模拟慢磁盘、慢 Main 或真实低配整机,不能从该结果推断它们合格。
原有 `perf:check` 回归上限比规范宽松;即使上限检查通过,违反规范仍须报告失败或明确的未满足项。
存储排队、吞吐、内容就绪和取消清理不得被 UI 流畅掩盖。每份 fixture 在测试实现时固定最大等待/排空期限,
使用既有产品期限或改造前重复测量支持的验收值,先记录依据再运行候选;禁止测完候选后放宽期限。
没有期限、样本不足或未采集的必需指标不能记为通过。固定输入下吞吐不得低于基线的重复测量波动范围,
队列不得持续增长,有限后台任务在持续前台负载下必须完成。容量溢出按设计等待或明确失败,不能静默丢事件。
每版本至少三轮交替运行,基线从独立 worktree 获得。交互延迟每轮每个指标至少 100 个有效样本;
首次进入通过独立启动样本报告各次值,不用三次启动估计 p95。持续负载场景先预热 30 秒,测量至少 120 秒;
C06 单独执行 5 分钟,足以观察多轮创建、完成后的资源趋势;需要更长时间时另行说明理由。记录 OS、CPU、内存、磁盘、Electron/Node、提交、数据校验值、并发、缓存状态和后台负载。
无新增数据的重复生命周期测试中,逻辑缓存/订阅/连接应回到约定存续范围,队列排空后为零,结束的所属子进程退出。
RSS 未立即回落不能单独判为泄漏;需结合存活对象、资源计数和重复轮次趋势。不能通过杀掉健康共享 Runtime 达到清零。
## 正确性断言
| ID | 断言 | 对应架构要求 |
| --- | --- | --- |
| A01 | Main 不直接或间接打开业务 SQLite;正常、失败、饱和、维护及退出路径均无同步回退 | SA-01、SA-06 |
| A02 | 同会话事件不丢、不重、不乱序;不同项目/会话/子任务的结果、取消和凭据不串用 | SA-03、SA-05 |
| A03 | 远程事件、checkpoint 提交后才 ACK;崩溃后重放去重;发件箱与模型账本不重复执行 | SA-02、SA-05 |
| A04 | 事务失败整体回滚,提交后立即读取可见;回顾结果与 checkpoint 同步发布 | SA-02、SA-05 |
| A05 | 取消排队请求不执行,取消在途请求等待实际结算;迟到结果不污染新请求,健康 peer 不受影响 | SA-03、SA-06 |
| A06 | 关闭前先停止新写入、排空必要写入、关闭连接;替换存储进程前观察旧进程退出 | SA-06 |
| A07 | 多项目切换不丢草稿、展开、选区、焦点和滚动;后台任务继续,切回不全量重建历史 | SA-04、SA-05 |
| A08 | UI DOM 随视口有界;缓存和队列有清理;未访问正文不随摘要列表整包返回 | SA-04 |
| A09 | 已发布数据库升级保留用户内容、ID、顺序、引用、去重与落盘语义,不静默换空库 | SA-02、SA-05、SA-06 |
| A10 | GoodBuddy 中转无非必要落盘;完整输出复用唯一正文,分页和释放正确,必要临时文件及历史残留按归属清理 | SA-04、SA-05、SA-08 |
故障注入覆盖提交前、提交后但响应前、初始化期间和退出期间。使用现有业务 ID 查询实际结果;
不得为了让测试容易恢复而引入新的持久化 receipt 或盲目重放。强杀共享存储进程仅用于隔离测试中的服务故障场景。
## 输出与临时资源验收
按[源码调查](../architecture/runtime-storage.md#output-and-temporary-file-investigation)选取生产入口,
在 C01/C02/C06 中记录 GoodBuddy 创建的文件数、写入字节、峰值保留字节、清理后残留及内存高水位。
区分正式保存、必要启动文件和中转写入;结果只计 GoodBuddy 所有的文件,不把第三方内部缓存归为产品临时副本。
沿用三轮前后对照,不以目录改名或文件移到数据库目录证明减少写入。
| 覆盖 | 必须验证的结果 |
| --- | --- |
| 普通 MCP、短命令输出、空 stderr、短搜索/子任务输出 | 中转不创建输出临时文件;需要保存的正常消息/事件仍持久化,MCP SDK 实际 transport 路径纳入检查 |
| 超出预览的输出 | 从现有工具到下一次模型 `output_read` 的生产链逐字节还原,覆盖 UTF-8、JSON 转义、游标、EOF、stdout/stderr、失败与取消;只保留一份所属正文,无 Temp 全文再复制 |
| 并发与生命周期 | 保留现有会话归属检查,不能读取其他会话输出;多路输出内存有界,释放调用资源不删除仍被历史/分支引用的内容 |
| 数据等价 | 分别核对模型收到的内容、工具事件预览和历史内容;不能用最终回答等价替代工具全文,也不新增整份事件正文副本 |
| 文档解析 | 不再新写/复制重复 `parsed.md`,manifest 内容、原文件、图片及附件采用/复制/重开仍正常;旧数据可读 |
| 必要启动文件 | 配置/插件加载正常,创建中失败能清理所属半成品,子进程及句柄关闭后再删除;失败不误删预先存在的同名文件 |
| 升级与再次启动 | 隔离夹具覆盖旧版失效目录、同版崩溃目录、仍活跃的子进程、归属不明目录和删除失败;只回收确认失效者,失败下次可重试,启动预算不退化 |
复用 `paged-output-store.test.ts`、`direct-model-process-service.test.ts`、
`direct-model-ripgrep.test.ts`、`direct-model-subagent-service.test.ts` 和
`model-runtime.test.ts` 中已有分页与模型下一轮请求断言,并扩展文档存储、附件及 Runtime 生命周期测试。
远程路径单独验证事件映射和提交后 ACK,不声称本地分页测试覆盖远程全文。
无需真实模型即可确定的字节、文件和清理断言使用固定输出;受影响的 Runtime 再按既有 Host 规则验证。
交付记录列出删除的旧 spool 创建入口、复用的正式内容入口、仍必要的文件及理由、实际残留与未能确认归属的旧目录。
本次文档调查未执行上述验收,没有磁盘节省或性能改善的实测结论。
## 平台与生产路径
性能主对照使用固定参考机。Windows、macOS、Linux x64/arm64 的原生验证检查进程启动、SQLite 可用性、
文件路径及关闭、升级和重启;没有硬件性能基线的平台只报告功能结果,不与参考机数字混算。
受影响的远程事件持久化、ACK、binding 和 Runtime 生命周期在共享 Linux x64 Host 开发时验收,
采用专用目录和既有固定身份;真实模型调用数单独报告,不混入无外部请求的性能基准。
正常 UI → Preload → Main → 存储进程 → SQLite 的完整路径必须至少覆盖 C01、C03、C07;
仅组件 mock bridge、独立 SQL 或进程握手均不能替代。原生客户端 ledger 的创建、失败、不确定结果及关闭删除也必须覆盖。
生产构建、各平台包内入口及进程启动通过既有 CI 和[发布手册](../development/release-runbook.md)验证。
本规范不授权发布准备期间本地打包或启动 portable 原始用户目录;源码测试使用隔离数据。
## 自动检查与交付记录
### 复用既有验收入口
扩展下表中的脚本和测试,不新增一套并行性能框架。现有能力与缺口均来自源码核对,不能把“已有测试文件”等同于已覆盖新语义。
| 场景/断言 | 既有入口 | 本次需要补齐的内容 |
| --- | --- | --- |
| C01/C02/C06/C07 | `build/run-app-perf.cjs`、`tests/support/app-perf-driver.mjs`、`build/run-startup-perf.cjs` | 多项目生产路由、并行流和子任务、冷热功能进入、资源回落;为 C06 显式设置匹配的运行期限,保留原场景期限 |
| C03/A04 | `build/run-main-perf.cjs`、`build/run-knowledge-perf.cjs`、`build/run-write-perf.cjs` | 保留既有数据,新增存储客户端和真实重叠负载;与完整 App 结果对应,不把分开测量冒充同时执行 |
| C04/A02/A03 | `remote-event-batcher.test.ts`、`ipc.test.ts`、`runtime-acp-backend.test.ts`、`runtime-controller.test.ts` | 异步提交延迟/拒绝时禁止提前通知及 ACK;覆盖交互、后台和恢复三个入口,open/prepare/close 清理时健康 peer 仍可操作 |
| C05/C06/A05/A06 | `readonly-query-reader.test.ts`、`supervision-model-pool.test.ts`、`subagent-scheduler.test.ts`、`shutdown.test.ts` | 取消后真实容量、队列饱和、进程丢失和等待退出;升级预检 SQL 迁出,checkpoint 终止得到确认 |
| A04/A07/A09 | `supervision-worker.test.ts`、`supervision-ui-reads.test.ts`、`supervisor-layout.electron.test.ts`、`message-expansion.electron.test.ts` | 保留事务、来源及交互断言;完整 App 扩展 UI 到真实存储链,不能只用组件模拟 bridge |
| SA-07 | `eslint.config.js`、`build/check-perf-thresholds.cjs`、`tests/perf-thresholds.test.ts`、已有 Desktop/Agent workflow | 间接打开点、白名单基线、负载元数据、缺失指标及并发场景;预算与历史回归上限分别报告 |
既有部分脚本默认使用系统临时目录,复用时按 AGENTS 统一到仓库 `temp/`;新增的可重用夹具进入 `tests/` 或 `scripts/`。
C06 使用独立场景时长参数,不能通过放宽所有测试超时处理。
现有阈值检查未验证全部负载规模,需校验项目、会话、子任务、数据量及降速条件,缺少或不匹配时不能产生合格结果。
### 替代路径核验
A01/A03/A05/A06 还必须逐项确认:Main 升级预检与间接 ledger 打开已迁出,生产 `readWithFallback` 同步回退已删除,
监督清单不再持有独立业务写入权限,远程 `flush` 等待提交且失败可传播,原生客户端先完成 ledger claim 再发模型请求,
关闭 ledger 后才删所属目录。检查外部 live repository 工厂、重复客户端、重复队列和旧 shutdown 回调无生产调用。
保留合法的仓储同步单测、原子事务内部读取和远端数据库实现;不以删除测试或已发布兼容代码实现“清零”。
### 最终记录
- 静态检查覆盖 `DatabaseSync`、同步文件 API、`openPrivateSqliteDatabase` 及桌面间接构造点;
允许的存储宿主/测试/远端代码按执行环境列出,不能整片豁免 `src/main`。
- 白名单和 App 上限与已提交基线比较;迁出后删除例外,不因换文件名增加例外。
- 普通 CI 运行契约、顺序、取消、资源释放和生产路径测试;可复现性能执行环境运行上述关键并发基准,
在合并/发布候选检查中提供必需结果。没有合适 runner 时,记录为验收缺口,不能默认通过。
- 最终同一候选运行 `npm test`、`npm run typecheck`、`npm run lint`,并核对 native CI/Host 与性能报告对应的源码。
- 验收记录列出每个 C/A 编号的结果、命令、日志位置、样本数、原始数据和未测项;单独报告低配、故障注入及真实模型结果。
截至本文编写,只有历史局部测量可引用,尚无本矩阵的全项通过记录。待实现完成后新增具体日期的验收报告,
保留失败结果和复测理由,不把目标架构文档改写成未经验证的进度证明。
@@ -0,0 +1,323 @@
# 存储架构升级实施与验收记录
## 交付范围
本记录跟踪 [架构设计](../architecture/desktop-storage-direction.md) 的 SA-01 至 SA-08,并汇总
[Assistant 消费者](../architecture/assistant-storage-consumers.md)、[Knowledge 迁移](../architecture/desktop-storage-knowledge.md)、
[文件存储](../architecture/desktop-storage-files.md) 和 [Runtime 存储](../architecture/desktop-storage-runtime-integration.md) 的当前实现。
同一候选已完成多项桌面数据库所有权迁移、异步生产调用、输出唯一存储和临时资源清理。
内部工作按依赖并行执行,最终整体交付。接口、进程握手或分组测试通过均不代表产品完成。
保留现有未提交文档及仓库规则修改,不自动提交或发布。
## 实施分工
| 工作包 | 实施内容及替代入口 | 验证要求 | 当前状态 |
| --- | --- | --- | --- |
| S01 存储宿主 | 复用 reader 请求关联与 utilityProcess;宿主打开 Assistant/Knowledge,负责升级、查询分派、提交和退出 | 真实 Electron 进程、升级失败重试、取消结算、提交后读取、退出排空 | 已实现并接入生产组合;全量验收未完成 |
| S02 Assistant 调用 | IPC、心跳、监督、任务、笔记、通道、审计和工具接口改用异步领域操作;回调事务留在宿主 | 先写后读、回顾原子发布、项目隔离、暂停/恢复、错误传播 | 已接入生产组合;完整并发验收未完成 |
| S03 Knowledge 调用 | 数据库与 external repository 归宿主;解析、模型与凭据保留原所属;发布 DTO 替代事务回调 | 导入、检索、重建、embedding、删除和取消 | 已接入生产组合;完整 App 和并发验收未完成 |
| S04 文件与关联 | 文档结果、附件引用、正文分页由同一存储宿主管理;删除重复 Markdown;保留分支引用 | 采用/复制/释放/重启,取消失败全文,删除项目引用回收 | 已接入生产组合;完整 App 和并发验收未完成 |
| S05 绑定与账本 | 桌面远程绑定和原生客户端账本迁入宿主;远端数据库保持原所有权 | claim 后派发、提交后 ACK、响应不确定、关闭后删除目录 | 已接入生产组合;完整 App 和并发验收未完成 |
| S06 输出与启动材料 | 短输出有界内存,全文唯一 backing;必要配置文件归属、部分创建清理、启动回收 | 字节等价、内存上限、无多余文件、活跃所有者保护 | 生产 backing 已接入;跨场景文件与生命周期验收未完成 |
| S07 应用组合 | Main 初始化、事件通知、远程批处理、shutdown 接入统一存储;删除旧打开和同步回退 | 正常 UI 进入真实宿主,后台/恢复入口,退出失败可见 | 当前 Main 组合已接入;全路径验收未完成 |
| S08 守护与验收 | 扩展现有 lint、性能脚本和 Electron 测试;复核旧调用及重复实现 | 全仓检查、C01–C07/A01–A10、真实模型及数据副本 | 待最终候选 |
S01 的契约供 S02–S06 复用,S07 统一组合。跨包共享文件由指定整合者修改;业务代理提供所需操作和测试。
全量测试只对组合后的候选作最终判定。每次失败记录源码版本、原因、修复及复测,不放宽预算。
## 已发现问题对应关系
监督者重读/发布 Main 阻塞对应 S01/S02/S07;过大响应、窗口化和摘要缓存保留此前修复并在 C03/C07 回归。
跨会话全局等待、取消/迟到路由、超限用量与 Telegram 交付保留已有修复,纳入故障和混合负载验收。
输出短内容写后即删、全文引用缺失、重复解析 Markdown、Continue 部分创建和 helper 退出、Python 归档残留对应 S04/S06。
Agent 旧包/远程 staging 的失败清理、项目删除附件回收、诊断解析结果归属及笔记原子临时残留需逐项核对现有实现后记录处理结果。
有意保留的原生历史、安装缓存、用户导出、模型文件与 WAL 不作为垃圾删除。
## 真实数据与模型
已定位 portable:`dist/GoodBuddy-windows-x64/data`。该位置是原始数据,只读取统计并制作一致性备份,禁止直接启动候选写入。
数据库存在活动 WAL 时使用 SQLite 一致性备份;备份与工作副本分开,测试材料位于仓库 `temp/goodbuddy-storage-upgrade/`。
仅复制测试必需的数据库及所引用文件,记录校验值、schema、行数与完整性;不把凭据或正文写入报告。
源库无 WAL 且确认关闭时才允许直接文件复制。升级后逐项比较稳定 ID、顺序、引用、事件还原与重放去重。
性能采用本地确定性模型,真实模型用于生产功能链:普通会话及工具、子任务、监督发布、本地/远程 Runtime。
外部调用有界,记录实际请求数;真实数据内容不会为了性能测试默认发送到外部模型。
远程验证沿用共享 Linux x64 Host、现有凭据和专用测试目录,结束确认所属进程退出。
## 完成条件
最终运行 `npm test`、`npm run typecheck`、`npm run lint` 和差异检查;构建入口变化验证开发构建。
按 [并发验收规范](./concurrent-desktop-acceptance.md) 固定负载和三轮前后对照,包含 30 分钟生命周期。
分别报告界面延迟、Main 阻塞、存储等待/提交、吞吐、取消、内存和文件增长,禁止将单测通过替代性能结论。
实测超预算、未覆盖平台或外部环境阻断均保留为未满足项,不能宣称全部已解决。
当前记录确认 S01-S07 已有生产组合和定向证据;解析 Worker 的无依据队列拒绝已移除。
完整架构验收、平台包验证和 C01-C07/A01-A10 仍未完成。
## 2026-10-05 Portable 真实副本验证
本轮已执行当前源码的 `DesktopStorageClient → utilityProcess → DesktopStorageOwner`,
使用生产升级、读取、发布和关闭入口。脚本为
`tests/support/portable-storage-upgrade.mjs` 与 `portable-storage-upgrade-fixture.ts`。
成功运行始于 `2026-10-05T12:03:32.624Z`,总耗时 265.074 秒;
基于提交 `4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a` 的未提交工作树。
环境为 Windows `10.0.26200`、Intel Core Ultra X7 358H、33,873,752,064 字节内存、
Node `24.21.0`、Electron `44.5.1`。这次未启动完整桌面 UI。
### 原件、备份与清理
原件位于 `dist/GoodBuddy-windows-x64/data`。成功运行没有在该目录打开 SQLite 连接,
也没有启动以该目录为用户数据目录的候选程序。先复制主文件与 WAL 到隔离 capture 目录,
确认源文件的大小、修改时间和 SHA-256 未变,再从 capture 执行 SQLite online backup,
由备份生成独立工作副本。本轮四个源 WAL 均为 0 字节;不能据此声称已覆盖持续写入的活动 WAL。
各数据库分别备份,文件集合不构成跨数据库事务快照。
四个源主文件及各自 WAL/SHM 共 12 个文件的前后大小、修改时间和 SHA-256 全部一致。
升级后的四个备份主文件 SHA-256 也与升级前一致。73 个附件引用均有可复制内容,
包括目录附件,合计 12,720,927 字节;50 个笔记文件合计 431,056 字节。
附件及笔记在复制时校验内容摘要,生产写入与重开后再次确认原件及工作副本内容未变。
日志只保存表名、计数、摘要和断言结果;没有复制设置或凭据,没有把正文写入报告,外部模型调用为 0。
被取消的旧探针曾继续占用 `run/work` 的 Assistant 文件。环境中的 `node` 实际由
`GoodBuddy.exe` 承载,按可执行文件名查询 Node/Electron 会漏掉该进程。
确认其命令行为本任务旧探针后,终止所属进程树并删除残留文件。
成功运行自动删除 `fresh-run` 的 capture、backup、work、WAL/SHM 与 Electron 临时目录;
旧 `run` 也已删除。保留的 `temp/goodbuddy-storage-upgrade/summary.json` 为无正文统计证据。
原取消轮没有保存完整源文件前后报告,因此上述原件不变结论限定于本次有记录的验证区间。
### 数据与正确性
| 数据库 | 源主文件/备份字节数 | schema 前 → 后 | 实际数据规模 | 完成写入练习后的主文件字节数 |
| --- | ---: | --- | --- | ---: |
| Assistant | 2,429,247,488 | 61 → 62 | 11 项目、312 会话、4,836 消息、2,353 任务、1,755,171 事件、233 成果、23,690 活动记录 | 2,432,491,520 |
| Knowledge | 1,568,768 | 13 → 13 | 2 知识库、2 文档、27 分块、27 embedding | 1,568,768 |
| Attachments | 98,304 | 1 → 1 | 73 附件、73 引用 | 98,304 |
| Runtime Bindings | 143,360 | 0 → 0 | 53 绑定 | 143,360 |
本轮实际覆盖的 Assistant 61 → 62 迁移新增监督来源/结果索引;数据已有历史差量存储,
没有重跑 schema 33/34 的正文压缩迁移。
在追加合成记录之前,对四库全部已有用户表逐行计算规范化摘要,比较行数与表摘要。
比较包含稳定 ID、顺序字段、时间、引用和远程来源字段;子任务事件按任务及事件 ID 顺序,
调用生产 `restoreSubagentPayload` 还原后比较。未变的不可变过程块复用内容摘要,
过程块数量与顺序仍参与每条事件的摘要。所有已有表的 `changed` 与 `added` 列表均为空,
没有需要豁免的启动恢复差异。Assistant 完整扫描前/后分别耗时 80.393/107.989 秒。
备份、升级副本及完成写入练习后的四库 `integrity_check` 均为 `ok`,
`foreign_key_check` 均为 0 条。真实监督数据包含 3 个结果、434 个事件、758 个来源;
读取其中一个图谱并关闭/重开,内容摘要一致。
新增远程事件的首次写入、同内容重放去重、冲突拒绝、已提交序号读取,以及重开后的重放去重均通过。
100 批合成监督发布每批包含 20 个来源及 20 个事件;最后一批的事件、来源和 20 条关联均存在,
关联端点有效,关闭/重开后图谱摘要一致。
Assistant 空闲页从 1,643 变为 1,619,Knowledge 维持 90 页;本轮没有证明空间回收收益。
表中 Assistant 最终大小包含后续合成发布与去重记录,不能当成仅升级造成的增长。
最终工作 WAL 的独立字节数没有写入该轮报告,退出后相关副本已清理。
### 耗时与边界
| 项目 | 单轮观测 |
| --- | --- |
| 首次生产存储就绪/关闭后再次就绪 | 557.778/308.364 ms |
| 后续练习初始化/再次就绪 | 333.349/286.487 ms |
| 100 次发布与 100 组图谱+结果列表读取重叠执行 | 32,363.016 ms |
| 发布 p50/p95/max | 10.830/15.269/27.026 ms |
| 读取组 p50/p95/max | 334.647/355.728/368.961 ms |
| 夹具 Main 事件循环延迟 p99/max | 16.482/17.121 ms,共 2,117 个采样 |
| 结束时 pending/Main RSS | 0/93,634,560 字节 |
这些数值来自单轮、两个有界生产者的闭环负载,不能替代固定 offered load、三轮前后对照、
多项目混合负载或 C01–C07/A01–A10。pending 高水位只在请求完成后采样,观测值 1 不是全程峰值。
未测 UI 响应、Renderer、存储进程内的等待与提交分解、30 分钟生命周期、真实模型及远程 Host。
Main 延迟采样也不能替代单次阻塞测量,当前证据不足以宣称所有性能预算达标。
探针设有 15 分钟总期限、每个 Electron 练习 180 秒期限、完整扫描 600 秒检查,
扫描每 100,000 行报告进度,练习每 10 秒报告阶段;通过后台运行与有界轮询执行。
前一版重复序列化累计过程的扫描超过期限,修正为复用块摘要后才完成全量比较,未减少事件数。
另外修复了 `publishedGraph` 的 `unknown` 使用:用共享 `supervisionGraphViewSchema` 校验响应,
没有用类型断言跳过检查。依赖目录缺失曾阻断续跑,经用户授权按 lockfile 恢复后完成执行。
### 检查结果
`npm run typecheck` 全仓通过,用时 14.312 秒。两个探针文件的 ESLint 通过。
定向运行 `desktop-storage.test.ts`、`desktop-storage-files.test.ts`、
`desktop-storage-runtime-operations.test.ts` 与 `assistant-storage-upgrade.test.ts`,
4 文件、31 测试全部通过,Vitest 报告 8.56 秒;包含真实 Electron 存储及历史迁移合成回归。
结束前执行 `--verify-source`,再次确认 12 个源 DB/WAL/SHM 指纹和笔记摘要与本轮基线一致。
`git diff --check` 无空白错误。中文审校扫描无阻断项,复核项中的测试边界与实际统计表述保留。
全仓 `npm run lint` 最后仍有一个范围外错误:
`tests/support/story-graph-focus-driver.mjs:122` 的 `console` 未定义;没有修改该探针。
全仓 `npm test` 使用 300 秒总期限启动,结束时没有留下最终汇总或退出结果,不能标记为通过。
已输出的失败涉及 `MarkdownRenderer.cache.test.tsx` 的 1 个微基准、
`image-tool-mcp.test.ts` 的 3 个测试及 `obsidian-electron.test.ts` 的 1 个测试。
这些失败保留给对应实现负责者,不以本轮存储验证通过覆盖全仓失败。
检查产生的本任务临时目录也在所属进程退出后清理;没有提交代码。
## 2026-10-05 C01-C07 并发桌面验收(未完成)
本轮使用现有 `build/run-app-perf.cjs` 与
`tests/support/app-perf-driver.mjs`,新增最小并发模式夹具
`tests/support/app-perf-concurrent.mjs`。夹具通过真实 UI、Preload、Main、存储宿主和本地回环确定性模型运行;没有启动 portable 原始目录,也没有外部模型请求。基线使用独立 worktree 的提交
`4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a`,候选为同一提交的当前未提交工作树。日志和原始 JSON 位于 `temp/goodbuddy-concurrent-upgrade/`。
本轮没有达到验收规范要求的三轮交替、每项三轮各 100 个有效样本,也没有完成全部 C01-C07。因此不能标记 PERF-15/PERF-16 或本架构验收通过。
| 场景 | 实际结果 | 证据与主要指标 |
| --- | --- | --- |
| C01 3/4/8 | 未通过 | 当前候选首轮测量 1,898 个按键样本,p95 输入 16.8 ms、p95 帧 17.0 ms,但 Main 最大事件循环延迟 64.4 ms;请求未排空,驱动超时。基线首轮在第一批真实发送前等待发送按钮超时。 |
| C02 4/8/16 | 未通过 | 两个 30 秒测量周期完成;周期 1 有 494 个按键样本,p95 输入 17.4 ms、p95 帧 17.2 ms、4.2% 抖动、Main p99 18.4 ms、最大 25.3 ms。第三周期在请求排空前超时;标准机输入和帧 p95 均超预算。 |
| C03 回顾发布+知识重叠 | 未执行 | 现有 App 夹具没有真实重叠的 100 批回顾发布、知识导入/检索和图谱读取生产动作。 |
| C04 故障隔离 | 未执行 | 没有挂起启动/模型路由/清理并验证取消及健康 peer 的夹具。 |
| C05 存储故障与饱和 | 未通过 | C06 运行期间生产存储路径实际多次返回 `STORAGE_CAPACITY`;错误来自 `ReadonlyQueryReader.call` 的会话队列读取,已保留在 `soak-current/execution.log`。专门读 Worker 失败、存储进程退出和提交前后故障注入尚未完成。 |
| C06 30 分钟生命周期 | 未完成 | 已启动完整期限为 1,800,000 ms 的后台运行,实际约 34.5 分钟后因驱动超时退出;首个 30 秒测量有 494 个按键样本,p95 输入 16.6 ms、p95 帧 16.9 ms、Main p99 17.5 ms。由于请求持续在途且存储容量错误,未完成规范要求的完整生命周期、关闭重开和排空,不能算通过。 |
| C07 冷/暖进入 | 未通过 | 两个周期完成;周期 1 有 442 个按键样本,p95 输入 17.8 ms、p95 帧 50.0 ms、8.2% 抖动、Main p99 17.7 ms。夹具尚未分别测量监督概览、图谱、历史选择和来源正文的冷/暖内容可用时刻。 |
资源证据包括每个场景的 Browser、Tab、GPU、Utility PID、CPU 和工作集快照。当前候选的 C02 周期 1 Tab 工作集约 210 MB、Browser 约 200 MB;C07 周期 1 Tab 约 210 MB、Browser 约 212 MB。夹具已记录 3/4 和 4/8 项目/会话档位及 8/16 子任务 offered load,但生产事件中没有形成稳定的 8/16 个同时运行子任务,不能把 offered load 当成实际吞吐。周期性 4 KiB 工具更新、存储排队/执行拆分、缓存/订阅计数、临时文件字节数、C03-C05 故障动作和完整 supervisor 冷/暖内容指标均为未采集项。
本轮没有修改核心存储或调度实现。`node --check build/run-app-perf.cjs` 和
`node --check tests/support/app-perf-concurrent.mjs` 通过;未运行全仓 `npm test`,
因此本节只记录局部复测,不改变 C01-C07 的未完成状态。
### 2026-10-05 C01/C02/C07 实际证据复核与局部修复
复核了上述原始 JSON、`soak-current/execution.log` 及生产源码。容量错误的实际调用链为
`pumpConversationQueue` → `DesktopStorageClient.listConversationQueueItems` →
`ReadonlyQueryReader.call`;同一压力段还可见 `tasks:execution-stats`、工作区变化和概览读取竞争同一
有界读容量。C01 候选首轮为 1,898 个输入样本,输入 p95 16.8 ms、帧 p95 17.0 ms、Main 最大
64.4 ms;C02 周期 1 为 494 个样本,输入 p95 17.4 ms、帧 p95 17.2 ms、Main p99 18.4 ms、
最大 25.3 ms;C07 周期 1 为 442 个样本,输入 p95 17.8 ms、帧 p95 50.0 ms、Main p99
17.7 ms。C02/C07 的长帧主要出现在持续事件输入和显示刷新期间,采样中没有长任务脚本时间,
但有长动画帧的渲染/样式布局时间;因此未减少 offered load、输入样本或阈值。
本轮应用了三个最小局部调整:隐藏窗口时丢弃已过时的会话显示刷新并在重新可见时重读;侧栏隐藏时
不挂载项目切换器、活动菜单及其项目行;队列泵遇到 `STORAGE_CAPACITY` 时按会话只保留一个
50 ms 后重试,避免同一队列因容量瞬时不足进入错误风暴。正常事件、持久化和队列项目均保留。
定向 `ipc`、刷新和项目切换回归为 248 passed、1 skipped;类型检查和相关 lint 通过。
重新构建后以相同 C02 档位(4 项目/8 会话/16 子任务、每轮 30 秒输入)执行了四个周期,
原始报告位于 `temp/goodbuddy-concurrent-upgrade/profile-1MGfH7/artifacts/result.json`。修复后四轮
输入 p95 为 16.9/16.9/17.0/17.3 ms,帧 p95 为 16.9/16.9/17.0/17.1 ms,Main
最大事件循环延迟为 46.5/31.1/45.8/292.8 ms;最后一轮仍有 1 个 51 ms 长任务,驱动因
请求未排空而超时。日志仍观察到少量其他后台读取的 `STORAGE_CAPACITY`,所以本轮只能证明显示
刷新和队列重试的局部改善,不能标记 C02 或容量验收通过。相对原 C02 周期 1,前三轮输入/帧
p95 已回到约 17 ms,但 Main 尾延迟和完整排空仍未达预算;C01/C07 尚未取得修复后的同口径复测。
## 2026-10-05 C03/C04/C07 补充运行(仍未满足)
本轮只扩展现有 `build/run-app-perf.cjs`、`tests/support/app-perf-driver.mjs` 的
并发夹具,并通过已有 Preload/IPC/存储宿主/SQLite 生产入口执行。模型仍为本地
回环确定性模型,数据目录和运行目录均由 `run-app-perf` 隔离在
`temp/goodbuddy-concurrent-upgrade/`。构建命令为 `npm run build:bundle`;本轮没有
修改核心存储或调度实现,也没有提交。
| 场景 | 实际结果 | 证据 |
| --- | --- | --- |
| C03 混合存储 | 未满足 | `C03-current-r4/result.json` 记录知识导入耗时 4,307.193 ms,100 个重叠批次,100 次历史读取、100 次知识检索、100 次图谱读取,重叠窗口 3,784.373 ms。当前隔离数据没有已持久化监督结果,回顾发布批次为 0,因此没有把图谱读取冒充回顾发布;运行期间日志出现 `STORAGE_CAPACITY`。结果状态保留为 `not-fulfilled`。 |
| C04 故障隔离 | 未满足 | 复核发现原夹具在请求观察器登记前允许点击取消,且 HTTP `request.close` 被误记为模型中止;现已改为等待请求登记、使用响应 `close` 判断异常中止,并等待健康 peer 真正进入和完成。`C04-final-local-r2/result.json`(本地确定性、Windows x64、Electron 44.5.1)记录取消后请求结算、查询和 Runtime status 响应、清理完成,健康 peer 用时 37,142 ms,但 `healthyPeerUnaffected: false`,且循环账本 4 次请求中没有 `fault-hung` 条目,因此不能声称挂起模型路径已完成。外部模型请求数为 0;未授权执行真实模型。 |
| C07 冷/暖进入 | 未满足 | `C07-current-r1/result.json` 记录冷进入:输入 0.591 ms、历史 41.889 ms、监督概览 2.532 ms、图谱 2.244 ms、知识快照 2.555 ms;暖返回:输入 18.844 ms、历史 219.868 ms、图谱 10.757 ms、知识快照 2.062 ms。暖监督概览因 `STORAGE_CAPACITY` 无可用时刻;来源正文没有持久化来源,仍未测得。夹具状态为 `measured-with-gaps`,整体未满足。 |
所有补充运行均保存原始 `result.json` 和执行日志;模型边界仍为本地回环,外部请求数为 0。运行中观察到的存储容量错误及一次存储 Worker 退出均保留为当前候选的未满足项,不能以夹具已采集样本或构建成功覆盖。三轮交替、每项三轮 100 个有效交互样本、C03 回顾发布、C04 健康 peer 完成和 C07 来源正文时刻仍未完成。
## 2026-10-06 Portable 证据轻量复核
本次只读复核针对原始 `dist/GoodBuddy-windows-x64/data`、保留的
`temp/goodbuddy-storage-upgrade/summary.json` 和当前仓库状态执行;没有启动 portable 原始目录,
没有复制或写入原始数据库,也没有提交。保留证据的成功运行时间仍为
`2026-10-05T12:03:32.624Z`,记录提交为 `4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a`,
且记录的工作树为未提交状态。当前 HEAD 为
`edf019aaa728f094a433a2601d149d0ec9b28a21`,工作树仍有大量未提交修改,因此 portable 运行早于当前
源码状态,不能把它当作当前 HEAD 的运行结果。
保留验证器 `node tests/support/portable-storage-upgrade.mjs --verify-source` 本次未通过:
当前 `assistant.sqlite` 与保留基线的 SHA-256 和修改时间不同,当前 `knowledge.sqlite` 的修改时间不同,
且保留基线中的四个 0 字节 WAL 与四个 SHM 当前已不存在。当前四个主库的字节数仍分别与记录一致;
`conversation-attachments.sqlite` 和 `remote-runtime-bindings.sqlite` 的 SHA-256/修改时间仍一致,
`knowledge.sqlite` 的 SHA-256 仍一致。当前 `notes` 仍为 50 个文件、431,056 字节,内容指纹与保留证据一致。
因此原件相对本轮 portable 执行后的“当时未变”结论有后续漂移,不能宣称当前原始数据仍与 portable 运行基线完全相同。
对当前四个原始主库执行只读 SQLite `PRAGMA integrity_check` 均为 `ok`,`foreign_key_check` 均为 0 条,
版本分别为 Assistant 62、Knowledge 13、Attachments 1、Runtime Bindings 0。这只证明当前文件的轻量结构完整性,
不恢复 portable 运行与当前源码的一致性,也不替代一次新的隔离副本迁移验证。
清理复核确认 portable 验证使用的 `temp/goodbuddy-storage-upgrade/fresh-run`、`run` 目录均不存在,
保留目录中只有日志、JSON 结果和源基线等证据文件;进程命令行未发现 portable 数据目录、验证脚本或
`goodbuddy-storage-upgrade` 相关进程。机器上发现的已安装 GoodBuddy 进程使用独立用户数据目录,未计入本次
portable 验证进程。上述结果只更新本实施记录,未修改原始目录或其他实现文件。
## 2026-10-06 简化与前后对照验收
### 简化
按“出现实测问题才加机制”复核并发修复期间新增的机制,删除以下实现(共约 400 行):
| 删除项 | 原因 | 替代 |
| --- | --- | --- |
| 自定义分帧传输 `desktop-storage-wire.ts` | 同机实测原生 `utilityProcess.postMessage` 传 32 MB 文本 126–139 ms、5 万行 71–85 ms,Main 最大延迟在 16 ms 计时精度内;自定义分帧分别为 249–271 ms、448–496 ms | 原生结构化克隆 |
| 读/写/后台三类准入预算、交互保留槽 | 容量错误是“满了就拒绝”造成的,分类只是缓解 | 单一在途上限 32,超出按顺序等待,不再拒绝 |
| 同键读取合并、存储进程侧重复准入、整值读取串行 | 客户端已限流,宿主再做一遍没有新约束 | 删除 |
| 队列泵 `STORAGE_CAPACITY` 50 ms 重试 | 等待替代拒绝后不再出现容量错误 | 删除 |
另把存储进程启动移到建窗口之前,与窗口创建重叠。取消、提交后才确认、进程丢失时不重放、关闭前排空的语义不变;真实 Electron 存储夹具(含 16 MiB 视频、2,560 万字符历史)通过。
### 前后对照
基线为独立 worktree 的 `edf019aa`,候选为同一提交加当前工作树;同机交替运行,本地确定性模型,无外部请求。判定口径见[并发验收的发布门槛](./concurrent-desktop-acceptance.md#发布门槛与目标差距)。
| 场景 | 正确性门槛 | 对照结果(基线 → 候选,3 轮) |
| --- | --- | --- |
| 单会话 11 个场景 | — | 输入、切换、长会话打开、流式均在轮间波动内;Main 延迟 p99 在流式场景约 20.6 → 16.8 ms;启动至输入框可用中位数约慢 33 ms(1,300 → 1,333 ms),与轮间波动同量级 |
| C01(3/4/8) | 两版均 3/3 通过:全部排空、0 错误 | 排空耗时一致(约 37 s/17.5 s);Main p99 约 21 → 16.7 ms |
| C02(4/8/16) | 两版完成的轮次均通过 | 排空耗时一致(约 23 s/53 s);Main p99 约 22 → 16.7 ms |
| C03 混合读取 | 两版均无回顾结果可发布,标未满足 | 100 批重叠读取窗口约 1,010 → 912 ms;基线知识导入在该夹具中报错,候选无错误 |
| C04 故障隔离 | 两版均 3/3 通过:挂起请求取消结算,健康会话完成 | 取消观测约 135 → 101 ms,健康会话耗时一致 |
| C06 5 分钟生命周期 | 两版均通过 | 每轮排空耗时一致;4 轮后堆 18.2 → 17.8 MB;另有候选 30 分钟运行 21 轮全部排空、0 错误,堆每轮约增 0.3 MB,与每轮新增 8 条持久化会话一致,结束时驱动退出出错,未记为通过 |
| C07 冷/暖进入 | 无持久化来源,来源正文未测 | 冷进入各阶段 1–7 ms,两版一致;暖返回监督概览 38–45 → 25–38 ms |
旧版记录曾按产品子任务并发 3 判定 16 个子任务的测量;该数字没有用户设置或故障测量依据,已从生产默认配置移除。后续验收按实际 offered load 和实际运行数记录,不再用内部上限替代吞吐结果。夹具修正:排空等待改为 180 s(两版相同)、子任务按 `childTaskId` 统计、输入框焦点未就绪时重试输入。
结论:**正确性门槛在 C01/C02/C04/C06 两版都通过,候选在单会话和并发场景均未退化,Main 尾延迟有改善。** 剩余未满足项:C03 回顾发布与 C07 来源正文需要先有持久化回顾数据的夹具;C05 存储故障注入只有真实 Electron 存储夹具中的进程丢失、发送失败、取消覆盖,未做 App 级故障矩阵。输入 p95 约 8–9 ms,帧 p95 多数 8.5 ms,部分轮出现 16.5/58 ms,两版相同,记为目标差距。
### 真实数据与真实模型
portable 新副本(`--fresh`)当前源码复验通过:Assistant 库 175 万条事件、4,848 条消息,schema 62,所有已有表规范化摘要无变化,`integrity_check` 为 ok,外键违规 0;73 个附件、50 个笔记前后一致,原件 DB/WAL/SHM 指纹未变,副本已删除。100 批发布与读取重叠时,发布 p95 19 ms,夹具 Main 延迟 p99 16.4 ms。
真实模型输出分页首次走通:使用当前默认 `openai-responses` 配置,模型调用 1 次 `process_execute`、2 次 `output_read`,逐页游标连续至 EOF,恢复两个随机标记,**4 次外部请求**。之前两次失败的原因是该手动用例未注入输出正文存储(生产在 `index.ts` 注入),工具在模型分页前就报错;已改为注入与其他单测相同的文件存储。
### 全仓检查
`npm run typecheck`、`npm run lint` 通过。`npm test` 三次全量结果:1 失败(`workspace-import` 图片加载超时)、9 失败、1 失败(`local-runtime-reuse` 压缩返回 false),每次失败的用例不同,单独复跑均通过(`local-runtime-reuse` 3/3、`workspace-import` 4/4、其余 3 个文件 481/481)。判定为满载时的时序敏感用例,未放宽断言或超时,但不能记为全仓一次通过。
## 2026-10-06 有界真实模型复验
本节记录当前未提交源码的最终有界复验。没有启动 portable 原始数据目录,也没有把其中的会话、附件、笔记、日志或凭据发送给外部模型。所有发送给模型的监督输入均为脚本内合成内容;取消和输出分页场景也只使用脚本生成的短提示、临时工作区和随机标记。请求计数来自生产 Runtime 的 fetch 层或本地计数 relay,不记录请求正文、响应正文、请求头、端点或密钥。
| 路径 | 实际外部 HTTP 请求 | 结果 | 范围与边界 |
| --- | ---: | --- | --- |
| 生产存储宿主监督链 | 4 | 通过,4 次 HTTP 200 | `supervision-storage-live.cjs` 使用当前运行时设置读取凭据,在隔离 Electron、Assistant/Knowledge 存储宿主、生产监督工厂和 Heartbeat 入口中运行合成项目;故事、经验、建议发布成功,`no_change` 重跑新增 0 次,设置未改变,pending 为 0 |
| 生产监督审核工厂 | 2 | 通过,完成 | `supervision-audit-validation.ts live` 使用当前 `ModelAgentRuntime`、生产监督服务和隔离 SQLite;1 条合成消息完成提取与故事归属,重跑新增 0 次,SQLite 完整性与外键检查通过 |
| 直连 Runtime 取消 | 3 | 通过 | 现有 `cancelled-turn-live.test.ts` 的两个 direct-model 取消后续答复用本地计数 relay 执行;取消轮和后续请求均完成,取消内容没有并入下一轮,3 次请求均由 relay 计数 |
| 直连 Runtime 输出分页/工具请求 | 2 | 未通过覆盖断言 | 现有 `runtime-e2e.manual.test.ts` 的生产源测试到达模型两次,但模型没有发出 `process_execute` 或 `output_read`,因此没有证明真实模型工具请求和分页闭环;该次仅计入实际请求,不把失败运行写成通过 |
| 本地 OpenCode | 0 | 未执行 | 当前源码 checkout 没有 `.runtime-resources/win32-x64/opencode.exe`;没有改用已发布 portable 或其他缓存二进制 |
| 本地 Continue | 0 | 未执行 | 当前源码 checkout 没有 `node_modules/@continuedev/cli/dist/cn.js`;没有改用已发布包 |
| 共享 Linux Host | 0 | 未执行 | `192.168.0.23` 的 SSH 连接可达,但 BatchMode 连接在认证前由 Host 关闭;没有凭据交换或模型请求 |
本节合计观察到 **11 次** 外部模型 HTTP 请求:监督存储 4 次、监督审核 2 次、取消 3 次、输出分页失败尝试 2 次。没有重试被计入,也没有远程或 OpenCode/Continue 本地请求。监督链使用的运行时设置仅在隔离进程内解密,原设置字节保持不变。
验证结束后,隔离数据库、Electron profile、临时工作区、凭据副本、计数 relay 和生成的 probe 目录均已删除;进程检查没有发现属于这些运行的残留进程。该结果证明了当前源码的生产存储监督与直连取消路径,以及一次明确失败的真实模型工具分页尝试;不证明本地 OpenCode、Continue、远程 Linux Host、真实模型工具分页成功、完整桌面 UI、portable 数据副本或 C01–C07/A01–A10 验收已经完成。
## 2026-10-07 C03/C07 继续验收(当前 HEAD 1f85d0c0)
本次只修改了 `tests/support/app-perf-concurrent.mjs`、`tests/support/app-perf-driver.mjs` 和 `build/run-app-perf.cjs` 的验收夹具。运行目录为 `temp/goodbuddy-acceptance-continue/`,使用 `npm run build:bundle` 生成当前源码开发构建;每次运行都由 `build/run-app-perf.cjs` 启动隔离 Electron profile、存储 utility process 和 127.0.0.1 回环确定性模型。模型请求总数分别保留在原始 JSON 中,均为回环请求,未发送外部模型请求。
### C03 混合存储
命令为 `GB_PERF_MODE=C03 node build/run-app-perf.cjs`,最终证据为 `temp/goodbuddy-acceptance-continue/C03-final4/result.json`。夹具先通过生产 `conversations.saveLocal` IPC 保存 99 个会话、每个 20 条消息,再通过生产 `knowledge.createLibrary` 和 `knowledge.importPaths` 导入知识源;监督页通过真实导航和“回顾”按钮启动生产 `supervision.run`,确定性模型走生产 Runtime 流式接口。回顾提交完成后,结果通过生产 overview、activity 和 batches IPC 读取,首批分页返回的每组证据数为 20,提交进度为 100 批。
生产重叠窗口包含 100 次历史读取、100 次 Knowledge search 和 100 次监督 overview,Knowledge 导入耗时 7,190.102 ms,重叠窗口 6,701.095 ms。结果 `status=measured`,运行器最终状态为 `passed`;没有使用直接 SQL 写入或预置 review 表。C03 的通过条件仅断言回顾结果存在、100 批提交、抽取分页每批 20 条证据、导入已执行以及三类生产读取各 100 次并发生在导入窗口内。
### C07 冷热进入
命令为 `GB_PERF_MODE=C07 node build/run-app-perf.cjs`,最终证据为 `temp/goodbuddy-acceptance-continue/C07-passed/result.json`。冷进入和暖返回分别测量输入框、历史、监督概览、图谱和 Knowledge snapshot 的可用时刻;随后通过生产监督页触发一次确定性回顾,刷新监督页并打开图谱事件。夹具点击真实图谱事件的来源链接,等待 `.supervisor-workspace__source pre` 出现并读取其 DOM 文本长度;本次 `textLength=51`、`rendered=true`,因此来源正文是 UI 内容可用证据,不是 API 返回时间的替代。
两次运行均为 Windows x64、Electron 44.5.1、当前源码 dirty worktree,外部模型请求为 0。C03/C07 的测量状态通过;其余并发矩阵、三轮交替对照、C05 故障注入、完整预算判定和 A01–A10 仍按上文状态保留未完成,不能由本节扩展为 PERF-15/PERF-16 全项通过。
@@ -14,6 +14,13 @@ requests remain paused during reconnection. Startup preserves closed intervals,
discards unclosed intervals, and marks them incomplete; old history is not
backfilled by scanning messages or events.
An incomplete zero duration means no known timed interval, not a measured zero.
Both conversation totals and task cards display "unavailable" in this case.
Positive partial durations retain the incomplete warning; complete zero durations
still display zero. The upgrade regression verifies that a new completed reply
in an old conversation retains its accumulated milliseconds after reopening the
database. This presentation fix does not restore pre-upgrade historical totals.
Schema 59 stores timing rows and adds a covering index for conversation
summaries. The old historical aggregation, its caches, dedicated reader/worker,
worker build entries, and five-second statistics polling have been removed.
+106
View File
@@ -0,0 +1,106 @@
# KISS 局部修复与验证
## 范围
基线为 `e50ca6d1`。先更新文档,再实测实施,完成后回填证据并提交本轮改动。
目标架构见[桌面存储方向](../architecture/desktop-storage-direction.md);独立存储服务迁移留待之后讨论。
本轮复用现有 Worker、数据库、IPC 和控件,不全面异步化存储,不更换数据库驱动,不增加通用恢复框架。
工作区已有三份宣传稿修改,以及实施期间并行加入的“删除会话后继续回顾”改动,不属于本轮提交;共享文件按片段区分。
## 实施清单
| 编号 | 局部改动 | 验收要点 | 状态 |
| --- | --- | --- | --- |
| K01 | 概览不提前加载图谱,来源正文按需读取 | 首次概览无需 graph;图谱和来源内容完整,历史导航正确 | 已实现,组件及 Electron 通过 |
| K02 | 概览索引与故事/经验批量、按项目查询 | 查询结果与排序等价;升级保留原数据,实测查询及输出规模 | 已实现,schema 61→62 数据等价验证通过 |
| K03 | 故事摘要建索引并只计算一次 | 摘要分类及时间规则不变;暖切换不重复计算 | 已实现,三轮对照及回归通过 |
| K04 | 图谱选择列表虚拟化 | DOM 有界,键盘、焦点、选中、滚动和内容访问保留 | 已实现,补齐外部 Tab 入口后 Electron 通过 |
| K05 | 监督重读取接入现有只读 Worker | 生产 UI 调用确实离开 Main,读快照一致,失败不静默回退重查询 | 已实现,真实 Worker/IPC 回归通过 |
| K06 | 发布只构造一次最终快照 | 完整结果与 checkpoint 原子提交;实测剩余 Main 阻塞,不隐藏未解决预算 | 已实现一次构造;Main 发布仍超预算,待架构迁移 |
| K07 | 远程取消清理等待及迟到路由 | 不占全局控制队列,清理后迟到安装不残留,活跃 peer 正常 | 已实现,当前源码 Linux x64 Host 通过 |
| K08 | 超容量响应保留已收到模型用量 | 不保留超限正文、不重试容量错误,实际用量不丢失或重复 | 已实现,两个协议及清理期限回归通过 |
| K09 | Telegram 启动冲突、投递错误及限流 | 待发回复不因接收冲突永久终止;错误可见;跨层遵守 retry_after | 已实现,loopback/SQLite/Electron 通过 |
## 已有审查基线
前三轮合成 SQLite:20 份结果、40,000 条消息、8,000 个事件/来源时,概览同步读取
310/328/324 ms;完整图谱 379/383/387 ms,JSON 约 25.74 MB。400 个故事、800 条经验
的响应执行 1,204 条 SQL,首次 52~65 ms。100 批次、每批 20 来源的最终发布为
567/711/740 ms。该分布是压力样本,不代表用户实际库,首次调用也不等于冷 OS 缓存。
Renderer 源码 Electron 场景中,2,000 事件产生 8,207 个 DOM 元素,出现 152~314 ms 长任务;
2,000 故事各十事件的摘要首次计算 77~81 ms,缓存路由返回仍重复计算。
原始基线脚本位于批准的临时目录,后续对照必须固定数据、版本和口径。
## 验证要求
- 对功能缺陷先运行失败回归,再修复;不放宽断言、超时或性能预算。
- 性能对照至少三轮交替执行,基线用独立临时 worktree;分别记录 Main 阻塞、查询耗时、
响应大小、Renderer 长任务及点击反馈,不能把不同探针耗时相加成端到端值。
- 数据用隔离合成 SQLite 或一致性副本,不修改 portable 原始库;索引升级遵守迁移指南。
- 真实 Electron 验证首次/热切换、历史与详情、键盘、焦点、选中和滚动;不以截断内容提速。
- 远程 Runtime 改动在共享 Linux x64 Host 验证当前源码,使用专用目录及进程;记录有界真实模型调用数。
- 最后运行完整测试、类型检查和 lint;发布准备不运行本地生产打包或已打包应用探针。
## 实施与测量结果
### 监督者读取与界面
后端保留原业务合同,文件数据库的三个 UI 读取在现有 Worker 的读快照中执行,失败直接报告,不回退 Main 重查询。概览新增匹配索引;故事/经验批量查询按项目筛选;图谱保留所有事件和关联,来源只传已有 schema 所需标题与时间,完整正文仍走来源详情。
| 测量项 | 基线 | 修复后 |
| --- | --- | --- |
| 大样本概览:调用方最大循环间隔,三轮 | 389/471/873 ms | 2.45/2.10/3.17 ms |
| 大样本图谱:调用方最大循环间隔,三轮 | 583/572/646 ms | 12.04/12.29/15.79 ms |
| 故事/经验响应的 SQL 数 | 1,204 | 6 |
| 大样本图谱 JSON 大小 | 25,742,067 字节 | 7,143,667 字节 |
| 2,000 事件图谱点击至两帧反馈 | 188~346 ms | 24~39 ms |
| 同场景挂载列表行/文档元素 | 2,000/8,207 | 24/329(首轮窗口实现) |
| 2,000 故事各十事件的首次摘要 CPU | 48~80 ms | 9~16 ms |
| 缓存路由返回的摘要 CPU | 43~74 ms | 0 ms |
| 100 批次发布的同步耗时 | 686/729/730 ms | 271/286/303 ms |
| 100 批次的快照 UPDATE 数 | 101 | 1 |
数据来自固定基线 `e50ca6d1` 与修复版三轮对照,平台为 Windows x64、Core Ultra X7 358H、Electron 44.5.1。
存储测量使用真实合成 SQLite 和 Worker,界面测量使用生产 React 组件与模拟 bridge,二者没有相加成端到端时间。
发布复测包含工作树并行删除校验,不能作为本轮独立提交的精确耗时保证。迁移前后 55 张表、32,947 行含 rowid 的哈希一致,外键及完整性检查通过,未执行正文转换或回收。
独立复审后,故事读取增加 scope/刷新版本所有权校验,避免旧编辑完成时使新读取失效。
虚拟列表额外保留逻辑首末行,保证从外部 Tab/Shift+Tab 进入时仍按完整顺序访问;最终挂载量比上述首轮测量最多多两行,后续 Electron 继续检查有界性与可见焦点。共享列表的主动滚离焦点行为保持不变。
新 Electron 回归还暴露既有消息展开测试的等待缺陷:语言切换后等到了旧节点,且帧轮询回调异常会留下未结算 Promise。测试现等待替代请求的最终渲染,帧回调异常正常拒绝;原超时及断言不放宽,相同严格场景在基线与当前源码均通过。
完整后端和前端逐轮数据分别见[后端记录](../features/conversation-supervision/progress.md#2026-10-05-监督页面读取与发布局部优化)和[界面记录](../features/conversation-supervision/frontend-progress.md)。
### 运行时与交付
K07 的路由 HTTP 清理不再占用全局控制队列;迟到安装按原 Session/operation 清除,不影响活跃 peer。
当前源码通过共享 Linux x64 的 OpenCode 与 Continue:peer 查询分别 13/46 ms,取消完成分别 176/194 ms;工具、断开恢复、测试 Agent 提升及事件重放均通过。本轮准确为 **8 次外部文本 HTTP 请求、0 次图片调用**,含首次测试触及旧请求数上限前的两次请求;各运行结束时所属进程和目录残留均为零。详见[运行时记录](../features/unified-execution/progress.md#2026-10-05-k07k08-局部修复)。
K08 超容量后立即中止并清空正文,最多消费一秒用量事件,保留原错误且不重试。OpenAI/Anthropic、已缓冲完成响应和不响应取消的迭代器均有回归。
K09 已验证启动 webhook 冲突时仍可发送已验证身份匹配的待发结果;发送错误可见且不覆盖接收错误;最后一次 429 的等待可取消并跨越驱动/发件箱边界。
Telegram 验证只使用 loopback HTTP、合成 SQLite 和真实 Electron 设置入口,没有向真实收件人发消息。详见[通道记录](../features/telegram-channel/progress.md)。
### 最终检查
将本轮 59 个路径的暂存内容应用到 `e50ca6d1` 的隔离工作树,排除并行删除功能和宣传稿后执行完整验收。验证时暂存树为 `31e6199b91cb703ddffa3806807326143064a7f1`;随后只补充文档中的验收结果,没有再改代码。暂存代码、测试与独立工作树逐文件核对一致。
| 命令 | 最终结果 |
| --- | --- |
| `npm test` | 523 文件通过、15 跳过;6,212 用例通过、0 失败、87 跳过,832.51 秒 |
| `npm run typecheck` | Main、Agent、Renderer 全部通过,7.36 秒 |
| `npm run lint` | 通过,24.24 秒 |
| `git diff --cached --check` | 通过 |
完整测试使用本机缓存的 Runtime 二进制及配置,包含原生 OpenCode、监督者布局、消息展开和 Telegram Electron 场景;未开启外部模型验收开关。首次隔离测试因缺少 Runtime 缓存多跳过 13 项,不作为最终结果;补齐已有缓存后完整重跑得到上表计数。并行实施期间的其他失败轮次保留在各组记录中,不与最终结果混算。
日志、JSON 测试结果、跳过清单、暂存快照及文件哈希位于批准临时目录 `opencode/kiss-staged-validation-Bmc3dm`,以 `final-verification.json`、`snapshot.json` 和 `test.log` 为索引。验证前后原工作区 77 个已变更文件逐字节未变;未暂存的并行修改完整保留。本轮没有生产打包、推送或发布。
## 剩余限制
- 大批量发布仍在 Main,约 271~303 ms,未达到 16 ms 预算;保留原事务及持久化语义,后续讨论迁移。
- 故事完整响应仍约 3.32 MB,曾有一次 19.74 ms 的调用方间隔;图谱正文统计在 Worker 仍需数百毫秒。
- 未预载的源码模块首次进入仍约 0.31~0.34 秒;一轮小图谱首开出现 55 ms 长任务,没有宣称所有预算达标。
- 未覆盖完整生产窗口 IPC 的端到端性能、低端机节流、macOS/ARM64、签名包安装或真实 Telegram 平台收发。
- schema 62 按既有规则禁止旧客户端打开;独立存储服务、全仓异步化和发布版本号调整均不属于本轮。
@@ -1,5 +1,7 @@
# 桌面端性能改造(第一期,参照 VS Code 架构)结项报告
2026-10-05 校正:本文保留第一期测量截面,开头结论只适用于当时已测场景。生产监督者首次进入和最终发布未被这些场景覆盖,后续实测发现阻塞并开展[局部修复](./kiss-local-fixes-2026-10-05.md)。`perf:check` 的历史回归上限也不等于性能原则的全部预算。
- **时间:** 2026-10-02 至 2026-10-04
- **范围:** Desktop 的 Electron Main、Preload、Renderer 及其后台线程。远程主机(Agent、SSH、远程 Runtime)的稳定性和性能不在本期范围,下一期单独审查。
- **结论:** 本期计划内的工作已完成。用户日常能感受到的卡顿(打字、流式输出、切换会话、滚动、启动、写库)都已测量并修复。剩下的工作是架构上的长期项(存储接口异步化、独立数据进程),风险高,已经写明理由,留到后续单独立项。
@@ -72,7 +74,7 @@ Main 卡顿的数据来自真实数据,以及 5 万个文本块的知识库。
| Markdown 解析放到后台线程 | 需要重写渲染流程。标准机上流式输出已经不卡,只有低端机上的长回答还有余量 |
| 窗口提前加载,接口请求排队等待 | 可省 200–300 ms,但要改动所有接口的注册顺序和首次运行流程 |
| 会话队列移出 App | 和发送、恢���、中断联系太紧,拆出来的风险大于收益 |
| 性能阈值接入 CI | 仓库目前没有 CI 配置 |
| 性能阈值接入 CI | 当期未将性能基准接入已有 CI;原文“仓库没有 CI 配置”有误 |
## 4. 已知问题和下一步
@@ -0,0 +1,77 @@
# 阶段审计修复与验收
本轮十项审计发现均已修复,最终完整测试、类型检查和 lint 通过。修改基于 `126dbc57` 后的工作树,未提交或发布。数据验证使用隔离的合成 SQLite 和本轮真实 Runtime 产生的事件,没有改写 portable 原始用户数据。
## 修复清单
| 编号 | 问题 | 必须覆盖的失败场景 | 状态 |
| --- | --- | --- | --- |
| A01 | 远程 ACP 启动占用全局控制队列 | 延迟初始化时其他绑定查询、取消、看门狗和关闭仍可完成 | 已修复;回归与真实 Host 通过 |
| A02 | 本地 OpenCode 提交失败后等待静默 SSE | 拒绝、错误响应、提交超时均及时结束并保留错误,正常流不变 | 已修复;回归与真实本机模型通过 |
| A03 | 监督候选生产链路遗漏项目参数 | 真实生产装配与 Worker 路径尊重 projectId/crossProject | 已修复;生产 Worker、SQLite 与真实模型通过 |
| A04 | 最终候选查询期间暂停仍发布 | 暂停后不保存结果,正常完成仍保存 | 已修复;暂停/继续与取消回归通过 |
| A05 | 在途故事分配写入已移除故事 | 移除候选后不得产生隐藏关联或消耗待处理事件 | 已修复;移除/合并/拆分回归通过 |
| A06 | 图片生成结果被通道严格契约拒绝 | 含 artifactIds 的执行结果通过生产通道交付,桌面产物保留 | 已修复;生产服务与 SQLite 发件箱通过 |
| A07 | Telegram 原样保存不能恢复轮询 | 终止错误解除后原样保存重启,正常运行保存不重复启动 | 已修复;组件与 Electron 生产入口通过 |
| A08 | 钉钉 HTTP 200 业务错误被算作送达 | 非零 errcode 保留失败及重试上下文,成功回复正常交付 | 已修复;loopback HTTP 与 SQLite 重试通过 |
| A09 | 监督视图请求失效后永久忙碌 | 刷新期间旧请求结束不覆盖新状态,控件恢复可用 | 已修复;组件与 Electron 通过 |
| A10 | 虚拟消息行淘汰丢失展开状态 | 超过八行交互后滚出再滚回保持展开,DOM 仍有界 | 已修复;组件、Electron 与缓存保留测量通过 |
## 验收要求
- 每项记录旧实现失败、新实现通过的测试证据及命令。
- 运行时改动检查本地与远程路径,Agent 改动在共享 Linux x64 Host 验证当前源码。
- 真实模型请求保持有界,记录实际请求数;模拟 Provider 和真实 Provider 分开报告。
- 数据验证使用真实 SQLite 合成数据或 portable 数据隔离副本,记录规模及一致性检查,不记录用户正文或凭据。
- 性能检查遵守 `docs/architecture/performance-principles.md`,测量与推断分开;不删除动画、虚拟化、取消或正常功能来取得通过。
- 最后运行完整测试、类型检查与 lint,并同步所属功能文档。
## 验证记录
### 复现与修复
| 范围 | 修复前证据 | 修复及验证 |
| --- | --- | --- |
| A01/A02 | 静默 SSE 三种提交失败不结束;挂起启动阻塞取消和其他绑定;协议分派只收到 start | 启动等待释放 backend 和 socket 分派队列;相同启动合并、冲突拒绝;迟到 Session 单独关闭。本机提交失败取消所属订阅并保留原错误。九项新增回归通过 |
| A03/A04/A05 | 八个新增用例中七个失败,既有取消场景原本通过;关闭跨项目仍收到其他项目候选,暂停后仍发布,已移除故事仍收到新关联 | 贯通批次参数;发布前检查取消;原事务中检查目标当前状态。96 项监督/Worker 定向测试通过,无 schema 迁移 |
| A06/A07/A08 | 选择性回归九项失败、三项通过;图片结果失败、原样保存不调用重启、业务错误被记作送达 | 通道边界映射公开结果;异常轮询状态允许原样保存重启;有界解析钉钉业务响应。106 项通道测试、3 项 IPC 测试及 Telegram Electron 通过 |
| A09/A10 | 控件保持禁用;超过八行交互后的展开状态丢失,Electron 原生交互也复现 | 失效请求释放等待状态;展开选择由历史面板保存,虚拟行卸载后可恢复。112 项相关组件回归及真实 Electron 通过 |
独立复审没有发现运行时修复引入的高置信度回归,但发现首次 A10 实现保存了未操作的默认展开值。跟进回归复现浏览 10,000 个作用域留下 10,000 条记录,现改为仅保存用户覆盖项并清理已删除来源;该修正包含在最终全仓测试中。
### 真实模型与 Host
本轮准确合计 **7 次外部文本模型请求、0 次图片模型请求**。另有 Telegram Electron 中的 2 次 loopback 模型请求,不计为外部请求。没有向真实消息通道收件人发送测试消息。
| 路径 | 真实外部请求 | 结果 |
| --- | ---: | --- |
| Linux x64 OpenCode | 2 | 当前源码工具执行、断开、恢复、测试 Agent 提升和事件重放通过;启动挂起时查询 48 ms、取消 216 ms |
| Linux x64 Continue | 2 | 同类生产路径通过;启动挂起时查询 60 ms、取消 258 ms |
| Windows 本地 OpenCode | 1 | 真实二进制正常返回固定文本,0 工具调用,总耗时 4,727 ms |
| 监督生产服务与文件 Worker | 2 | 147 字符合成消息完成提取和故事归属,总耗时 3.84 秒;无变化重跑新增请求为 0 |
Host 使用现有固定身份和加密凭据,测试资源位于 `/root/tmp/gb-lifecycle-*`。结束时所属进程为零,没有替换原 Host 安装。延迟是单次场景观察,不是 p95 或吞吐结论。完整命令、脚本及日志索引见[运行时验证](../features/unified-execution/progress.md#2026-10-05-runtime-生命周期审查修复)。
### 数据与性能
- SQLite 合成库包含 10 个项目、1,000 个实体、10,000 个事件和 10,000 条事件实体关联。候选 ID 与排序逐项一致,数据计数不变、外键检查为空、完整性为 `ok`。真实 Host 事件另外通过生产数据库与批处理器写入:OpenCode 45 条/23 个 checkpoint,Continue 16 条/8 个 checkpoint,重放去重通过。
- 激活项目隔离后,测量发现原相关子查询较慢,改用实体集合查询。三轮各十次项目查询,旧 SQL 合计 3,517/5,302/4,190 ms;新 Worker 路径含传输为 39.18/34.01/36.04 ms。120 事件归属事务为 6.63 ms。此结果仅适用于该合成库 SQL 对照,见[监督后端证据](../features/conversation-supervision/progress.md#2026-10-05-后端阶段审查-a03a04a05)。
- Windows Electron 使用 2,000/10,000 条合成消息,原版与首次修复各三轮交替运行;后续缓存修正又做三轮对照。最大采样挂载量均为 15 个消息行、926 个 DOM 元素,展开、键盘、焦点、选区和滚动回归通过。缓存修正后的导航中位数为 38.5~39.1 ms,含聚焦后的两帧等待,不能作为纯渲染耗时或整体预算结论。
- 未操作导航后缓存条目为零;13 条消息交互后保留 38 条用户覆盖项,删除消息后归零。该测量统计缓存条目,没有测量堆字节。方法、每轮数据及原有滚动回归见[前端验证](../features/application-tool-navigation/progress.md#2026-10-05展开状态缓存复查)。
### 最终检查
| 命令 | 最终结果 |
| --- | --- |
| `npm test` | 518 文件通过、15 文件跳过;6,136 项通过、0 失败、87 跳过;852.63 秒 |
| `npm run typecheck` | Main、Agent、Renderer 全部通过 |
| `npm run lint` | 通过 |
| `git diff --check` | 通过 |
首轮完整测试的唯一失败为既有 `cleanup-timeout`。独立 GC 探针确认 mock 返回永不结算 Promise,却未保留待完成的 Request;GC 后 Undici 取消转发失效。测试夹具现保持 Request 存活至取消,并按真实 fetch 行为拒绝。原断言、调用顺序和一秒清理预算均保留,新增强制 GC 回归先在旧 mock 上失败,再在修正后通过。生产清理代码未因此调整。详见[GC 复查](../features/unified-execution/progress.md#sdk-cleanup-timeout-复查)。
最终全仓原始日志为 `C:/Users/jiang/AppData/Local/Temp/opencode/a02-cleanup-final-20261005.log`,退出码及时间记录为同名 `.json`。通道网络替身、发件箱及图片元数据验证见[通道记录](../features/telegram-channel/progress.md#2026-10-05-通道审计修复验证)。
## 验证边界
本轮未做真实 Telegram/钉钉平台收发或付费图片生成,A06 验证的是生成完成后的生产交付契约。未做 macOS、Linux arm64、低端机节流、完整 App 性能预算或签名包安装验收,也未用用户生产大库证明性能。上述已测场景未观察到功能或性能退化,不能推广为所有设备和数据规模的保证。远程修复生效需要部署包含当前源码的 Agent;本轮没有提交、打包发布或升级用户安装。
@@ -1019,29 +1019,38 @@ Renderer 主线程降速 4 倍(Main 不降速,每次 reload 后重新应用
### PERF-15 存储接口异步化
- **优先级 / 状态:** P1 / 进行中(热点读走 worker;高频写降为 NORMAL、checkpoint 移出 Main、启动恢复按索引,见 7.2.2;接口异步化未开始)
- **范围:** 在原进程内把 `AssistantDatabase`、`KnowledgeDatabase` 等对外接口改为异步,
调用方全部 `await`;会话列表只返回摘要,搜索下推到 SQL,活动记录改为增量追加。
远程 Runtime 实时事件改用已有的 `appendRemoteTaskEventsBatch` 批量写入,不再逐条同步事务。
Story Graph 检索先补测耗时,扫描规模设上限,匹配尽量下推到 SQL。
- **验收:** 行为与测试不变;`PERF-11` Main event-loop 延迟不变差。
2026-10-05 决策更新:[KISS 局部修复](../review/kiss-local-fixes-2026-10-05.md)已以 `b90ff50f` 提交。
用户随后确定 PERF-15/PERF-16 及必要守护按一个完整架构候选交付,不拆成多轮局部产品交付。
职责与 KISS 边界以[桌面存储与并发执行](../architecture/desktop-storage-direction.md)为准,
多项目、多会话、多子任务的共同验收见[并发规范](../quality/concurrent-desktop-acceptance.md)。
当前候选已完成存储宿主及主要生产组合,完整迁移验收尚未完成;以下编号保留为职责索引,不表示分别验收的发布阶段。实现状态汇总见[存储升级实施记录](../quality/storage-upgrade-implementation-2026-10-05.md)。
- **优先级 / 状态:** P1 / 进行中(存储域接口和主要生产调用已异步化;完整调用清理、异常路径和 C01-C07/A01-A10 验收仍未完成)
- **范围:** 为桌面业务存储建立异步领域接口,保留完整事务和先写后读顺序;包含 Assistant、
Knowledge、附件、桌面 Runtime 绑定和本机原生客户端账本。跨边界调用方等待实际完成;同步缓存读取
与内部纯计算无需变成 IPC。已完成的摘要、SQL 搜索、活动增量和远程事件批处理继续复用。
- **验收:** 与 PERF-16 一起通过 SA-01~SA-07;只增加 Promise 或保留 Main 同步执行不计完成。
### PERF-16 数据进程
- **优先级 / 状态:** P1 / 待开始
- **实施条件:** `PERF-15` 完成,调用方已全部异步。
- **范围:** 新增独占 SQLite 的 `utilityProcess`,承接会话、知识库、向量检索、Story Graph
(含 Agent 主动调用的检索)、远程 Runtime 事件持久化、活动记录及文档解析;Main 只转发;
保证同一时刻只有一个写入方。吸收 `PERF-06` 的范围。
- **验收:** 大知识库检索、会话列表和文档导入期间 Main event-loop 延迟达到批次目标;取消、
崩溃恢复和迁移语义不回退。
- **优先级 / 状态:** P1 / 进行中(`utilityProcess` 宿主已接入 Assistant、Knowledge、文件和 Runtime 域;全量生产路径与并发验收仍未完成)
- **接口条件:** 每条跨进程业务操作具备异步调用及事务边界;与 PERF-15 在同一候选完成和验收,不单独交付半迁移状态。
- **范围:** 目标为统一管理业务 SQLite 的 `utilityProcess`,承接会话、知识库、Story Graph、
远程 Runtime 事件持久化和活动记录的业务事务;Main 只做校验与转发。重查询由受管只读 Worker
分担,解析、OCR 与 Runtime 执行保持独立,每个数据库明确一个写入所有者;不合并现有数据库。
职责、生命周期及 VS Code 对照以[桌面存储方向](../architecture/desktop-storage-direction.md)为准。
- **验收:** Main 无目标业务 SQLite 直接或间接访问、无同步回退;多项目/会话/子任务、重查询、
导入及发布重叠时满足性能原则。完整矩阵、取消、故障、旧库和平台要求按并发验收执行。
### PERF-17 架构守护
- **优先级 / 状态:** P2 / 待开始
- **范围:** lint 禁止 `src/main` 使用同步文件 API 和在数据进程外引用 `DatabaseSync`;禁止
组件直接订阅 `window.goodbuddy.*.on*`;限制 `App.tsx` 规模;`PERF-11` 关键指标设置回归阈值。
- **验收:** 违反规则的改动在 lint 或基准阶段失败。
- **优先级 / 状态:** P2 / 部分实施(已有 lint 与历史回归上限;间接访问、单调收紧及并发性能检查待补)
- **范围:** 按实际执行环境检查 Main 同步文件操作、直接和间接 SQLite 打开;存储宿主、受管 Worker、
测试及远端 Agent 的合法连接按[并发验收的自动检查规则](../quality/concurrent-desktop-acceptance.md#自动检查与交付记录)列明,
不按目录一概禁止。禁止组件直接订阅 `window.goodbuddy.*.on*`,限制 `App.tsx` 规模,
为 `PERF-11` 及并发场景建立符合性能原则的检查。
- **验收:** 违反规则的改动在 lint 或基准阶段失败;本次完整架构所需守护随同一候选交付。
历史性能回归上限不能替代规范预算,无 CI/性能环境结果时不标记验收通过。
## 8. 体验与可访问性
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

+1
View File
@@ -132,6 +132,7 @@ export default defineConfig({
input: {
index: resolve('src/main/index.ts'),
'assistant-storage-worker': resolve('src/main/assistant-storage-worker.ts'),
'desktop-storage-entry': resolve('src/main/desktop-storage-entry.ts'),
'readonly-query-worker': resolve('src/main/readonly-query-worker.ts'),
'document-parse-worker': resolve('src/main/document-parse-worker.ts'),
'wechat-sidecar': resolve(
+80 -32
View File
@@ -12,9 +12,8 @@ import tseslint from 'typescript-eslint'
// assigns it (data process, async fs, domain store, layout shell).
// ---------------------------------------------------------------------------
// PERF-16: SQLite belongs to a dedicated data process (utilityProcess); Main
// only routes messages. Files that imported DatabaseSync from 'node:sqlite'
// (including `import type`) on 2026-10-02. Shrink only.
// Concrete SQLite implementations, executed by storage owners or the remote Agent.
// Type imports do not grant permission to execute SQLite.
const databaseSyncAllowlist = [
'src/agent-daemon/agent-model-gateway.ts',
'src/agent-daemon/event-journal.ts',
@@ -24,19 +23,83 @@ const databaseSyncAllowlist = [
'src/main/agent/runtime-session-binding-store.ts',
'src/main/assistant/assistant-database.ts',
'src/main/assistant/assistant-storage-upgrade.ts',
'src/main/assistant/story-graph-reader.ts',
'src/main/assistant/subagent-progress-storage.ts',
'src/main/assistant/supervision-experiences.ts',
'src/main/assistant/supervision-review-store.ts',
'src/main/assistant/supervision-stories.ts',
'src/main/assistant/supervision-suggestions.ts',
'src/main/assistant/supervision-timeline.ts',
'src/main/conversation-attachment-storage.ts',
'src/main/knowledge/external/external-knowledge-store.ts',
'src/main/knowledge/knowledge-database.ts',
'src/shared/node/private-sqlite-database.ts'
]
// Explicit execution contexts, not a src/main or *worker* naming exemption.
const sqliteExecutionContexts = [
'src/agent-daemon/agent-owned-acp-prompt.ts',
'src/agent-daemon/daemon.ts',
'src/agent-daemon/direct-linux-stdio-process-owner.ts',
'src/agent-daemon/index.ts',
'src/agent-daemon/runtime-composition.ts',
'src/main/desktop-storage-owner.ts',
'src/main/desktop-storage-files.ts',
'src/main/desktop-storage-runtime-operations.ts',
'src/main/desktop-storage-entry.ts',
'src/main/desktop-storage-runtime-host-fixture.ts',
'src/main/readonly-query-worker.ts',
'src/main/assistant-storage-worker.ts',
'src/main/agent/private-sqlite-database.ts'
]
// Match known module entry points, including the legacy opener re-export. This
// guards imports rather than trying to infer arbitrary transitive call graphs.
const sqliteValueImports = {
'node:sqlite': ['DatabaseSync'],
'private-sqlite-database': ['openPrivateSqliteDatabase', 'PreparedPrivateSqliteDatabaseFile'],
'assistant-database': ['AssistantDatabase'],
'knowledge-database': ['KnowledgeDatabase'],
'external-knowledge-store': ['ExternalKnowledgeStore'],
'conversation-attachment-storage': ['ConversationAttachmentStorage'],
'runtime-session-binding-store': ['SqliteRuntimeSessionBindingStore'],
'agent-model-gateway': ['AgentModelCallLedger'],
'event-journal': ['EventJournal'],
'runtime-owner-registry': ['RuntimeOwnerRegistry'],
'semantic-prompt-store': ['SemanticPromptStore'],
'assistant-storage-upgrade': ['getPendingAssistantStorageUpgrade', 'upgradeAssistantStorage'],
'desktop-storage-owner': ['DesktopStorageOwner'],
'desktop-storage-files': ['openDesktopStorageFiles'],
'desktop-storage-runtime-operations': ['DesktopStorageRuntimeOwner']
}
const sqliteExecutionRule = {
meta: { type: 'problem', schema: [], messages: {
owner: 'SQLite value access belongs in an explicit storage owner, worker or remote Agent context (SA-07). Main must use the async storage interface; type imports are allowed.'
} },
create(context) {
function check(node, source, specifiers) {
if (node.importKind === 'type' || node.exportKind === 'type') return
const path = source?.type === 'TemplateLiteral' && source.expressions.length === 0
? source.quasis[0].value.cooked : source?.value
if (typeof path !== 'string') return
const name = path.split('/').at(-1).replace(/\.(?:[cm]?[jt]s)$/, '')
const restricted = /(?:^|\/)agent-daemon(?:\/index(?:\.[cm]?[jt]s)?)?$/.test(path)
? ['AgentModelCallLedger', 'EventJournal', 'RuntimeOwnerRegistry', 'SemanticPromptStore']
: sqliteValueImports[name]
if (!restricted) return
if (!specifiers || specifiers.length === 0 || specifiers.some(specifier => {
if (specifier.importKind === 'type' || specifier.exportKind === 'type') return false
const imported = specifier.imported ?? specifier.local
return !['ImportSpecifier', 'ExportSpecifier'].includes(specifier.type)
|| restricted.includes(imported?.name ?? imported?.value)
})) context.report({ node, messageId: 'owner' })
}
return {
ImportDeclaration: node => check(node, node.source, node.specifiers),
ExportNamedDeclaration: node => check(node, node.source, node.specifiers),
ExportAllDeclaration: node => check(node, node.source),
ImportExpression: node => check(node, node.source),
CallExpression: node => {
if (node.callee.type === 'Identifier' && node.callee.name === 'require') check(node, node.arguments[0])
},
TSImportEqualsDeclaration: node => check(node, node.moduleReference.expression)
}
}
}
// PERF-15/PERF-16 rule 1: Main must not block its event loop on file IO.
// Synchronous fs functions; also caught as `fs.xxxSync` member access.
const syncFsFunctions = [
@@ -48,15 +111,9 @@ const syncFsFunctions = [
'symlinkSync', 'truncateSync', 'unlinkSync', 'utimesSync', 'writeFileSync', 'writeSync', 'writevSync'
]
const syncFsMessage = 'Synchronous fs APIs block the Main event loop (PERF-17). Use node:fs/promises, or move the work to the data process (PERF-16).'
const databaseSyncRestriction = [{
name: 'node:sqlite',
importNames: ['DatabaseSync'],
message: 'DatabaseSync may only be used by the data process (PERF-16); other code must go through its async interface (PERF-15). The allowlist in eslint.config.js must only shrink.'
}]
const syncFsRestriction = ['node:fs', 'fs', 'original-fs'].map(name => ({ name, importNames: syncFsFunctions, message: syncFsMessage }))
// Files under src/main (non-test) that used synchronous fs APIs on 2026-10-02. Shrink only.
const mainSyncFsAllowlist = [
'src/main/agent/paged-output-store.ts',
'src/main/assistant/assistant-database.ts',
'src/main/assistant/assistant-storage-upgrade.ts',
'src/main/conversation-attachment-storage.ts',
@@ -98,6 +155,7 @@ export default tseslint.config(
'docs/**/vendor/**',
'node_modules/**',
'out/**',
'temp/**',
'shareserver/**'
]
},
@@ -170,26 +228,16 @@ export default tseslint.config(
}
},
// --- Architecture ratchets (PERF-17), see allowlists at the top. ---------
// Rules a (DatabaseSync) and b (sync fs) share `no-restricted-imports`, and a
// later flat-config block replaces an earlier block's options for the same
// rule. The blocks below therefore partition the files so that each file
// gets exactly the restrictions it is subject to.
// DatabaseSync outside the data process (all of src, both value and type imports).
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['**/*.test.{ts,tsx}', ...databaseSyncAllowlist],
rules: { 'no-restricted-imports': ['error', { paths: databaseSyncRestriction }] }
files: ['src/**/*.{ts,tsx,js,mjs,cjs}'],
ignores: ['**/*.test.{ts,tsx,js}', ...databaseSyncAllowlist, ...sqliteExecutionContexts],
plugins: { architecture: { rules: { 'sqlite-execution-context': sqliteExecutionRule } } },
rules: { 'architecture/sqlite-execution-context': 'error' }
},
// Synchronous fs in Main: files not allowlisted for either rule get both restrictions.
// Synchronous fs in Main.
{
files: ['src/main/**/*.ts'],
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist, ...databaseSyncAllowlist],
rules: { 'no-restricted-imports': ['error', { paths: [...databaseSyncRestriction, ...syncFsRestriction] }] }
},
// Main files allowlisted for DatabaseSync but not for sync fs.
{
files: databaseSyncAllowlist.filter(file => file.startsWith('src/main/')),
ignores: mainSyncFsAllowlist,
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist],
rules: { 'no-restricted-imports': ['error', { paths: syncFsRestriction }] }
},
// `fs.readFileSync(...)` style access through a namespace/default import.
+1559 -20
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "goodbuddy",
"version": "0.15.11",
"version": "0.16.1",
"private": true,
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
"desktopName": "GoodBuddy",
@@ -20,6 +20,9 @@
"type": "module",
"scripts": {
"dev": "electron-vite dev",
"docs:dev": "vitepress dev sites/docs --config sites/docs/.vitepress/config.mts",
"docs:build": "vitepress build sites/docs --outDir sites/docs-dist --config sites/docs/.vitepress/config.mts",
"docs:preview": "vitepress preview sites/docs-dist",
"start": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.agent.json && tsc --noEmit -p tsconfig.web.json",
"lint": "eslint .",
@@ -419,6 +422,7 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
"vite": "^7.3.6",
"vitepress": "^1.6.4",
"vitest": "^4.1.10"
},
"optionalDependencies": {
+21
View File
@@ -0,0 +1,21 @@
# GoodBuddy Agent 0.15.5 更新内容
## 问题修复
- **原生启动期间仍可取消和查询**:OpenCode 或 Continue 初始化等待不再占住全局控制队列,其他绑定、取消和看门狗可以继续处理。同一启动保持合并与冲突校验,取消后的迟到 Session 单独关闭,不重发原 Prompt。
- **模型路由清理不阻塞其他任务**:取消时的路由清理等待不再阻塞全局控制操作,迟到安装按所属 Session 和 operation 清除,不影响仍在运行的其他会话。真实 Linux x64 Host 已验证两个 Runtime 的工具、取消、断开恢复及回放去重,不代表所有平台和恢复场景均已覆盖。
## 升级须知
先升级至 **Desktop 0.15.12**,再下载此包并更新 Host。已有进程不会热更新,请先妥善处理活动任务;runtime/acp 6、wire 2.0、Node 24.19.0、OpenCode 1.18.29 和 Continue 1.5.47 保持不变。
# What's New in GoodBuddy Agent 0.15.5
## Bug Fixes
- **Keep cancellation and queries responsive during native startup**: Waiting for OpenCode or Continue initialization no longer holds the global control queue, so other bindings, cancellation, and watchdogs can progress. Duplicate startup coalescing and conflict checks remain, and late sessions after cancellation are closed separately without replaying the original prompt.
- **Avoid blocking other tasks during model-route cleanup**: Cancellation no longer holds global control operations while waiting for route cleanup, and late installations are removed for their owning session and operation without affecting active peers. Real Linux x64 Host checks covered both Runtimes' tools, cancellation, reconnection, and replay deduplication, not every platform or recovery scenario.
## Upgrade Notes
Upgrade to **Desktop 0.15.12** before downloading this package and updating the Host. Running processes are not hot-updated, so handle active tasks first; runtime/acp 6, wire 2.0, Node 24.19.0, OpenCode 1.18.29, and Continue 1.5.47 remain unchanged.
+21
View File
@@ -0,0 +1,21 @@
# GoodBuddy Agent 0.15.6 更新内容
## 问题修复
- **配合桌面存储 owner 迁移**:远程 Runtime、工具、图片和历史路径继续使用既有 Agent 会话、权限和回放语义;本版包含与 Desktop 0.15.13 配套的 Runtime 调整。
- **工具失败后继续请求**:直连模型在工具执行失败后可继续下一轮,分页输出和下一轮请求保持有界;取消、资源清理和错误上下文继续保留。
## 升级须知
先升级 **Desktop 0.15.13**,再下载此包并更新 Host。runtime/acp 6、wire 2.0、Node 24.19.0、OpenCode 1.18.29 和 Continue 1.5.47 保持不变。已有 Agent 进程不会热更新,请先处理活动任务。
# What's New in GoodBuddy Agent 0.15.6
## Bug Fixes
- **Support the desktop storage-owner migration**: Remote Runtime, tool, image, and history paths retain existing Agent session, permission, and replay semantics; this package contains the matching Runtime changes for Desktop 0.15.13.
- **Continue after tool failures**: Direct-model requests can continue after tool execution failures while paged output and next-turn requests remain bounded. Cancellation, resource cleanup, and error context remain intact.
## Upgrade Notes
Upgrade to **Desktop 0.15.13** before downloading this package and updating the Host. runtime/acp 6, wire 2.0, Node 24.19.0, OpenCode 1.18.29, and Continue 1.5.47 remain unchanged. Running Agent processes are not hot-updated; handle active tasks first.
+176
View File
@@ -1,6 +1,182 @@
{
"formatVersion": 1,
"releases": [
{
"version": "0.16.1",
"releasedAt": "2026-10-10",
"notes": {
"zh-CN": {
"highlights": [],
"features": [],
"fixes": [
"**让消息自然延伸到输入框边缘**:修复聊天消息在输入框上方提前被实色区域截断的问题,现在消息会沿着输入框轮廓自然淡出,输入区和消息区的过渡更平滑。",
"**改善输入框与消息区的主题适配**:调整浅色和深色主题下的渐变、背景和焦点边界,减少输入框上方出现粗边或明显横线的问题;滚动到底部按钮也会保持合适的可见位置。"
],
"notices": [
"本版本不新增数据库 schema,Agent 0.15.6 无需更新。"
]
},
"en-US": {
"highlights": [],
"features": [],
"fixes": [
"**Let messages fade naturally to the composer edge**: Chat messages no longer stop at a solid band above the composer. They now fade along the composer outline for a smoother transition between the message area and input area.",
"**Improve composer and message-area theme behavior**: Light and dark theme gradients, backgrounds, and focus boundaries are refined to reduce thick edges and visible horizontal bands above the composer; the scroll-to-bottom control also remains positioned for clear access."
],
"notices": [
"This release adds no database schema version. Agent 0.15.6 does not need to be updated."
]
}
}
},
{
"version": "0.16.0",
"releasedAt": "2026-10-09",
"notes": {
"zh-CN": {
"highlights": [],
"features": [
"**在输入框中选择专家或团队角色**:发送消息前可以直接在输入框顶部选择专家或团队角色,选择入口更接近实际发送操作。每个会话独立保存自己的选择,切换会话时不会误用其他会话的角色。",
"**保留历史消息使用的角色**:已发送的回复会保留请求当时使用的专家或团队角色,即使之后重命名或删除该角色,历史记录仍能准确显示当时的使用情况。"
],
"fixes": [],
"notices": [
"本版本不新增数据库 schema,Agent 0.15.6 无需更新。"
]
},
"en-US": {
"highlights": [],
"features": [
"**Choose an expert or team role from the composer**: Select an expert or team role directly at the top of the composer before sending a message. Each conversation keeps its own selection, so switching conversations does not accidentally carry over another conversation’s role.",
"**Preserve the role used by historical messages**: Sent replies retain the expert or team role selected when the request was made. Later renaming or removing that role does not make existing history lose the context of how it was produced."
],
"fixes": [],
"notices": [
"This release adds no database schema version. Agent 0.15.6 does not need to be updated."
]
}
}
},
{
"version": "0.15.14",
"releasedAt": "2026-10-09",
"notes": {
"zh-CN": {
"highlights": [],
"features": [],
"fixes": [
"**恢复历史监督回顾的继续操作**:对于旧版本遗留的“来源已变化或已移除”失败回顾,现在可以直接从监督活动中继续,而不必重新开始整个回顾。",
"**让变化中的来源能够有界地重新纳入回顾**:来源在回顾期间发生变化时,系统会重新冻结并处理有限次数;持续变化的来源会被省略,避免回顾无法结束。已删除、归档或不再可用的来源会按现有规则安全省略,同时保留其余来源的进度。",
"**改进监督活动状态**:监督活动继续保留暂停、失败、取消和已保存批次的状态,继续操作不会绕过项目范围和来源处理规则。",
"**让设置分类更易识别**:设置页的分类现在显示对应图标,帮助用户更快找到外观、模型、Runtime、通道和其他设置。"
],
"notices": [
"本版本不新增数据库 schema,继续使用 schema 62。监督回顾可能产生模型调用费用;持续变化的来源会被有限次数地重新处理,之后自动省略。"
]
},
"en-US": {
"highlights": [],
"features": [],
"fixes": [
"**Resume historical Supervisor reviews**: Reviews left in the legacy “source changed or was removed” failure state can now be continued directly from Supervisor Activity instead of restarting the entire review.",
"**Re-review changing sources within a bound**: When a source changes during a review, GoodBuddy re-freezes and processes it for a limited number of attempts. A source that keeps changing is then omitted so the review can finish. Deleted, archived, or unavailable sources continue to be omitted under the existing rules while surviving progress is retained.",
"**Improve Supervisor activity state**: Paused, failed, cancelled, and partially saved review states remain available, and resuming a review continues to follow the project-scope and source-processing rules.",
"**Make settings categories easier to scan**: Settings categories now show matching icons, helping users find Appearance, Models, Runtime, Channels, and other settings more quickly."
],
"notices": [
"This release adds no database schema version and remains on schema 62. Supervisor reviews may use model calls; continuously changing sources are reprocessed a limited number of times and then omitted."
]
}
}
},
{
"version": "0.15.13",
"releasedAt": "2026-10-07",
"notes": {
"zh-CN": {
"highlights": [],
"features": [
"**把桌面业务 SQLite 交给独立存储进程**:生产路径现在由一个 storage utility process 统一拥有业务数据库,Assistant、Knowledge、附件、运行绑定和账本等域通过类型化异步操作访问。保留原有事务、顺序、提交后确认和数据路径;进程异常、关闭排空和就绪失败会显式反馈,不使用第二个 Main 同步写入者。",
"**让长输出和工具失败后的对话继续可用**:直连模型在工具执行失败后可继续请求,分页输出保持有界并保留下一轮所需正文;桌面和远程 Runtime 的工具、图片、知识和历史绑定继续沿用原有权限和会话隔离。Agent 0.15.6 包含配套 Runtime 调整,需与 Desktop 一起升级。"
],
"fixes": [
"**减少存储迁移中的重复工作与等待**:监督、知识、附件和运行时访问复用同一存储 owner,读取 Worker 仍使用有界快照;监督来源、图谱、摘要和文件操作不再分别创建业务数据库 owner。utility IPC 对请求、取消、容量、进程退出和提交结果有明确边界,未完成的全量并发验收仍不宣称通过。",
"**改进 Magic Notes 标签和项目设置入口**:标签管理保留空的全局标签、重复标签错误保留原始原因;直接打开设置路由时预加载对应设置,文件列表元数据更简洁,聊天 Composer 过渡更平滑。",
"**放宽监督者可识别的来源别名**:历史来源使用短引用或旧主键时,生产监督流程可通过已知映射解析,不因非 UUID 的历史标识直接拒绝有效来源。删除会话、暂停、重试和项目范围规则继续生效。",
"**移除未被产品支持的并发限制假设**:存储和并发代码不再依赖未经支持的队列容量或 Worker 队列上限;明确的资源边界、取消、排空和错误传播仍保留,未将目标架构文档中的性能预算写成已达标结论。"
],
"notices": [
"本轮不新增数据库 schema 版本,沿用 schema 62;升级前仍建议关闭应用并备份完整数据目录、相邻 `notes/` 和设置文件。存储 owner 迁移不改变数据库路径、表、ID、事务语义或远程 Agent 数据库。",
"远程 Runtime 路径有生产代码变化,请先升级 Desktop 0.15.13,再下载 Agent 0.15.6 并更新 Host;已有进程不会热更新。runtime/acp 6、wire 2.0、Electron、Node、OpenCode 和 Continue 固定版本不变。",
"并发验收规范要求的 C01–C07 全矩阵、跨平台存储进程安装和低端机性能对照仍是后续验收项。本轮 focused storage/监督测试、类型检查和 lint 证据不替代这些完整验收。"
]
},
"en-US": {
"highlights": [],
"features": [
"**Move desktop business SQLite ownership to a utility process**: Production paths now use one storage utility process as the business-database owner, with typed asynchronous operations for Assistant, Knowledge, attachments, runtime bindings, and ledgers. Existing transactions, ordering, commit-before-confirmation, and data paths remain; process failure, drain-on-close, and readiness errors are explicit, with no second synchronous Main writer.",
"**Keep conversations usable after long output and tool failures**: Direct models can continue after tool execution failures, while paged output remains bounded and retains the content needed by the next request. Desktop and remote Runtimes retain existing tool, image, knowledge, history, permission, and session isolation; Agent 0.15.6 contains the matching Runtime changes and should be upgraded with Desktop."
],
"fixes": [
"**Reduce duplicate work and waits during storage migration**: Supervisor, Knowledge, attachments, and Runtime access reuse one storage owner, while read Workers retain bounded snapshots; review sources, graphs, summaries, and file operations no longer create separate business-database owners. Utility IPC defines request, cancellation, capacity, process-exit, and committed-result boundaries; the full concurrency matrix is not claimed complete.",
"**Improve Magic Notes tags and direct settings entry**: Tag management preserves empty global tags and original causes for duplicate-tag errors; direct settings routes preload their target settings, file-list metadata is simpler, and the transition above the chat composer is smoother.",
"**Accept more historical Supervisor source aliases**: Production review flows resolve short references and known legacy keys through the existing mapping instead of rejecting valid historical sources solely because they are not UUIDs. Conversation deletion, pause, retry, and project-scope rules remain active.",
"**Remove unsupported assumptions about concurrency limits**: Storage and concurrency code no longer relies on unsupported queue-capacity or Worker-queue limits. Explicit resource bounds, cancellation, draining, and error propagation remain, and target-architecture performance budgets are not presented as achieved."
],
"notices": [
"This release adds no database schema version and remains at schema 62; close the app and back up the complete data directory, adjacent `notes/`, and settings files before upgrading. Storage ownership does not change database paths, tables, IDs, transaction semantics, or remote Agent databases.",
"The remote Runtime path changed in production code. Upgrade to Desktop 0.15.13 first, then download Agent 0.15.6 and update the Host; running processes are not hot-updated. runtime/acp 6, wire 2.0, and pinned Electron, Node, OpenCode, and Continue versions remain unchanged.",
"The C01–C07 concurrency matrix, cross-platform storage-process installation, and low-end performance comparisons remain follow-up acceptance work. Focused storage/Supervisor tests, type checks, and lint do not replace that complete validation."
]
}
}
},
{
"version": "0.15.12",
"releasedAt": "2026-10-05",
"notes": {
"zh-CN": {
"highlights": [],
"features": [
"**新增 Telegram 私聊通道,真实平台联调待完成**:可配置自建 Bot 的 Token 和数字用户白名单,通过通道项目处理文字请求并回复,使用 Electron 网络以支持系统代理。可先留空发送者 ID、启用并保存,再向 Bot 发送 `/whoami` 获取自己的 ID 后重新保存;连接测试不等于开始接收消息,真实 Bot 与模型完整收发仍待验证。",
"**按自定义时间回顾工作**:除最近 1、7、30 天,还可输入正整数天数或指定本地开始、结束日期。指定日期包含结束日全天,今天截到提交时刻,并校验未来日期及逆序区间;普通回顾仍跳过已处理且未变化的来源,重新整理需明确确认。"
],
"fixes": [
"**减少监督者打开和切换时的等待**:概览不再提前加载完整图谱,来源正文按需读取,页面重查询交给现有 Worker,故事摘要复用结果,长选择列表只挂载附近条目。保留来源详情、键盘、焦点和历史选择;大型回顾最终发布仍可能短暂阻塞主进程,不宣称所有性能预算已达标。",
"**删除会话后继续未发布的回顾**:已删除会话涉及的未发布批次和依赖汇总会失效,混合批次按剩余来源重算,其余来源继续,并显示省略数量。历史已发布结果保持不变,全部来源删除时不生成空报告;已发送给模型的内容无法撤回,剩余来源修改或归档仍会要求处理冲突。",
"**修正回顾隔离、暂停与模型失败处理**:关闭跨项目时按当前项目读取候选,暂停后不再因晚到查询继续发布,故事分配拒绝已移除或合并的目标。临时网络错误、限流及部分服务端错误最多额外重试一次,保留已报告用量;认证、配置、取消、超时和容量错误不因此重试,额外尝试可能增加费用。",
"**让 Runtime 失败和取消及时结束**:远程原生启动和模型路由清理不再占住全局控制队列,其他会话的查询、取消和看门狗仍可推进,并清除迟到路由。修复本地 OpenCode 提交失败后持续等待的问题,保留原错误;远程修复需更新 Agent 0.15.5。",
"**保留通道待发结果并显示交付错误**:修复生成图片结果在通道契约处被拒、钉钉 HTTP 成功但业务失败被误记为送达的问题。Telegram 异常后原样保存可重启接收,轮询冲突时保留身份匹配的待发回复,发送错误可见,限流等待跨重试层保留并可取消。",
"**保留长会话展开状态并区分缺失计时**:浏览长会话时,展开过的详情在滚出再滚回后保持,仅保存用户修改过的状态,删除来源后清理。历史执行时长缺失不再显示成零,旧记录不通过扫描历史补算。"
],
"notices": [
"首次启动升级到 schema 62:schema 61 扩展 Telegram 通道约束,schema 62 增加监督查询索引,不改写用户正文。升级前请关闭应用并备份完整数据目录,至少包括数据库、相邻 `notes/` 和设置文件;大库建索引可能耗时,旧客户端回退需恢复升级前备份。",
"远程修复请先升级 Desktop 0.15.12,再下载 Agent 0.15.5 并更新 Host;已有进程不会热更新,更新前妥善处理活动任务。runtime/acp 6、Electron、Node、OpenCode 和 Continue 的固定版本不变。",
"继续使用无 Ask/Execute 模式的统一执行,请求按启用能力及执行账号权限运行。Telegram 真实平台完整联调、跨平台性能和独立存储服务迁移不属于已完成验证;高频本地写入继续沿用 NORMAL 的断电持久性取舍。"
]
},
"en-US": {
"highlights": [],
"features": [
"**Add Telegram private messaging, with live-platform acceptance pending**: Configure a self-created bot token and numeric sender allowlist to handle text requests through channel projects and send replies, using Electron networking for system proxy support. Sender IDs can initially be blank: enable and save, send `/whoami` to the bot, then save your ID; connection testing is not message reception, and complete real-bot-plus-model delivery remains unverified.",
"**Review a custom time range**: Alongside the last 1, 7, or 30 days, enter a positive number of days or choose local start and end dates. Date ranges include the whole end date, cap today at submission time, and reject future or reversed dates; ordinary reviews still skip unchanged processed sources, while reanalysis requires explicit confirmation."
],
"fixes": [
"**Reduce waiting when opening and switching Supervisor views**: Overview no longer preloads the complete graph, source text loads on demand, heavy page reads use the existing Worker, story digests are reused, and long selection lists mount nearby rows. Source details, keyboard access, focus, and history selection remain available; large final review publication can still briefly block Main, so not all performance budgets are claimed met.",
"**Continue unpublished reviews after deleting conversations**: Unpublished batches and dependent summaries containing deleted conversations become invalid, mixed batches recompute surviving sources, and remaining work continues with an omitted-source count. Published historical results remain unchanged and deleting all sources produces no empty report; already-sent model input cannot be recalled, and changes or archival of surviving sources still require conflict handling.",
"**Correct review isolation, pause, and model failure handling**: Candidate reads honor the current project when cross-project work is disabled, late queries cannot publish after pause, and story assignment rejects removed or merged destinations. Transient network errors, rate limits, and selected server failures allow at most one additional attempt while retaining reported usage; authentication, configuration, cancellation, timeout, and capacity errors do not gain this retry, and extra attempts can add cost.",
"**Finish Runtime failures and cancellation promptly**: Remote native startup and model-route cleanup no longer hold the global control queue, allowing other conversations, cancellation, and watchdogs to progress while removing late routes. Local OpenCode submission failures now end the wait and preserve their original errors; remote fixes require Agent 0.15.5.",
"**Preserve pending channel replies and expose delivery errors**: Generated-image results no longer fail the channel contract, and DingTalk business failures in HTTP-success responses are no longer marked delivered. Saving unchanged Telegram settings can restart failed reception, polling conflicts retain identity-matched pending replies, send errors remain visible, and cancellable rate-limit waits survive retry-layer boundaries.",
"**Retain expanded conversation details and distinguish missing timing**: Details expanded in long conversations stay expanded after scrolling away and back, storing only user overrides and clearing deleted sources. Missing historical execution duration is no longer displayed as zero, and old timing is not reconstructed by scanning history."
],
"notices": [
"First launch upgrades to schema 62: schema 61 extends Telegram channel constraints and schema 62 adds Supervisor lookup indexes without rewriting user text. Close the app and back up the complete data directory, including at least the database, adjacent `notes/`, and settings files; indexing large databases may take time, and downgrading requires the pre-upgrade backup.",
"For remote fixes, upgrade to Desktop 0.15.12, then download Agent 0.15.5 and update the Host; running processes are not hot-updated, so handle active tasks before updating. runtime/acp 6 and pinned Electron, Node, OpenCode, and Continue versions remain unchanged.",
"Unified execution remains in place without Ask/Execute modes, using enabled capabilities and execution-account permissions. Full live Telegram acceptance, cross-platform performance, and an independent storage-service migration are not completed validation; frequent local writes retain the NORMAL power-loss durability tradeoff."
]
}
}
},
{
"version": "0.15.11",
"releasedAt": "2026-10-04",
+1 -1
View File
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { parseEnv } from 'node:util'
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
import { extractExperiences } from '../src/main/assistant/supervision-experiences.ts'
import { extractExperiences } from '../src/main/assistant/experience-extraction-service.ts'
const [prepared, envPath, directory, maxCallsArg] = process.argv.slice(2)
const config = parseEnv(readFileSync(envPath, 'utf8'))
+145
View File
@@ -0,0 +1,145 @@
/* global process, console, setTimeout, clearTimeout */
import { spawn, spawnSync } from 'node:child_process'
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const script = fileURLToPath(import.meta.url)
const root = resolve(dirname(script), '..')
const base = join(root, 'temp', 'goodbuddy-final-tests-final')
const stateFile = join(base, 'status.json')
const lockFile = join(base, 'runner.lock')
const npm = join(root, 'node_modules', 'npm', 'bin', 'npm-cli.js')
const [mode = 'status', selection = 'all', ...filters] = process.argv.slice(2)
const alive = (pid) => {
try { process.kill(pid, 0); return true } catch { return false }
}
const readState = () => JSON.parse(readFileSync(stateFile, 'utf8'))
const killTree = (pid) => {
if (!pid || !alive(pid)) return
if (process.platform === 'win32') {
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, timeout: 15000 })
} else {
try { process.kill(-pid, 'SIGKILL') } catch { /* Already exited. */ }
}
}
if (mode === 'start') {
if (!['all', 'test', 'typecheck', 'lint'].includes(selection)) throw new Error('Unknown validation selection')
if (!statSync(join(root, 'temp')).isDirectory()) throw new Error('Repository temp directory is required')
mkdirSync(base, { recursive: true })
if (existsSync(lockFile)) {
const pid = Number(readFileSync(lockFile, 'utf8'))
if (alive(pid)) throw new Error(`Validation runner already active: ${pid}`)
rmSync(lockFile)
}
const lock = openSync(lockFile, 'wx')
writeFileSync(lock, String(process.pid))
closeSync(lock)
const log = openSync(join(base, 'runner.log'), 'a')
try {
const child = spawn(process.execPath, [script, 'worker', selection, ...filters], {
cwd: root, detached: true, windowsHide: true, stdio: ['ignore', log, log]
})
await new Promise((resolveSpawn, reject) => {
child.once('spawn', resolveSpawn)
child.once('error', reject)
})
writeFileSync(lockFile, String(child.pid))
child.unref()
console.log(JSON.stringify({ runnerPid: child.pid, selection, logs: base }))
} catch (error) {
rmSync(lockFile, { force: true })
throw error
} finally { closeSync(log) }
} else if (mode === 'status') {
const state = readState()
console.log(JSON.stringify({ ...state, runnerAlive: alive(state.pid) }, null, 2))
} else if (mode === 'stop') {
const state = readState()
if (state.status === 'running') {
killTree(state.childPid)
killTree(state.pid)
state.status = 'stopped'
state.endedAt = new Date().toISOString()
writeFileSync(stateFile, JSON.stringify(state, null, 2))
rmSync(lockFile, { force: true })
}
} else if (mode === 'worker') {
const run = join(base, new Date().toISOString().replace(/[:.]/g, '-'))
const tmp = join(run, 'tmp')
mkdirSync(tmp, { recursive: true })
if (!statSync(tmp).isDirectory()) throw new Error('Child temp directory was not created')
const state = { pid: process.pid, status: 'running', startedAt: new Date().toISOString(), run, results: [] }
const save = () => writeFileSync(stateFile, JSON.stringify(state, null, 2))
let child
let interrupted = false
const interrupt = (reason) => {
interrupted = true
state.reason = reason
killTree(child?.pid)
}
process.on('SIGINT', () => interrupt('SIGINT'))
process.on('SIGTERM', () => interrupt('SIGTERM'))
process.on('exit', () => killTree(child?.pid))
const deadline = setTimeout(() => interrupt('30-minute overall deadline exceeded'), 30 * 60 * 1000)
save()
try {
for (const job of selection === 'all' ? ['test', 'typecheck', 'lint'] : [selection]) {
if (interrupted) break
const started = Date.now()
const report = join(run, 'tests.json')
const args = job === 'test'
? ['test', '--', ...filters, '--reporter=default', '--reporter=json', `--outputFile.json=${report}`]
: ['run', job]
const logPath = join(run, `${job}.log`)
const log = openSync(logPath, 'w')
try {
child = spawn(process.execPath, [npm, ...args], {
cwd: root, windowsHide: true, detached: process.platform !== 'win32',
env: { ...process.env, TEMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMPDIR: join(tmpdir(), 'goodbuddy-final-tests-final'), NO_COLOR: '1' },
stdio: ['ignore', log, log]
})
state.active = job
state.childPid = child.pid
save()
const result = await new Promise((resolveExit, reject) => {
child.once('error', reject)
child.once('exit', (code, signal) => resolveExit({ code, signal }))
})
const entry = { job, ...result, seconds: (Date.now() - started) / 1000, log: logPath }
if (job === 'test' && existsSync(report)) {
const data = JSON.parse(readFileSync(report, 'utf8'))
entry.totals = Object.fromEntries(Object.entries(data).filter(([key]) => key.startsWith('num') || key === 'success'))
entry.failedFiles = data.testResults.filter((file) => file.status === 'failed').map((file) => ({
file: file.name.replace(root, '').replaceAll('\\', '/'),
tests: file.assertionResults.filter((test) => test.status === 'failed').map((test) => test.fullName)
}))
}
state.results.push(entry)
} finally {
killTree(child?.pid)
child = undefined
closeSync(log)
delete state.childPid
delete state.active
save()
}
}
state.status = interrupted ? 'interrupted' : 'completed'
} catch (error) {
state.status = 'error'
state.reason = error.message
} finally {
clearTimeout(deadline)
killTree(child?.pid)
try { rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }) }
catch (error) { state.cleanupError = error.code }
state.endedAt = new Date().toISOString()
save()
rmSync(lockFile, { force: true })
}
} else {
throw new Error('Usage: node scripts/final-regression.mjs start [all|test|typecheck|lint] [test filters...] | status | stop')
}
+1 -1
View File
@@ -86,7 +86,7 @@ try {
'CANDIDATES:', JSON.stringify(request.candidates), 'Return only JSON.'].join('\n\n')),
{ supervisionRunId, heartbeatRunId: run.id })
})
const plan = heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
const plan = await heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
let tick = 0
const due = async () => {
+1 -1
View File
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { parseEnv } from 'node:util'
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
import { assignStories } from '../src/main/assistant/supervision-stories.ts'
import { assignStories } from '../src/main/assistant/story-assignment-service.ts'
const [prepared, envPath, directory, mode, maxCallsArg] = process.argv.slice(2)
const config = parseEnv(readFileSync(envPath, 'utf8'))
+147
View File
@@ -0,0 +1,147 @@
// Synthetic SQLite / bounded live validation for A03-A05. Never opens a user database.
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
import { isAbsolute, join, relative, resolve } from 'node:path'
import { parseEnv } from 'node:util'
import { build } from 'esbuild'
import type { DatabaseSync } from 'node:sqlite'
import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
import { supervisionReviewSettingsSchema } from '../src/shared/supervision-review-contracts'
import { ReadonlyQueryReader } from '../src/main/readonly-query-reader'
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
async function main() {
const [mode, parent, envPath] = process.argv.slice(2)
assert((mode === 'scale' || mode === 'live') && parent)
const rel = relative(process.cwd(), resolve(parent))
assert(rel.startsWith('..') || isAbsolute(rel), 'Output must be outside repository')
const directory = await mkdtemp(join(resolve(parent), 'supervision-audit-'))
const path = join(directory, 'assistant.sqlite')
const worker = join(directory, 'readonly-query-worker.cjs')
await build({ entryPoints: [resolve('src/main/readonly-query-worker.ts')], outfile: worker,
bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
const db = new AssistantDatabase(path)
db.initialize(directory)
const reader = new ReadonlyQueryReader('assistant', path, worker)
const storage = asyncSupervisionStorage(db, undefined, reader).supervision
const sql = (db as unknown as { requireDatabase(): DatabaseSync }).requireDatabase()
const pool = new SupervisionModelPool()
const report: Record<string, unknown> = { mode, directory, externalCalls: 0 }
let calls = 0
const started = performance.now()
const request: SupervisionRunRequest = { trigger: 'manual', scope: { kind: 'global' },
timeRange: { from: '2026-10-01T00:00:00.000Z', to: '2026-10-02T00:00:00.000Z' } }
const empty = { summary: 'Synthetic audit', changeDigest: '', openItems: [], events: [], entities: [], entityChanges: [], relations: [] }
try {
if (mode === 'scale') {
const projects = [db.listProjects()[0]!, ...Array.from({ length: 9 }, (_, i) => db.createProject({ name: `Synthetic ${i}`, description: '', rootPath: directory }))]
for (const project of projects) {
db.saveSupervisionResult({ request, evidence: [{ id: 's', sourceType: 'conversation', sourceId: randomUUID(),
title: 'Synthetic ownership', content: 'Synthetic source', occurredAt: request.timeRange.from, locator: { projectId: project.id } }],
output: { ...empty,
entities: Array.from({ length: 100 }, (_, i) => ({ id: `e${i}`, label: `${project.id}:${i}`, description: 'Synthetic knowledge', sourceReferenceIds: ['s'] })),
events: Array.from({ length: 1000 }, (_, i) => ({ title: `Event ${i}`, description: 'Synthetic decision', occurredAt: request.timeRange.from,
eventType: 'discussion' as const, entityIds: [`e${i % 100}`], sourceReferenceIds: ['s'] })) } })
}
report.seedMs = performance.now() - started
const counts = () => ['supervision_entities', 'supervision_events', 'supervision_event_entities', 'supervision_sources'].map(table =>
[table, Number(sql.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()!.n)])
const before = counts()
await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
const legacy = sql.prepare(`SELECT e.id FROM supervision_entities e WHERE e.confirmation_state != 'revoked' AND EXISTS (
SELECT 1 FROM supervision_event_entities ee JOIN supervision_events ev ON ev.id = ee.event_id
WHERE ee.entity_id = e.id AND ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?)
ORDER BY e.updated_at DESC, e.id LIMIT 100`)
const rounds: Array<{ legacyMs: number; isolatedMs: number; crossProjectMs: number }> = []
for (let round = 0; round < 3; round++) {
const legacyStart = performance.now()
const expected = new Map(projects.map(project => [project.id, legacy.all(project.id).map(row => row.id)]))
const legacyMs = performance.now() - legacyStart
const start = performance.now()
for (const project of projects) {
const candidates = await storage.supervisionCandidates(request, { projectId: project.id, crossProject: false })
assert.equal(candidates.length, 100)
assert(candidates.every(item => item.label.startsWith(project.id + ':')))
assert.deepEqual(candidates.map(item => item.id), expected.get(project.id))
}
const isolatedMs = performance.now() - start
const crossStart = performance.now()
const candidates = await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
assert.equal(candidates.length, 100)
rounds.push({ legacyMs, isolatedMs, crossProjectMs: performance.now() - crossStart })
}
assert.deepEqual(counts(), before)
report.rows = Object.fromEntries(before)
report.rounds = rounds
report.isolatedQueriesPerRound = projects.length
// Exercise the new destination check with the largest admitted assignment chunk.
sql.exec('INSERT INTO supervision_story_assigned SELECT id FROM supervision_events')
const project = projects[0]!
const events = sql.prepare('SELECT id, project_id, started_at, event_type, title, description FROM supervision_events WHERE project_id = ? LIMIT 120').all(project.id)
const stories = db.supervisionStories()
const applyStart = performance.now()
stories.apply(project.id, events as Parameters<typeof stories.apply>[1], [], {
stories: [{ key: 'new_1', level: 'feature', name: 'Synthetic feature', description: '' }],
assignments: events.map((_, i) => ({ event: `e_${i + 1}`, story: 'new_1' })), concluded: [], links: []
}, { crossProject: false, threadEvents: 500 })
report.assignment120Ms = performance.now() - applyStart
assert.equal(stories.list({ kind: 'projects', projectIds: [project.id] })[0]!.events.length, 120)
} else {
assert(envPath, 'Environment file required')
const env = parseEnv(await readFile(envPath, 'utf8'))
assert(env.DEEPSEEK_API_KEY && env.DEEPSEEK_BASE_URL && env.DEEPSEEK_MODEL)
const content = 'We decided to use SQLite transactions to save review facts and coverage together. This prevents partial publication. The implementation is pending.'
db.saveLocalConversations([{ header: { id: randomUUID(), projectId: db.listProjects()[0]!.id, title: 'Synthetic review decision', updatedAt: Date.parse(request.timeRange.from) },
messages: [{ id: randomUUID(), role: 'user', state: 'complete', content, createdAt: Date.parse(request.timeRange.from) }] }])
const runtime = new ModelAgentRuntime({ protocol: 'openai-chat-completions', authentication: 'api-key',
baseUrl: env.DEEPSEEK_BASE_URL, model: env.DEEPSEEK_MODEL, apiKey: env.DEEPSEEK_API_KEY, defaultWorkspace: directory,
maximumOutputTokens: 1800, requestBody: { thinking: { type: 'disabled' }, response_format: { type: 'json_object' } },
toolProvider: { listTools: async () => [], getApproval: () => { throw new Error('Tools disabled') },
callTool: async () => { throw new Error('Tools disabled') }, releaseConversation: async () => undefined, dispose: async () => undefined },
fetcher: async (input, init) => {
assert(calls < 3, 'Three external request cap')
assert.equal(JSON.parse(String(init?.body)).tools?.length ?? 0, 0)
calls++
return fetch(input, init)
} })
const service = createProductionSupervisorService(storage, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
supervisionReview: supervisionReviewSettingsSchema.parse({ crossProject: false, experienceMinEvents: 200 }) }), async () => runtime, pool)
const result = await service.run(request)
assert.equal(result.status, 'completed')
assert.equal(result.coverage?.stories?.status, 'completed')
const evidence = db.supervisionReviewStore().batches(result.runId!).flatMap(batch => batch.evidence)
assert.equal(evidence.map(item => item.content).join(''), content)
const before = calls
assert.equal((await service.run(request)).status, 'no_change')
assert.equal(calls, before)
report.messages = 1
report.characters = content.length
report.status = result.status
report.storyCalls = result.coverage?.stories?.calls
report.noChangeAdditionalCalls = calls - before
}
assert.deepEqual(sql.prepare('PRAGMA foreign_key_check').all(), [])
assert.equal(sql.prepare('PRAGMA integrity_check').get()!.integrity_check, 'ok')
report.integrity = 'ok'
report.passed = true
} catch (error) {
report.passed = false
report.errorType = error instanceof Error ? error.name : 'Error'
process.exitCode = 1
} finally {
report.externalCalls = calls
report.elapsedMs = performance.now() - started
await reader.close()
db.close()
pool.dispose()
report.databaseBytes = (await stat(path)).size
await writeFile(join(directory, 'metrics.json'), JSON.stringify(report, null, 2))
console.log(JSON.stringify(report, null, 2))
}
}
void main().catch(() => { console.error('Synthetic supervision validation preflight failed'); process.exitCode = 1 })
+2 -1
View File
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
async function main() {
const [directory, envPath, mode] = process.argv.slice(2)
@@ -68,7 +69,7 @@ async function main() {
const candidates = db.listSupervisionCandidates(state.request)
writeFileSync(join(directory, 'candidates.private.json'), JSON.stringify(candidates))
metrics.originalCandidates = candidates.length
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
const originalStore = db.supervisionReviewStore.bind(db)
const save = store.save.bind(store)
store.save = (...args) => { save(...args); service.pause(incident.run.id) }
+2 -1
View File
@@ -7,6 +7,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
import type { AgentRuntime } from '../src/main/agent/runtime'
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
async function main() {
const [sourcePath, destinationPath, runId] = process.argv.slice(2)
@@ -37,7 +38,7 @@ async function main() {
const leaves = store.batches(runId, 20)
assert.equal(leaves.length, 6)
assert.equal(store.progress(runId).remainingSources, 0)
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1 }),
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1 }),
async () => ({ runtimeId: 'offline-replay', capability: 'chat', run } as AgentRuntime), pool)
const result = await service.resume(runId)
assert.equal(result.status, 'completed')
+2 -1
View File
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
async function main() {
const [sourceArg, outputArg, envArg, mode] = process.argv.slice(2)
@@ -79,7 +80,7 @@ async function main() {
title: 'Private selected conversation', updatedAt: Date.parse(scope.lastAt) },
messages: messages.map(message => ({ id: message.id, content: message.content, role: message.role,
createdAt: Date.parse(message.created_at), state: 'complete' })) }])
const service = createProductionSupervisorService(db, async () => ({ heartbeatEnabled: true,
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ heartbeatEnabled: true,
supervisorModelConcurrency: 2, supervisorOrganizeTimeoutSeconds: 180,
supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 20, executionSeconds: 600 }
}), async () => runtime, pool)
+33
View File
@@ -0,0 +1,33 @@
/* global require, process, __dirname, console, setTimeout, clearTimeout */
/* eslint-disable @typescript-eslint/no-require-imports */
// node scripts/supervision-storage-live.cjs <current-runtime-settings.json>
const { build } = require('esbuild')
const { spawn } = require('node:child_process')
const { mkdir, mkdtemp, rm } = require('node:fs/promises')
const { join, resolve } = require('node:path')
async function main() {
const repo = resolve(__dirname, '..')
if (!process.argv[2]) throw new Error('Runtime settings path required')
const parent = join(repo, 'temp', 'supervision-storage-live')
await mkdir(parent, { recursive: true })
const root = await mkdtemp(join(parent, 'probe-'))
try {
for (const [entry, output, format] of [
['scripts/supervision-storage-live.ts', 'main.mjs', 'esm'],
['src/main/desktop-storage-entry.ts', 'desktop-storage-entry.mjs', 'esm'],
['src/main/readonly-query-worker.ts', 'readonly-query-worker.cjs', 'cjs'],
['src/main/assistant-storage-worker.ts', 'assistant-storage-worker.cjs', 'cjs']
]) await build({ entryPoints: [join(repo, entry)], outfile: join(root, output), bundle: true,
platform: 'node', format, packages: 'external', logLevel: 'silent' })
const env = { ...process.env, GB_SUPERVISION_STORAGE_ROOT: root, GB_SUPERVISION_SETTINGS: resolve(process.argv[2]), TEMP: root, TMP: root, TMPDIR: root }
delete env.ELECTRON_RUN_AS_NODE
const child = spawn(require('electron'), [join(root, 'main.mjs')], { env, stdio: ['ignore', 'pipe', 'pipe'] })
child.stdout.on('data', data => process.stdout.write(data))
// The probe prints only bounded metrics; native stderr is not a credential-safe report.
child.stderr.resume()
const timer = setTimeout(() => child.kill(), 300_000)
try { process.exitCode = (await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })) ?? 1 }
finally { clearTimeout(timer) }
} finally { await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }
}
main().catch(() => { console.error('Supervision storage probe failed before completion'); process.exitCode = 1 })
+134
View File
@@ -0,0 +1,134 @@
// Run through supervision-storage-live.cjs. Only synthetic evidence is sent to the model.
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { readFile, writeFile, rm } from 'node:fs/promises'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { app, safeStorage } from 'electron'
import { RuntimeSettingsStore } from '../src/main/runtime-settings-store'
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
import { DesktopStorageClient } from '../src/main/desktop-storage-client'
import { createSupervisionDomainPorts } from '../src/main/assistant/supervision-domain-ports'
import { createProductionSupervisorService, createProductionSuggestionPhraser } from '../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
import { HeartbeatService } from '../src/main/assistant/heartbeat-service'
import { deriveSuggestions } from '../src/main/assistant/supervision-suggester'
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
const root = process.env.GB_SUPERVISION_STORAGE_ROOT!
const settingsPath = process.env.GB_SUPERVISION_SETTINGS!
const calls: Array<{ status?: number; inputBytes: number; ms?: number }> = []
const report: Record<string, unknown> = { syntheticOnly: true, maximumHttpCalls: 4, calls }
async function run() {
report.stage = 'preflight'
assert(root && settingsPath)
const profileDirectory = join(root, 'electron')
// Electron must see the copied encryption state before its ready event.
mkdirSync(profileDirectory)
app.setPath('userData', profileDirectory)
if (process.platform === 'win32') {
const state = JSON.parse(readFileSync(join(dirname(settingsPath), 'Local State'), 'utf8'))
writeFileSync(join(profileDirectory, 'Local State'), JSON.stringify({ os_crypt: state.os_crypt }))
}
await app.whenReady()
const original = await readFile(settingsPath)
const snapshot = join(root, 'settings.snapshot.json')
await writeFile(snapshot, original)
const settings = await new RuntimeSettingsStore(snapshot, {
isAvailable: () => safeStorage.isEncryptionAvailable(),
encrypt: () => { throw new Error('Probe does not write credentials') },
decrypt: value => safeStorage.decryptString(value)
}, {}).getResolvedSettings().finally(() => rm(snapshot, { force: true }))
const application = JSON.parse(await readFile(join(dirname(settingsPath), 'application-settings.json'), 'utf8'))
const selectedId = application.supervisorModelProfileId ?? settings.defaultModelProfileId
const selected = settings.modelProfiles.find(profile => profile.id === selectedId)
report.selection = { profileCount: settings.modelProfiles.length, explicitSupervisor: Boolean(application.supervisorModelProfileId),
found: Boolean(selected), protocol: selected?.protocol, authentication: selected?.authentication, credentialAvailable: Boolean(selected?.apiKey) }
assert(selected && selected.protocol !== 'openai-images-generations', 'No configured supervisor text model')
assert(selected.authentication === 'none' || selected.apiKey, 'No configured credential')
report.model = selected.modelName
report.protocol = selected.protocol
const storage = new DesktopStorageClient({ assistantPath: join(root, 'assistant.sqlite'), knowledgePath: join(root, 'knowledge.sqlite'),
userDataPath: root, defaultRootPath: root, entryPath: join(root, 'desktop-storage-entry.mjs'),
readerWorkerPath: join(root, 'readonly-query-worker.cjs'), upgradeWorkerPath: join(root, 'assistant-storage-worker.cjs') })
const pool = new SupervisionModelPool()
const runtime = () => new ModelAgentRuntime({ protocol: selected.protocol, authentication: selected.authentication,
apiKey: selected.apiKey, baseUrl: selected.baseUrl, model: selected.modelName,
requestHeaders: selected.requestHeaders, requestBody: selected.requestBody,
maximumOutputTokens: 3000, defaultWorkspace: root,
toolProvider: { listTools: async () => [], getApproval: () => { throw new Error('Probe tools disabled') },
callTool: async () => { throw new Error('Probe tools disabled') }, releaseConversation: async () => {}, dispose: async () => {} },
fetcher: async (input, init) => {
assert(calls.length < 4, 'Probe HTTP capacity reached')
const body = JSON.parse(String(init?.body))
assert.equal(body.tools?.length ?? 0, 0)
const call: (typeof calls)[number] = { inputBytes: Buffer.byteLength(String(init?.body)) }
calls.push(call)
console.log(JSON.stringify({ event: 'model-request', number: calls.length }))
const start = performance.now()
const response = await fetch(input, { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(60_000)]) })
call.status = response.status
call.ms = Math.round(performance.now() - start)
return response
} })
try {
report.stage = 'storage-ready'
await storage.ready
const ports = createSupervisionDomainPorts(storage)
const project = (await storage.call('assistant', 'listProjects', []))[0]!
const now = Date.now()
const messages = [
'Synthetic project Aurora maintains a weather sensor calibration module. Decision: calibrate against a reference sensor before each field deployment. This replaced the earlier plan to calibrate only after deployment.',
'Milestone: the pre-deployment reference calibration detected a two-degree offset, which we corrected before deployment. The field measurements then matched the reference. Lesson: verify calibration before deployment to prevent biased field data.',
'Open question for the calibration module: should routine recalibration happen monthly or quarterly? No interval has been decided.'
].map((content, index) => ({ id: randomUUID(), role: 'user' as const, content, state: 'complete' as const, createdAt: now + index }))
await storage.call('assistant', 'saveLocalConversations', [[{
header: { id: randomUUID(), projectId: project.id, title: 'Synthetic Aurora calibration', updatedAt: now + 2 }, messages
}]])
const request: SupervisionRunRequest = { trigger: 'heartbeat', scope: { kind: 'global' },
timeRange: { from: new Date(now - 1000).toISOString(), to: new Date(now + 1000).toISOString() } }
const getSettings = async () => ({ supervisorModelProfileId: selectedId, supervisorOrganizeTimeoutSeconds: 60,
supervisorModelConcurrency: 1, supervisionReview: { pageSize: 10, batchCharacters: 8000, batchMessages: 10,
executionSeconds: 300, experienceMinEvents: 2, storyThreadEvents: 20 } })
const supervisor = createProductionSupervisorService(ports.supervision, getSettings, async () => runtime(), pool)
const phrase = createProductionSuggestionPhraser(ports.supervision, getSettings, async () => runtime(), pool)
const heartbeat = new HeartbeatService(ports.heartbeat, {
review: async ({ run }) => {
const result = await supervisor.run(request, run.id)
assert.equal(result.status, 'completed')
assert.equal(result.coverage?.complete, true)
assert.equal(result.coverage?.stories?.status, 'completed')
report.coverage = result.coverage
return { status: result.status, runId: result.runId }
},
suggest: async ({ run, supervisionRunId }) => deriveSuggestions(ports.suggestions, phrase, { supervisionRunId, heartbeatRunId: run.id })
})
const config = await heartbeat.create({ name: 'Synthetic probe', scope: request.scope, timezone: 'UTC',
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
const first = await heartbeat.runNow({ id: config.id, idempotencyKey: 'first' })
report.stage = 'verify-publication'
const activity = (await storage.call('assistant', 'listSupervisionActivity', [10, 0, config.id]))[0]!
assert.equal(activity.status, 'completed')
assert.equal(activity.suggestionStatus, 'completed')
const stories = await ports.stories.list(request.scope)
const experiences = await ports.experiences.list()
const suggestions = await ports.suggestions.countForHeartbeat(first.id)
assert(stories.length > 0 && experiences.length > 0 && suggestions > 0)
const before = calls.length
const quiet = await supervisor.run(request)
assert.equal(quiet.status, 'no_change')
assert.equal(calls.length, before)
report.counts = { stories: stories.length, experiences: experiences.length, suggestions }
report.noChangeCalls = calls.length - before
report.usage = (await storage.call('assistant', 'getTokenUsageSummary', [])).systemTotals
report.settingsUnchanged = (await readFile(settingsPath)).equals(original)
assert.equal(report.settingsUnchanged, true)
assert.equal(storage.pendingCount, 0)
report.passed = true
} finally { pool.dispose(); await storage.close() }
}
void run().catch(error => {
report.passed = false
report.errorType = error instanceof Error ? error.name : 'Error'
// Do not print provider errors, headers, credentials, or settings.
}).finally(() => { console.log(JSON.stringify(report)); app.exit(report.passed ? 0 : 1) })
+43 -4
View File
@@ -1,6 +1,6 @@
# GoodBuddy 静态官网
`sites` 是无需构建步骤或额外依赖的中英文静态官网源码,可直接托管整个目录。
`sites` 包含中英文官网和使用 VitePress 默认主题的用户手册。官网可直接预览;手册正文使用 Markdown 维护,发布前需要构建。
正式站点地址:<https://mesalogo.github.io/goodbuddy/>
@@ -16,7 +16,7 @@ x64 / arm64 安装包与独立龙芯 loong64 预览版,并说明兼容性与
首屏示意显示所选 Runtime,不展示 Ask / Execute 模式或一般工具审批。工具说明以
已启用能力和 MCP 服务分配为准;站点校验会检查中英文页面中的旧模式文案与控件。
FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准备和远端使用条件。
英文页面位于 `en.html`,用平台与架构覆盖描述同一产品事实,不使用中文信创
英文页面位于 `en.html`,用户手册由 `docs/content/` 中按功能拆分的 Markdown 内容构建到 `docs-dist/`。英文页面用平台与架构覆盖描述同一产品事实,不使用中文信创
政策语汇;页面会标明独立的 LoongArch 实验预览,但三个平台的下载按钮仍始终
前往 GitHub 最新正式 Release。
首屏产品界面默认正面展示,在精确指针设备上使用克制的 3D 倾斜、
@@ -26,7 +26,7 @@ FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准
## 部署
`.github/workflows/pages.yml` 会在 `main` 分支中的官网文件发生变化后,
校验并部署整个 `sites` 目录。工作流也支持在 GitHub Actions 中手动运行。
安装依赖、构建手册、校验并部署整个 `sites` 目录。工作流也支持在 GitHub Actions 中手动运行。
首次部署前,需要在 GitHub 仓库的 **Settings > Pages** 中将 **Source**
设为 **GitHub Actions**。站点使用项目 Pages 地址,不需要 `CNAME` 文件
@@ -42,17 +42,41 @@ FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准
## 本地预览
使用 Node.js 24,在仓库根目录安装依赖后运行 `npm run docs:dev`。访问终端显示的地址,在手册 base 下打开 `zh/` 或 `en/`,默认路径为 `/docs-dist/zh/` 和 `/docs-dist/en/`。Markdown 修改会触发热更新。
预览构建产物时,先按下方命令使用本地 base 构建,再运行 `npm run docs:preview`;同样以终端显示的地址和端口为准,在 base 下打开 `zh/` 或 `en/`。
### 维护手册
- 章节顺序、双语标题和稳定 slug 维护在 `docs/manifest.ts`;VitePress 配置从该清单生成侧栏。
- 中文正文放在 `docs/content/zh/`,英文正文放在 `docs/content/en/`。每个语言目录都必须包含 manifest 中的十二个 slug:`start`、`concepts`、`connections`、`runtimes`、`workbar`、`knowledge`、`notes`、`tasks`、`supervision`、`remote`、`privacy`、`troubleshooting`。
- 每个章节文件以一个 `#` 标题开始,正文使用 `##` 和 `###`。除 `start.md` 的 `##` 章节标题外,其余文件的 `#` 标题应与 manifest 对应语言的标题一致。标题尽量使用普通文字。
- VitePress 根目录为 `docs/`,`srcDir` 为 `content`。新增章节时添加两份 Markdown 并更新 manifest,同时更新校验脚本中的必需章节清单。
- `zh/start.md` 和 `en/start.md` 分别重写为 `zh/index.md` 和 `en/index.md`,入口为 base 下的 `zh/` 和 `en/`;其余章节路径为 `zh/<slug>.html` 和 `en/<slug>.html`。
- 配置维护在 `docs/.vitepress/config.mts`,使用默认主题的本地搜索、双语侧栏和原生语言切换,不维护自定义主题或布局。
- 生产 base 为 `/goodbuddy/docs-dist/`,开发与预览默认使用 `/docs-dist/`,可用 `DOCS_BASE` 覆盖。构建和预览的 base 必须一致。开始页下载链接使用正式官网绝对地址:`https://mesalogo.github.io/goodbuddy/#download` 和 `https://mesalogo.github.io/goodbuddy/en.html#download`。
- `docs-dist/` 是构建产物,已忽略,不手工编辑或提交。
整站预览前先构建,以下命令在仓库根目录执行:
```powershell
$env:DOCS_BASE='/docs-dist/'
npm run docs:build
Remove-Item Env:DOCS_BASE
```
在仓库根目录运行:
```powershell
python -m http.server 4173 --bind 127.0.0.1 --directory sites
```
然后访问 <http://localhost:4173/>。也可以直接用浏览器打开 `sites/index.html`。
然后访问官网 <http://127.0.0.1:4173/> 或 <http://127.0.0.1:4173/en.html>,中文手册 <http://127.0.0.1:4173/docs-dist/zh/>,英文手册 <http://127.0.0.1:4173/docs-dist/en/>。手册需要通过 HTTP 服务预览。
## 校验
```powershell
npm run docs:build
node sites/scripts/validate.mjs
node --check sites/app.js
node --check sites/language.js
@@ -68,6 +92,7 @@ node --test sites/scripts/app.test.mjs sites/scripts/release-index.test.mjs
不可变 URL,并按发布生成器的实际命名绑定版本、平台、架构和格式。移动
导航行为测试同时覆盖现代 MediaQueryList 监听与旧版 Safari 的 `addListener`
回退。英文下载入口固定指向 GitHub Release。
手册校验覆盖全部 24 个构建章节的服务端渲染标题与正文、双语侧栏、语言切换和本地资源,并检查 Markdown 标题与 manifest 一致。上述命令检查生产 base;若检查本地构建,运行校验脚本时也设置 `DOCS_BASE='/docs-dist/'`。
## 下载入口
@@ -173,10 +198,24 @@ SIL OFL 1.1 许可证位于 `assets/fonts/inter-OFL.txt`。
焦点,切换回桌面宽度也会解除隔离。媒体查询监听兼容现代浏览器和使用
`MediaQueryList.addListener` 的旧版 Safari。
## 手册开发
手册正文维护在 `docs/content/zh/` 与 `docs/content/en/`,章节清单维护在
`docs/manifest.ts`,主题、语言、侧栏和本地搜索配置维护在 `docs/.vitepress/config.mts`。本地开发运行:
```powershell
npm run docs:dev
```
发布前清除本地 `DOCS_BASE` 覆盖并运行 `npm run docs:build`。VitePress 将中英文各十二个章节及资源生成到 `docs-dist/`,入口为 `docs-dist/zh/index.html` 和 `docs-dist/en/index.html`。Pages 工作流会在校验前自动构建;不要直接编辑构建产物。
## 文件
- `index.html`:页面结构与简体中文内容
- `en.html`:不包含信创适配文案的英文页面
- `docs/content/zh/`、`docs/content/en/`:按功能拆分的中英文手册 Markdown 正文
- `docs/manifest.ts`:手册章节顺序、标题和中英文映射
- `docs/.vitepress/config.mts`:VitePress 默认主题、语言、路由、本地搜索和构建配置,输出到 `docs-dist/`
- `styles.css`:语义令牌、浅深主题、焦点与响应式布局
- `app.js`:主题、移动导航、当前章节和中文下载索引
- `language.js`:浏览器语言自动选择与手动语言偏好
+55
View File
@@ -0,0 +1,55 @@
import { defineConfig } from 'vitepress'
import { chapters } from '../manifest'
const base = process.env.DOCS_BASE ?? '/goodbuddy/docs-dist/'
const sidebar = (language: 'zh' | 'en') => [
{
text: language === 'zh' ? '用户手册' : 'User guide',
items: chapters.map(({ slug, title }) => ({
text: title[language],
link: `/${language}/${slug === 'start' ? '' : `${slug}.html`}`,
})),
},
]
export default defineConfig({
title: 'GoodBuddy',
description: 'GoodBuddy user guide',
base,
srcDir: 'content',
rewrites: {
'zh/start.md': 'zh/index.md',
'en/start.md': 'en/index.md',
},
locales: {
root: { label: 'English', lang: 'en', link: '/en/' },
zh: { label: '简体中文', lang: 'zh-CN', link: '/zh/' },
},
themeConfig: {
siteTitle: 'GoodBuddy',
search: { provider: 'local' },
outline: 'deep',
sidebar: {
'/zh/': sidebar('zh'),
'/en/': sidebar('en'),
},
socialLinks: [{ icon: 'github', link: 'https://github.com/mesalogo/goodbuddy' }],
locales: {
root: {
langMenuLabel: '语言',
nav: [
{ text: 'Home', link: 'https://mesalogo.github.io/goodbuddy/en.html' },
{ text: 'GitHub', link: 'https://github.com/mesalogo/goodbuddy' },
],
},
zh: {
langMenuLabel: 'Languages',
nav: [
{ text: '官网首页', link: 'https://mesalogo.github.io/goodbuddy/' },
{ text: 'GitHub', link: 'https://github.com/mesalogo/goodbuddy' },
],
},
},
},
})

Some files were not shown because too many files have changed in this diff Show More