Compare commits

...
554 Commits
Author SHA1 Message Date
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
mesalogo 78afec7775 test: allow bounded activity paging equivalence checks on CI
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (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-05 00:01:17 +08:00
mesalogo 4e72aa0481 test: wait for workspace import controls to become ready
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-04 23:38:32 +08:00
mesalogo 29af409045 test: align review entry and await sidebar resize transitions
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-04 23:21:07 +08:00
mesalogo 245e296ad5 chore: prepare desktop 0.15.11 and agent 0.15.4
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 unified execution, coordinated Agent upgrade, workspace imports, review history and story colors. Record schema 60 and settings 22 backup boundaries and separate development evidence from release validation.
2026-10-04 22:56:38 +08:00
mesalogo ea3d607fdd fix: unify supervisor review layouts and story spiral colors
Review history and context were split across recap and graph views, while the story spiral rendered saved event intervals in a single neutral color. Share the history picker and context bar, collapse new-review inputs, improve recap and responsive detail layouts, and align graph toolbars and accessible legends.

Color actual event intervals and markers by primary story, preserve neutral gaps and unassigned events, and refresh colors across themes. Include related regression coverage, layout fixtures, overlay timing and workspace-import test adjustments, and updated design and validation documentation.

Release note: Work review and story graph now share the selected review and a compact history picker. Recaps and graph details fit smaller windows, and the time spiral uses story colors for recorded event intervals without filling gaps.
2026-10-04 22:50:31 +08:00
mesalogo b9186a616c fix: retain historical events in the story spiral
Render every selected review event, including events without current story membership, and label current grouping without inventing historical assignments. Keep history selection inside the central canvas heading, use shared camera controls, and fit the default scene within the viewport.

Validation: 46 focused tests, synthetic and read-only portable-data Electron checks, Node/Agent/Web typechecks, targeted ESLint, and diff checks passed. All 21 reported events remain represented at wide and narrow widths; no live data writes or model calls.

Release note: Historical story spirals now retain unassigned events. Review history sits inside the canvas column, and camera controls have consistent spacing and framing.
2026-10-04 21:09:32 +08:00
mesalogo 49fd0d3054 fix(workspace): make file import a compact icon action
Place the import icon after New File and retain destination tooltips, accessible labels, and disabled state while importing.

Validation: WorkspaceFilesPanel tests passed (41 tests).

Release note: Replace the prominent file import button with a compact icon beside New File.
2026-10-04 20:57:40 +08:00
mesalogo e205e90993 fix: align website with unified execution
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
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
Remove obsolete work-mode labels from bilingual product previews and describe tools through enabled capabilities and Runtime assignments. Fix preview overlap and narrow-screen clipping, guard the updated copy, and record execution migration and validation results.

Release note: 官网同步移除 Ask/Execute 模式展示,更新工具配置说明,并修复桌面与手机首屏示意的遮挡和裁切。
2026-10-04 20:40:39 +08:00
mesalogo 577210a8ae fix: synchronize graph review selection and attention radii
Share review history selection between overview and graph while preserving explicit navigation and stale-response guards. Return attention for the selected review scope and interval, and align spiral events and time with that review.

Normalize radii from message rates, including partial turns, without lifting quiet turns toward peaks. Preserve one continuous helix, uniform time height, round turn sections, and smooth transitions; equal densities retain equal radii. Document current-message and hourly-bucket limitations.

Validation: 51 focused unit tests, 2 production IPC tests, Electron DOM and geometry checks, Node/Agent/Web typecheck, targeted ESLint, and diff checks passed.

Release note: Story graphs now offer synchronized review history selection, and spiral radii reflect recorded message density within the selected review.
2026-10-04 20:24:49 +08:00
mesalogo 56ebd40bd8 feat: unify execution and add workspace file imports
Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
2026-10-04 20:02:59 +08:00
mesalogo 0cd5da33f4 fix: correct OpenCode cache hit rates
Include cache reads and writes in the complete input denominator for OpenCode usage. Use the same calculation for stored records, summary totals, and renderer fallbacks so existing history is corrected without migration.

Add regressions for full cache hits, cache writes, mixed Runtime aggregation, and persisted conversation/system usage. Focused validation passed 41 tests; repository-wide checks were blocked by concurrent work-mode changes.

Release note: 修复 OpenCode 缓存命中率偏高或完全命中时显示为空的问题,已有用量记录在重新查询时按完整输入量计算。
2026-10-04 18:03:33 +08:00
mesalogo 4d18794e74 perf: track execution duration by state and bound summary I/O 2026-10-04 16:21:55 +08:00
mesalogo 2c2b6c31eb fix(composer): add icon to story graph toggle 2026-10-04 16:21:33 +08:00
mesalogo ee186f9f70 fix(projects): require creation directories and show remote directory links
Require a root folder in the local project creation dialog and IPC while preserving updates to historical projects with blank paths. Show remote symlinks whose targets are directories and resolve their canonical paths when browsing.

Validation: focused UI, contract, IPC and SSH tests passed; typecheck and lint passed. Verified directory links on the shared Linux Host. Full test run timed out after a Magic Notes storage timing failure.

Release note: 新建本地项目必须选择目录;远程项目选择目录时可查看并进入指向目录的符号链接。
2026-10-04 15:00:13 +08:00
mesalogo 629e82545b fix(ocr): report the actual provider and PPTX failure location
HTTP OCR failures in PPTX parsing were incorrectly labeled as local OCR failures. Report the selected provider and slide/image location, and retain bounded structured server diagnostics with configured-key redaction and log IDs. Preserve existing fallback behavior.

Validation: 38 focused tests, typecheck and lint passed. Full test run timed out after 200 seconds. Synthetic service probes reproduced HTTP 500; the server-side failure remains unresolved.

Release note: PPTX parsing warnings now identify the actual OCR provider and failed image location, with server diagnostics for troubleshooting.
2026-10-04 14:54:39 +08:00
mesalogo 658e7f100f fix(workspace): open project menu with all projects 2026-10-04 14:25:27 +08:00
mesalogo ff61da28a0 test: wait for workspace readiness during cache rotation
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-04 12:42:53 +08:00
mesalogo 362fe50402 test: align supervisor settings regressions with grouped tabs
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
Select the review tab before saving and reopening its settings, use the current model heading and visible screenshot anchor, and allow the 1000-row activity regression 60 seconds on shared CI. Preserve all saved-value, paging, trimming and layout checks.
2026-10-04 11:58:05 +08:00
mesalogo 273a7fa7a5 chore: prepare desktop 0.15.10 release
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 unified navigation, Supervisor controls, activity and desktop performance changes. Keep Agent 0.15.3 and record schema 58 backup requirements, NORMAL durability tradeoffs, and bounded real Host evidence.
2026-10-04 11:33:33 +08:00
mesalogo 74efbcdc17 fix(workbar): reduce divider spacing to 4px
Reduce the workspace left margin and resize target from 9px to 4px so the divider takes less space without overlapping workspace content.
2026-10-04 10:12:40 +08:00
mesalogo e89db96b85 feat(workspace): unify projects and conversations in one menu
Replace separate project and activity navigation with a fixed-height two-line button and a shared workspace menu. Browse local, remote and channel projects, preview sessions on hover, restore the last opened conversation, and show ten recent conversations alongside active work.

Preserve project settings, recovery actions, draft leave guards and completion reminders. Reuse conversation summaries and windowed lists without hover fetches or polling.

Validation: focused component and App tests, real Electron layout and interaction checks, typecheck and lint passed. The earlier full suite timed out and reported Supervisor settings/layout failures.

Release note: 左上角统一项目与活动入口,支持分类查找、悬停预览和最近 10 个会话;双行按钮整体点击,保留项目设置与跨项目会话切换。
2026-10-04 09:53:11 +08:00
mesalogo ae4c6d2423 perf(disk): remove runtime caches left by earlier versions after startup 2026-10-04 09:45:50 +08:00
mesalogo 91edcf5e8b docs: add the desktop performance phase 1 report and refresh the principle gaps 2026-10-04 09:26:31 +08:00
mesalogo 0dd4d1d642 perf(db): relax frequent local writes, checkpoint WAL off Main, index startup recovery (schema 58) 2026-10-04 03:07:16 +08:00
mesalogo 75e50fc194 fix(activity): summarize every loaded conversation instead of the first 5,000 2026-10-04 03:07:16 +08:00
mesalogo 29a134b804 fix(workbar): keep scroll arrows clear of tab close buttons
Reserve space on both sides of the tab viewport for overflow arrows so they cannot cover tab close controls. Keep that space while arrows are hidden or disabled to avoid layout shifts.

Add Electron coverage for arrow bounds and native closing at the right viewport edge; update tab layout documentation.

Validation: 19 focused tests passed; typecheck and lint passed. Full suite did not complete before interruption and reported failures in supervisor settings and supervisor layout tests.

Release note: Fix workbar scroll arrows covering tab close buttons when many tabs are open.
2026-10-04 01:14:41 +08:00
mesalogo 59d36c2258 docs: add module conventions for store, selectors, sync and views to the performance principles 2026-10-04 00:35:45 +08:00
mesalogo 39fc15efe0 docs: record activity history incremental save and paging (P4) 2026-10-04 00:33:19 +08:00
mesalogo 5d0ca44b22 test: compare upgraded schema versions with ASSISTANT_DATABASE_SCHEMA_VERSION
The supervision upgrade tests asserted the latest version literally (56);
schema 57 (activity history order and indexes) made them fail.
2026-10-04 00:26:10 +08:00
mesalogo 78beade289 fix(composer): clarify DSH actions and keep stop confirmation inline
Name the browser and service controls explicitly for DSH. Keep confirmation and cancellation together in the composer action row, retain the interruption warning as a tooltip and accessible description, and show stopping progress on the confirmation button.

Validation: 20 focused tests passed; typecheck and lint passed.

Release note: 明确 DSH 网页和服务按钮名称,停止确认与取消按钮同行显示,避免确认区单独撑开聊天输入区。
2026-10-04 00:17:51 +08:00
mesalogo bf9850a6a9 perf(activity): keep only loaded activity pages in the renderer, driven by Main's summary
The renderer no longer holds the whole history. Startup sends queued
changes, lets Main reconcile leftover pending/running records against the
stored tasks (same localized interruptedOnRestart line), then loads the
first page (200). The Activity page loads a full visible batch (500, as
before paging); "load more" shows the next batch and loads the next page
behind it; a filter switch refetches that filter's first page; leaving the
page trims to one batch. Counts, project/conversation groups, titles,
conversation status and the clear count come from activityHistory.summary
(refreshed on entry, by the page timer, the refresh button and after load
more / filter changes).

Records that are not loaded are still updated incrementally: the update
contract gains content-matched changes (upsert-call, update-request,
resolve-approval, settle-request, remove-request, remove-call) that Main
applies in SQL to shown rows; the store applies the same semantics to its
pages through the shared reference. update returns Main's stored call
records so a call whose earlier record was not loaded keeps one id. Task
refresh reconciles via Main SQL. Legacy localStorage records migrate as
incremental changes; the renderer no longer calls replace.

Tests: randomized sequences of every action, page loads, trims, flushes and
injected failures against the former full-list code on a real database;
first page <= 200 after startup; flush payload depends only on changed
records (including unloaded ones); ActivityPanel output with pages + summary
equals the full-list rendering for randomized datasets with duplicates;
restart interrupts leftover records with the same detail; clear empties the
database, pages and summary; SQL change semantics equal the reference.
2026-10-04 00:03:33 +08:00
mesalogo 0e9c9d4927 fix: align supervisor settings layout and default model naming
Group settings tabs and forms in a single surface with consistent spacing. Rename the model section to cover model selection and execution limits, and use Default model for the default connection option.

Release note: 修复监督者设置页签与表单贴边的问题,统一设置布局,并将默认模型选项名称改为默认模型。
2026-10-03 23:43:58 +08:00
mesalogo 352da66dbd perf(activity): page, summarize and reconcile activity history in Main by index
Schema 57 rebuilds activity_history_records with order_seq (newer = larger;
a move to the front assigns a new maximum) and stored generated columns
(record_id, occurrence, status, status_group, kind, request_id,
conversation_id, project_id, created_at) with indexes. The order is migrated
from record_order_json exactly; legacy duplicates keep their occurrence and
only the first is shown. record_order_json is no longer written.

ActivityHistoryRepository owns the SQL: page({before, limit <= 500, filter,
projectId, conversationId}) newest first by index, summary() with the counts
and conversation titles/status the Activity page derived from the full list,
reconcile() joined with tasks with a renderer-localized detail line, update()
applied directly in SQL without reading the history, replace() for the
legacy migration, clear(). New activity-history:page/summary/reconcile IPC
with zod validation; page and summary run on the readonly worker.
perf:main measures 24,000 records: startup load 106 ms on Main (14 MB) ->
first page + summary 13.5 ms on the worker (0.6 ms loop block, 114 KB).
2026-10-03 21:42:52 +08:00
mesalogo 6aab78b9fa perf(activity): save activity history incrementally through an activity store and sync queue
The renderer no longer sends the whole history on every change. Domain
actions on an external activity store update the list and queue the matching
contract changes; activity-sync.ts flushes them every 250 ms via
activityHistory.update (<= 20,000 per call), keeps failed changes queued in
order with backoff retry, and flushes on quit and unmount. replace remains
only for the legacy localStorage migration; clearing uses the new
activityHistory.clear IPC. App's activity state is gone; ActivityPanel reads
a selector. update is now required in the desktop API.
2026-10-03 20:45:51 +08:00
mesalogo 2adee13638 fix(composer): keep first focus on the expert menu when opening options
48d7526 widened the options dialog first-focus selector to inputs, so the new story graph switch above the menus took initial focus. Keyboard users no longer landed on the expert / work mode menus like the model picker. Restore the button-only target; the switch stays reachable with Tab.
2026-10-03 20:04:55 +08:00
mesalogo 942fe3206c docs: record PERF-13 composer, route boundary, task store and conversation switch results 2026-10-03 19:54:49 +08:00
mesalogo 6b634c5d6b chore(lint): set the App.tsx max-lines cap to 8,000 after the supervisor commit 2026-10-03 19:17:56 +08:00
mesalogo 739f26702b chore(lint): lower the App.tsx max-lines cap to 7,900 2026-10-03 19:16:53 +08:00
mesalogo 48d7526b7e feat: configure supervisor model and story graph use per conversation
Add a supervisor text-model selector using existing connections and a per-conversation story graph switch above knowledge settings. Hide the switch when Supervisor is disabled and enforce conversation preferences at local and remote tool execution boundaries.

Release note: Choose a dedicated supervisor model and control whether each conversation can query existing stories and experiences. Enabling graph access does not start a review or automatically insert context.
2026-10-03 19:16:35 +08:00
mesalogo 492f1d208a perf(renderer): render App once per conversation switch and keep the titles map stable 2026-10-03 19:14:54 +08:00
mesalogo 651475d17a perf(renderer): move assistant tasks and artifacts into a task store with narrow selectors and a sync module 2026-10-03 18:58:01 +08:00
mesalogo 2e3d2e77a9 perf(renderer): memoize keep-alive routes, workspaces and sidebars behind stable callbacks 2026-10-03 17:59:20 +08:00
mesalogo 41be9f8076 perf(renderer): extract the chat composer into a memoized Composer with stable actions and a popup store 2026-10-03 17:34:57 +08:00
mesalogo 010aafb6f4 perf(startup): load canvas, node-pty, ssh2, html-to-text and the OpenCode SDK on first use
Main module evaluation drops from ~420 ms to ~330 ms. Types stay as import type; the DeepSeek Harness chunks stay static because the protocol chunk shares code with Main startup paths.
2026-10-03 17:15:13 +08:00
mesalogo 68cc0792e1 perf(startup): build the local tool environment in the background and gate its consumers
Runtime creation, MCP launches, Obsidian, native clients and the tool-environment IPC await the initialization promise instead of startup awaiting it before the window path. A failure still fails startup with the same diagnostic.
2026-10-03 16:56:13 +08:00
mesalogo b28ddd4ba8 chore: commit pending workspace changes (supervisor settings tabs, conversation keep-alive budget, PR article updates) 2026-10-03 16:22:42 +08:00
mesalogo 805acc3fce fix: restore the jump-to-bottom glide and keep user state, selections and scroll anchors in windowed lists
Pause scroll writes while the jump-to-bottom smooth scroll runs; keep rows with pending questions, approvals, recent interactions and selection ends mounted; cache Mermaid results; copy text spanning unmounted rows; anchor the sidebar on id changes; disable native overflow-anchor for windowed lists; write terminal output on the leading edge and before clear; forward remote events every 16 ms; flush streaming text before copy/export; add performance principle P10 (no UX regressions).
2026-10-03 16:19:56 +08:00
mesalogo a620c22676 fix: keep the notes list accessible in narrow workspaces
Default the notes list to 200px with a 160px minimum and hide it automatically only below 600px. Keep the detail toolbar toggle available to open a drawer, with dismissal and note-switching support. Restore reading positions after rich text content effects populate the record.

Include the existing document-table and file-tree scroll anchoring styles in this file-level commit. Validation: 143 related tests, Electron layout regression, typecheck and changed-file lint passed; repository lint is blocked by unrelated App.tsx changes.

Release note: The notes list takes less space and remains accessible through a drawer in narrow windows. Existing saved list widths are preserved.
2026-10-03 16:02:27 +08:00
mesalogo 10065da8ac docs: record the third parallel performance round 2026-10-03 13:54:24 +08:00
mesalogo b2eb86a0b4 perf: window the notes grid, workspace file tree and knowledge documents table 2026-10-03 13:54:24 +08:00
mesalogo f4458afe4b perf: coalesce terminal output in Main and write it to xterm once per frame 2026-10-03 13:54:24 +08:00
mesalogo 10c5162811 perf: parse, chunk and hash documents in a worker thread and add startup and document-parse benchmarks 2026-10-03 13:54:24 +08:00
mesalogo 9d03217f99 docs: add GoodBuddy performance principles and review checklist 2026-10-03 13:54:24 +08:00
mesalogo 55f554d13c fix: keep composer context usage on the action row by letting it shrink 2026-10-03 13:21:04 +08:00
mesalogo 9527402003 release: prepare Desktop 0.15.9 and Agent 0.15.3
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
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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 / 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
Document shared review timelines, stories and experiences, spiral navigation, conversation performance work, and Execute configuration behavior. Include schema 56 backup and cost boundaries, Agent tool-schema compatibility, and four successful current-source real Host calls with checkpoint persistence. Full release validation runs in CI.
2026-10-03 10:45:54 +08:00
mesalogo 50481deaa1 fix: let story graph views fill the canvas column with compact controls 2026-10-03 10:23:09 +08:00
mesalogo baa6defc0c docs: record what this performance round deferred and why 2026-10-03 06:28:13 +08:00
mesalogo 775763eee0 perf: keep sidebar ids stable during streaming and measure marquees from the resize observer 2026-10-03 06:28:12 +08:00
mesalogo 82107ad86a docs: record second parallel performance round 2026-10-03 05:53:33 +08:00
mesalogo b6c7de5f91 perf: move conversation persistence, refresh and agent events out of App
Persistence and history loading, refresh scheduling and the agent event handler work against the conversation store outside React; App.tsx drops to 9,566 lines and the lint ceiling to 9,600.
2026-10-03 05:53:33 +08:00
mesalogo 663aa85e1a perf: window the sidebar, cache Markdown trees and window chat scrolling once per frame
The chat pane decides re-windowing once per frame from scrollTop and cached row heights instead of reading layout per scroll event. Rendered Markdown hast trees are cached, so remounted rows skip parsing. Sidebar lists of 60+ conversations mount only rows near the viewport. The perf driver scrolls the windowed sidebar to reach a row.
2026-10-03 05:53:10 +08:00
mesalogo 3c31f0b499 perf: read conversation lists in the readonly worker and diff activity history saves
conversations:list-summaries and conversations:get run on the readonly query worker. Activity history replace validates and serializes only changed records; a new activity-history:update IPC writes one record. Adds npm run perf:main.
2026-10-03 05:53:10 +08:00
mesalogo 169941ceda docs: record conversation store, timeline windowing and read worker results 2026-10-03 02:53:51 +08:00
mesalogo fdf7d83b20 perf: move conversations into an external store and window the message timeline 2026-10-03 02:53:51 +08:00
mesalogo f571301764 perf: run knowledge, story graph and conversation search reads in a read-only worker 2026-10-03 02:53:51 +08:00
mesalogo 53b9ecbcae chore: add architecture lint ratchets and perf regression thresholds
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
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
ESLint allowlists (only to shrink) for DatabaseSync, synchronous fs in src/main and direct window.goodbuddy subscriptions in components, plus a max-lines ratchet for App.tsx; docs demos and vendored scripts are no longer linted. perf:check (or GB_PERF_CHECK=1 perf:app) fails a non-throttled benchmark that exceeds build/perf-thresholds.json. Records the results in the PERF plan.
2026-10-02 23:11:11 +08:00
mesalogo d859cbfdeb perf: persist remote runtime events in one transaction per checkpoint
Remote semantic events were written with one or two synchronous SQLite transactions each before being forwarded; recoverable conversations also rewrote the whole assistant message per event. Events now commit together at each remote checkpoint (before the Agent can acknowledge), and before non-remote, question and terminal events, on a size cap or a 50 ms timer. Only newly stored events are forwarded, in order; a failed batch rolls back and blocks later commits. Applies to interactive, background and recovery runs.
2026-10-02 23:11:11 +08:00
mesalogo 855b941aeb perf: catch up new conversation panes in small steps and keep pane order in state
The first message batch now grows by 20 per frame after the first paint instead of one large commit, keeping the reader at the bottom or in place. The cached pane order is reconciled into state by a pure helper instead of reading and writing a ref during render (react-hooks/refs).
2026-10-02 23:11:11 +08:00
mesalogo a1a1d24022 fix: release the spiral view's WebGL context and keep its failures local
Each open of the time spiral created a WebGL context that was never released, so repeated use hit the browser limit and lost the oldest context. The view now releases it on close, shows a local retry on creation failure or context loss, and the lazy chunk retries once before an in-canvas error.
2026-10-02 22:58:30 +08:00
mesalogo 9d8477b728 fix: fit the story graph to the window and scroll each column on its own 2026-10-02 22:22:03 +08:00
mesalogo 0fd2f7e3c8 fix: retry unusable model output in smaller review batches
Leaf batches retry at half and a quarter of the text budget, navigation merges retry once; provider errors are not retried. Adds the long-session acceptance script and records the results.
2026-10-02 21:48:29 +08:00
mesalogo 7aad78752b docs: correct PERF-13 attribution and record architecture review of concurrent changes
Opening a long conversation got faster because of the two-step pane render (09c415b), not the live message store, and its catch-up frame is now a long frame. Records which concurrent changes follow the batch rules, and adds Story Graph search and batched remote event writes to PERF-15/16 and the list-order state to PERF-14.
2026-10-02 21:04:47 +08:00
mesalogo 6ced760c1a test: add long-session acceptance script for the production review
Runs the review on an isolated copy of a real database with DeepSeek, injects one provider failure, resumes, and checks coverage and zero-call reruns. Credentials come from an env file passed on the command line.
2026-10-02 20:07:37 +08:00
mesalogo 09c415bd41 perf: render a newly opened conversation in two steps
A freshly mounted pane first renders the trailing 20 messages and the rest of its 80-message batch right after the first paint, keeping the bottom or the reader's place. Saved scroll positions and note targets still render the full batch at once. Keeps the write-on-change guard for --chat-context-height.
2026-10-02 20:06:44 +08:00
mesalogo e97263c79e feat: help agents find stories and experiences on their own
Search matches query terms (CJK pairs) instead of only the whole phrase, previews show readable fields first, and the tool description says when to call it and when to stop.
2026-10-02 20:02:07 +08:00
mesalogo 02a6f5c4e7 fix: remove redundant confirmation for configuration changes
Configuration apply previously required a separate native confirmation even in Agent Runtime Execute mode, and failed in clients without an approval callback. Use existing work-mode and tool authorization instead, preserving Ask read-only access and request-scoped plan validation. Update regression tests, tool descriptions and documentation.

Validation: 267 focused tests passed; typecheck and changed-file lint passed. Full tests timed out with failures in other modules; repository lint reported errors in unchanged files.

Release note: Execute 模式下通过 Agent Runtime 修改 GoodBuddy 配置不再额外弹出确认框,Ask 模式仍保持只读。
2026-10-02 19:39:44 +08:00
mesalogo dc635d185e perf: record PERF-13 streaming and draft store results; allow a custom fake model port 2026-10-02 19:27:36 +08:00
mesalogo 4d1ce38dad fix: record an experience application only when it touches the same knowledge 2026-10-02 19:08:05 +08:00
mesalogo c3fa262832 fix: keep area commits and tests in their own story, not in release 2026-10-02 19:00:18 +08:00
mesalogo 0ce9606c89 test: cover five-second conversation ordering and interaction pauses
Add App regression coverage and document five-second sidebar sorting, live row updates, and deferred reordering during interaction. The ordering hook and App wiring were already included in ee4eb73. Focused validation: 23 tests passed.
2026-10-02 19:00:10 +08:00
mesalogo ee4eb737e0 feat: show experiences outside the time spiral 2026-10-02 18:49:24 +08:00
mesalogo 1f420cd042 feat: organize the work review by story 2026-10-02 18:42:28 +08:00
mesalogo 43be465286 feat: suggest stalled stories and reusable experiences, and let agents read stories through MCP
Also includes pending workspace changes: unsaved-change guard, keep-alive cache and live message store, knowledge workspace dialogs and perf driver updates.
2026-10-02 18:23:08 +08:00
mesalogo 5f1ff24333 fix: soften chat scroll-to-bottom button border 2026-10-02 18:11:03 +08:00
mesalogo d773f5e961 chore: upgrade Electron to 44.5.1 and adapt clipboard APIs 2026-10-02 17:53:42 +08:00
mesalogo 90ea33ef5c perf: skip redundant composer measurement, cache i18next language codes and follow streaming once per frame 2026-10-02 16:57:29 +08:00
mesalogo 983cdd4e05 feat: extract reusable experiences from supervisor stories 2026-10-02 16:21:53 +08:00
mesalogo bf6c4a91a2 fix: keep scroll position when switching between cached conversations 2026-10-02 15:52:48 +08:00
mesalogo 1e10935d22 perf: add CPU-throttled, memory-growth and knowledge retrieval benchmarks
perf:app gains GB_PERF_CPU_THROTTLE (CDP renderer throttling) and a keep-alive memory scenario with post-GC heap samples; perf:knowledge times synchronous FTS, vector and hybrid search at 5k/20k/50k chunks. Results and conclusions recorded in the performance plan (PERF-05, PERF-09, PERF-12).
2026-10-02 15:51:33 +08:00
mesalogo 5dc65457d9 feat: shared supervisor timeline, story assignment and time-spiral view
Each source version is extracted once for every review scope; events carry spans and projects from their sources, and attention density is counted from messages. After publication, events are assigned to project features and sub-threads (cross-project stories only when enabled), with user rename, merge, remove, move and undo. The story graph gains a lazily loaded Three.js time-spiral view. Includes the 3D concept demo, real-data DeepSeek check scripts and docs.
2026-10-02 15:21:48 +08:00
mesalogo 3d4e9577b5 perf: keep Markdown segmentation for single-line display math and ship KaTeX fonts as files 2026-10-02 14:41:56 +08:00
mesalogo 80579ee421 perf: add full-app interaction benchmark and UI architecture plan 2026-10-02 13:42:40 +08:00
mesalogo 8280158edc test: preserve native focus sequence when discarding note drafts
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
Wait for detail navigation and continued editing focus, and focus the cancel button before synthetic clicks so confirmation restores the actual opener. Keep draft discard and final entry focus assertions unchanged. Validate in CI without touching parallel work.
2026-10-02 12:14:33 +08:00
mesalogo 5368e0ecf4 test: align fixtures with image tools and enabled browser defaults
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
Isolate Obsidian tests from the default browser capability, check the additional image-tool card, and center native overlay click targets below the frosted header. Retain existing permissions, identity, hit-testing and keyboard assertions; preserve unrelated uncommitted work.
2026-10-02 11:53:21 +08:00
mesalogo 20317c21b5 release: prepare Desktop 0.15.8 and Agent 0.15.2
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
Release committed image-save tools, note editing improvements, and enabled-by-default browser and frosted appearance preferences. Include bounded Linux Host save evidence and bilingual Desktop/Agent notes. Preserve all pre-existing uncommitted design and article files; full validation and packaging run in CI.
2026-10-02 11:31:43 +08:00
mesalogo b1129cedec fix: enable frosted glass effect by default 2026-10-02 10:40:16 +08:00
mesalogo 2e5c348dce fix: enable built-in browser by default 2026-10-02 10:27:58 +08:00
mesalogo f4d8fe7f06 fix: add new-entry cancel, full-width note content, and restore focus in magic notes
- Add a Cancel action to the new-entry composer with discard confirmation
- Remove the 880px cap so note content matches the toolbar width
- Focus the chat composer only after a deferred workspace leave commits
- Return focus to a sensible target after cancelling new/edited entries and switching entry type
2026-10-02 10:20:04 +08:00
mesalogo dd29f309f1 feat: add save_image tool and list image MCP tools
Expose save_image across direct model, MCP gateway, DeepSeek Harness and remote Agents (written on the remote host via chunked reads), and show the image MCP tool list in settings.
2026-10-02 10:08:42 +08:00
mesalogo b02ce31886 test: align native composer hints and bound Markdown corpus checks
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
Update the Electron probe for the two-line shortcut placeholder after greeting removal. Preserve all prefix-equivalence checks and give only the exhaustive Markdown corpus test 15 seconds. Validate through CI per user instruction.
2026-10-01 22:43:33 +08:00
mesalogo 7cb74cce78 test: account for sticky settings headers in layout checks
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
Scroll settings controls into the unobscured area below the measured sticky header, retaining native interaction and hit-testing assertions. Give the full legacy outbox migration fixture a scoped 15-second timeout. Product code and bilingual release notes are unchanged; validation remains in CI.
2026-10-01 22:13:42 +08:00
mesalogo 5e77118508 fix: remove redundant chat input placeholder greeting 2026-10-01 22:01:47 +08:00
mesalogo 227507de8f release: prepare Desktop 0.15.7
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 streaming rendering improvements, on-demand activity refresh, local custom MCP failure isolation, managed model source selection, and opt-in device sharing. Agent remains 0.15.1 and schema remains 52. Per user instruction, validate the exact candidate through CI without additional local tests.
2026-10-01 21:54:32 +08:00
mesalogo c8f327fc4e perf: reduce streaming render cost and refresh activity page on demand
- Segment streaming Markdown so completed blocks are reused and only the growing tail is re-parsed; fall back to whole-document parsing for document-wide constructs
- Memoize conversation sidebar rows and cache per-conversation derived activity, ordering and pending approvals
- Activity page loads on entry, refreshes every 15 s while visible, and has a single header refresh button
- Include pending settings, model download source, device sharing and knowledge gateway changes in the working tree
- Add Electron streaming input benchmark and regression tests
2026-10-01 21:49:09 +08:00
mesalogo 016f987026 test: assert note focus before opening creation form
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
Check detail navigation focus before the auto-focused creation input takes over. Preserve return-to-list and reading-position assertions, and include the reviewed sharing first-read test and independent validation record. Validation runs in CI per user request.
2026-10-01 09:16:15 +08:00
mesalogo 2c5300d031 chore: finalize expanded desktop 0.15.6 release scope
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
Include device-sharing metadata and the persistent New note entry in bilingual release notes, align five-app documentation, and update the configuration fixture. Per user instruction, validation proceeds through CI without more local tests. Agent remains 0.15.1.
2026-10-01 01:43:53 +08:00
mesalogo a3b41bf2b1 feat: add device sharing metadata application 2026-10-01 01:31:44 +08:00
mesalogo 0a02a17cb6 fix: keep New note in the magic notes page header 2026-10-01 01:31:43 +08:00
mesalogo d2ceff8db5 test: allow the long-history warm-switch regression to finish
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
Scope a 15-second budget to the 161-message switching scenario while retaining all render-count, DOM reuse, draft, attachment, and scroll assertions. The full App suite, typecheck, and lint pass.
2026-10-01 01:15:10 +08:00
mesalogo 8201c9c27e test: wait for theme transitions before comparing glass styles
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
The Linux full suite sampled a window-control color during its transition. Wait for CSS transitions to finish while retaining exact style, contrast, geometry, and native interaction assertions.
2026-10-01 00:48:35 +08:00
mesalogo cc451f698d release: prepare Desktop 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
Include the pending sidebar footer style correction and document frosted appearance, chat rendering, image previews, settings layout, and local MCP recovery. Record schema 52 backup guidance and 886 passing focused tests; Agent remains 0.15.1.

Release note: 新增可选磨砂外观,改善聊天输入、图片预览和本地工具连接恢复,并整理设置与笔记布局。
2026-10-01 00:23:54 +08:00
mesalogo 9cc5dc1244 fix: retire uncertain MCP names so late OpenCode adds cannot replace live connections 2026-10-01 00:08:50 +08:00
mesalogo f7f501a8e3 feat: add frosted glass appearance and improve local MCP recovery
Add an opt-in persistent frosted glass setting for the top bar and runtime checklist, with chat scrolling behind the header while preserving scroll anchors and window controls.

Retry transient local MCP initialization failures once after cleanup, include redacted connection diagnostics in feedback, and index lifecycle events to reduce execution statistics scans.

Release note: Add optional frosted glass appearance with chat visible behind the title bar; improve local tool connection recovery and diagnostic feedback.
2026-09-30 23:37:26 +08:00
mesalogo e4f7b8d993 fix: group settings navigation and align switches to the right 2026-09-30 23:33:25 +08:00
mesalogo b6d24ada87 fix: preview draft and queued image attachments directly
Make draft thumbnails and queued images open the shared image viewer without parsing. Keep queued attachment menus read-only and remove OCR source and settings entries while preserving explicit text extraction in drafts.

Validation: 14 focused tests, typecheck and lint passed. Full suite: 5534 passed, 3 failures outside this change in conversation scrolling and frosted-glass descriptions.

Release note: 草稿和队列附件中的图片现在支持点击放大查看,无需 OCR;队列只读菜单不再显示解析来源和解析设置。
2026-09-30 23:18:45 +08:00
mesalogo c23dca65e8 fix: reduce chat typing and cached conversation switching overhead
Keep unchanged history panes and task strips from rerendering during typing, and stabilize message callbacks across cached conversation switches. Preserve streaming updates, scroll positions, drafts, and asynchronous attachment ownership.

Add App regressions for render isolation, warm switches, retry, and image recovery. Focused tests, typecheck, and lint passed; the full suite encountered unrelated settings failures.

Release note: 减少任务运行时输入和切换已缓存长会话的重复渲染,保留实时输出、滚动位置和会话草稿。
2026-09-30 22:59:25 +08:00
mesalogo bf64181c85 fix: align magic note headers and editor spacing
Align the notes list, detail toolbar and comment direction row, removing stacked top padding. Let text editing controls fill the record card while preserving text and action padding and canvas spacing.

Add Electron geometry regressions and document the spacing rules. Release note: Magic Notes now aligns pane headers and removes excess gaps around text editing toolbars and input areas.
2026-09-30 22:31:30 +08:00
mesalogo 3be55348a2 fix: locate note entries after their content mounts
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (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 / 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
Quick Notes could open the full workspace before its selected record was mounted, losing the scroll request. Perform entry navigation after React commits and retain a deterministic regression. Align the manual-compaction fixture with layered model selection and loaded conversation state.

Release note: 从快速笔记打开完整记录时,等待目标内容挂载后再定位,避免偶发只打开页面却未滚动到所选记录。
2026-09-30 19:46:13 +08:00
mesalogo 89fc100d4f release: refresh Desktop 0.15.5 candidate
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
Include note tags and layout, workspace controls, and OpenCode launch/tool-name updates in bilingual notes. Update schema 51 guidance and align the Electron Supervisor regression with the implemented suggestions UI.
2026-09-30 19:18:09 +08:00
mesalogo 6edfc0046b feat: add magic note tags and a two-pane notes layout
Keep the notes list beside the open note, compact the detail view with a full-width sticky toolbar, and make Back to overview return to a full-width all-notes grid. Add manual note tags with AND filtering, a tag manager for rename, merge and delete, schema 51 tag tables, and tag support in the note Agent tools.
2026-09-30 19:04:47 +08:00
mesalogo ab85e72f01 fix: keep workspace file controls visible while scrolling
Scroll the file list independently so view selection, refresh, creation actions and directory navigation remain visible. Keep the selected Files entry available in projects without Git.

Add component coverage and 16 Electron layout scenarios for themes, window sizes and scroll retention. Focused tests, typecheck and lint passed; the full test run timed out after 200 seconds.

Release note: Workspace file controls remain visible while scrolling, and projects without Git retain the Files entry.
2026-09-30 17:33:16 +08:00
mesalogo 80edd15438 fix: shorten OpenCode temporary MCP tool names and keep them stable per conversation
Use gbd-/gbc- prefixes with a 6-char conversation hash instead of 20-char per-request hashes, lease per-conversation slots so parallel requests never share a name, and expose gateway custom tools as <server hash>_<name> within the 64-char provider limit.
2026-09-30 17:32:26 +08:00
mesalogo 7fb23d9b6b fix: speed up opening OpenCode in the terminal
Every 'Open in terminal' launch for OpenCode copied the bundled plugin dependency tree (about 3,600 files, 52 MB) into a temporary directory, adding 2.2-2.4 s on Windows before the TUI started. The tree is now copied once into terminal/opencode-config under user data and reused until its bundled version marker changes. Skills, Ask-mode plugins and model configuration are still generated per launch and cleaned up on close. OpenCode's own TUI startup (about 3 s) is unchanged; the remote Agent path is unaffected.

Release note: 在终端中打开 OpenCode 时不再每次复制插件依赖,第二次及以后的打开速度明显加快。
2026-09-30 16:59:13 +08:00
mesalogo 925d410247 release: prepare Desktop 0.15.5
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
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
Document incremental Smart heartbeat reviews, evidence-backed suggestions, interrupted-turn history, floating checklists, and note save behavior. Record schema 50 backup requirements and real Host cancellation validation; Agent remains 0.15.1.

Release note: 智能心跳按变化回顾并提供有依据的建议,修复取消后续聊、清单展开和笔记保存体验;升级前请备份数据库及笔记。
2026-09-30 11:23:45 +08:00
mesalogo 2d8e58e995 fix: exit note editing after saving changes
Return text and canvas records to read-only content after a successful save. Automatic analysis no longer reopens the editor; failed saves preserve drafts for retry. Update regression coverage and UI behavior documentation.

Validation: 118 MagicNotesWorkspace tests, typecheck and lint passed. Full test suite exceeded the 120-second timeout.

Release note: 笔记中的文字和画布记录保存修改成功后自动退出编辑状态,自动评论完成后不再重新打开编辑器;保存失败时保留草稿以便重试。
2026-09-30 10:57:25 +08:00
mesalogo 6293ef105d feat: make heartbeat trigger incremental reviews and suggest from graph changes
Heartbeat no longer writes its own report; it triggers the shared incremental review and, per plan intervention, derives evidence-backed suggestions from the review's graph changes. Manual review is incremental by default with explicit reanalysis. The Supervisor tab is renamed Smart heartbeat with heartbeat plans, and the redundant inner plan frame is removed.
2026-09-30 10:45:22 +08:00
mesalogo d30f794184 fix: expand runtime checklists without shifting chat content
Render checklist details in an anchored floating panel with bounded scrolling, outside dismissal and Escape focus restoration. Reuse the shared overlay mechanism and update layout documentation and regression coverage.

Validation: 25 focused tests, Electron checklist layout and keyboard scenarios, and lint passed. Full-suite and typecheck blockers outside this change were reported separately.

Release note: Runtime execution checklists now expand in a floating panel without pushing conversation content down.
2026-09-30 02:36:50 +08:00
mesalogo 608a9a5fec fix: keep cancelled turns as interrupted instead of merging into the next request
- Build runtime history per turn: a cancelled or failed reply keeps its partial
  text plus an interruption marker (or a marker-only placeholder), so its user
  message is never left unanswered and merged into the next request.
- Use the same history for send, manual compaction and Main compact validation.
- OpenCode: keep a cancelled prompt in the reused native session and prefix the
  next turn with a one-time synthetic interruption notice; failed prompts are
  still removed.
- Add an opt-in DeepSeek live test covering direct model and OpenCode.
2026-09-30 02:18:33 +08:00
mesalogo a662b16f5e fix: remove the conversation runtime override dot
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
Keep the Runtime icon and label clear after a conversation-specific selection. Preserve override details and the reset action inside the picker, with regression coverage and bilingual release notes.

Release note: 移除会话 Runtime 按钮图标与名称之间的圆点,保留菜单内的单独选择提示与恢复项目默认操作。
2026-09-29 22:50:43 +08:00
mesalogo 911eed5e67 test: wait for runtime readiness before question submission
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
Wait for the enabled send control rather than its initial presence. Preserve the question rounds, saved answers, skip, and reload assertions; production behavior and bilingual release notes are unchanged.
2026-09-29 22:38:08 +08:00
mesalogo ed47e2c2a5 chore: prepare desktop 0.15.4 and agent 0.15.1 releases
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 layered Runtime and model selection, separate system-task usage, and Continue history handling. Record schema 49 backup requirements and current-source Host validation with explicit failed-provider attempts.

Release note: Desktop 0.15.4 supports layered Runtime/model choices and separate system-task usage; update Desktop before Agent 0.15.1 to receive the remote Continue history fix.
2026-09-29 22:10:20 +08:00
mesalogo 9c88ca20f3 feat: layer runtime and model selection across conversation, project and global
- Store runtime selection as { provider?, model? } layers and resolve them per
  field (conversation -> project -> global); drop the legacy auto provider.
- Migrate stored selections (database v49) and clear deleted profile ids.
- Re-resolve selections in Main per request and at queue dispatch.
- Add ProjectRuntimeSelector and a two-column RuntimeModelPicker; manage
  link opens model connections; restore per-runtime default model tabs.
- Stop duplicating trusted instructions and history across OpenCode,
  Continue and DeepSeek Harness runtimes.
- Document the layered design, global provider removal plan and planned
  purpose-specific models.
2026-09-29 21:33:06 +08:00
mesalogo d1b92deff3 docs: update unreleased article through 86347af 2026-09-29 21:32:41 +08:00
mesalogo 9a9170441e feat: separate system task token usage and add usage refresh
Add a System tasks tab to run history that splits background usage (supervisor, auto-supervision, magic notes, knowledge base, delegation) from conversation usage, with a manual refresh button and automatic refresh when usage is recorded. Record previously dropped usage from supervisor reviews, image regeneration, knowledge graph extraction, and remote embeddings.
2026-09-29 10:38:37 +08:00
mesalogo 3c9229ac7a release: prepare Desktop 0.15.3
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
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
Document restored direct-model compaction, request-lifetime tool access, clearer cancellation and sidebar controls, reduced redundant startup work, and slimmer package inputs. Keep published Agent 0.15.0 and prior release notes unchanged. Review both feature matrices and retain Office editing as planned work. Typecheck, lint and bilingual notes validation passed; 85 focused tests passed and one POSIX-only test was skipped on Windows. Full source validation and production build remain CI gates before tagging.
2026-09-28 22:52:26 +08:00
mesalogo 86347af0b1 fix: refine sidebar search and composer shortcut hints 2026-09-28 22:38:35 +08:00
mesalogo 4baf4ec142 fix: restore manual context compaction for direct models
Local direct-model text conversations could not use manual context compaction,
and disabling automatic compression prevented saved summaries from being reused.
Restore the composer action regardless of the automatic setting or threshold,
use the selected summary connection, and persist summaries for subsequent
requests while retaining recent turns and full conversation history.

Image connections, remote channel conversations, and SSH projects remain outside
the local direct-model compaction workflow. Add focused regressions, a local
Electron end-to-end test, and an opt-in two-request live probe.

Validation: staged snapshot passed typecheck, changed-file ESLint, 13 focused
tests, and the Electron end-to-end test with two local HTTP fixture requests.
No additional real-model calls were made during commit verification.

Release note: 修复本地直连文本模型无法手动压缩上下文的问题。现在关闭自动压缩或尚未达到触发阈值时,也可点击“压缩上下文”;摘要会随会话保存并用于后续对话,同时保留最近轮次和完整历史。
2026-09-28 22:21:47 +08:00
mesalogo 1603871f7c fix: keep tool authorization valid throughout long requests
Remove the fixed gateway capability lifetime so active requests retain knowledge, notes, browser, configuration, and custom MCP access. Preserve token validation and request cancellation or cleanup revocation, and replace the misleading knowledge expiry error with a tool authorization error.

Remove native-client clock overrides and cover long-running scoped and HTTP MCP calls. Validation: 34 focused tests passed; changed-file lint passed. Repository typecheck and lint remain blocked by unrelated manual-compaction and sidebar probe files.

Release note: Long-running Agent requests no longer lose tool access after ten minutes, including when the knowledge-base switch is off. Request cancellation and completion still revoke access.
2026-09-28 21:36:41 +08:00
mesalogo 393a07ccdf fix: keep note capture actions compact and right-aligned
Remove initial generic validation instructions, show size errors beside their fields, and expose capture scope through contextual help. Keep save errors beside actions where space allows and preserve content-sized Save buttons in narrow note workspaces.

Release note: Note capture now uses a compact action bar with right-aligned buttons, on-demand scope help, and field-specific validation. Narrow workspaces no longer stretch the Save button.
2026-09-28 21:30:03 +08:00
mesalogo 50e8fde64c fix: soften sidebar dividers and tighten spacing 2026-09-28 20:54:56 +08:00
mesalogo 13ff182858 fix: reduce redundant work during chat and note startup
Avoid hidden activity renders, no-op message saves and attachment scans when pinning conversations. Reuse note body inspection and skip unchanged attachment reference writes while preserving recovery and cleanup behavior.

Skip unnecessary Markdown normalization and memoize subagent lookups without delaying live rich text. Add storage operation counts and real Electron rendering equivalence benchmarks; full-render speedup remains unproven.

Validation: 5397 tests passed, 84 skipped; typecheck passed. Lint passed excluding two existing untracked sidebar probe files.

Release note: Reduce repeated rendering and disk work during chat, conversation pinning and note startup while preserving existing content and interactions.
2026-09-28 20:53:43 +08:00
mesalogo 594222a2b5 fix: reduce desktop package size and loose files
Exclude validation outputs from desktop packages, ship renderer-only dependencies through Vite bundles, and omit DS Web type declarations, source maps, and PDB symbols. Preserve runtime assets and licenses without changing dependency versions.

Validate builder filters, production build and type checks, Windows packaged runtime smoke paths, and App page loading. Record measured size reductions and remaining validation limits in BUILD.md.

Release note: 减少桌面安装包中的重复依赖和非运行文件,降低安装后的磁盘占用与零散文件数量。
2026-09-28 20:10:19 +08:00
mesalogo 1c6264dd87 fix: group worktree diff actions with the file path
Move file actions above the divider alongside the path, allowing wrapping in narrow panels. Hide the redundant worktree heading unless staged changes also need a separate section.

Release note: Worktree diffs now group file actions above the divider and omit the redundant heading when only unstaged changes are shown.
2026-09-28 19:27:52 +08:00
mesalogo 120076a13d fix: group conversation search and standardize sidebar spacing
Move search to the top of the conversation area, remove its visible heading, and use a white surface in the light theme. Keep the list independently scrollable and sidebar controls reachable in short windows. Standardize footer divider spacing to 16px on both sides and update layout tests and design guidance.

Validation: 307 focused tests, typecheck, targeted ESLint, and four Electron layout scenarios passed.

Release note: Conversation search now sits above the conversation list in a distinct themed area, with consistent divider spacing and improved short-window scrolling.
2026-09-28 19:06:06 +08:00
mesalogo d8f0ef1301 fix: make browser Go button icon-only
Remove the visible Go label and use the toolbar icon-button width while preserving the tooltip and accessible name.
2026-09-28 19:00:44 +08:00
mesalogo 10f3b4f2c1 fix: center sidebar footer divider and correct Apps arrow
Use equal-width Apps and Settings buttons so the divider stays centered. Increase divider visibility and point the Apps arrow upward to match the menu opening direction.

Release note: Make the sidebar footer divider clearer and centered, and align the Apps arrow with its upward-opening menu.
2026-09-28 18:38:01 +08:00
mesalogo 58e6808f1f fix: deduplicate terminal notices and distinguish cancellation
Keep cancellation and failure details in message status while preserving existing replies. Render cancellation neutrally, restore terminal metadata from history, and offer Edit again only when input can be restored.

Release note: Cancelled and failed requests show a single notice; cancellation no longer appears as an error, and Edit again accurately describes the recovery action.
2026-09-28 18:10:10 +08:00
mesalogo 8650907b75 fix: distinguish Apps and Settings in the sidebar footer
Keep the horizontal footer layout with visible labels, matching button heights, a subtle divider, and an Apps dropdown indicator. Use a layout icon for Manage Apps so it is distinct from global Settings.

Release note: Make the bottom-left Apps and Settings entries easier to distinguish with labeled buttons and separate visual boundaries.
2026-09-28 18:08:52 +08:00
mesalogo c11f204b17 fix: align conversation list scrollbar with notes
Use a thin native scrollbar with a transparent track and theme colors for the conversation list on fine-pointer devices. Preserve system defaults for coarse pointers and forced colors.

Release note: The conversation list now uses the same subtle scrollbar style as notes, with native touch and high-contrast behavior preserved.
2026-09-28 18:06:20 +08:00
mesalogo be23c1346b docs: define live Office editing requirements and acceptance
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
Specify same-session Agent edits, visible changes, and Word, Excel, and PowerPoint acceptance scenarios. Record centralized deployment and ONLYOFFICE API licensing prerequisites as design requirements, not implemented capabilities.
2026-09-28 16:59:51 +08:00
mesalogo 47ba54f47d test: derive MCP settings expectations from the catalog
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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 / 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
Include Story Graph in the shared settings fixture and verify every catalog server by identity, rather than filtering buttons against an outdated list of five names. Preserve the existing count and interaction assertions. Complete SettingsPanel tests, typecheck, lint and bilingual notes checks passed; the full suite remains delegated to CI as requested.
2026-09-28 15:23:31 +08:00
mesalogo 76e781a0a8 release: prepare Desktop 0.15.2 and Agent 0.15.0
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
Add bilingual notes for Story Graph reads, full follow-up editing, note capture, diagnostics and website updates. Preserve published 0.15.1 history and include reviewed Host/cloud plans as explicitly unimplemented documentation. Agent remains 0.15.0 and requires Desktop 0.15.2. Typecheck, lint, notes and website validation passed; current-source Host runtime checks made four real model requests. User requested full-suite validation in CI rather than waiting for the interrupted local run.
2026-09-28 12:55:10 +08:00
mesalogo 0cdf04261a docs: update announcement draft with story graph access
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
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-09-28 12:48:45 +08:00
mesalogo 66f77b83c2 feat: let agents read supervisor story graphs
Expose saved decisions, timelines and source evidence through read-only Story Graph tools across Model, Harness and local/remote OpenCode and Continue. Supervisor enablement and runtime assignments govern discovery and calls; reads do not invoke models or modify stored evidence. Remote use requires matching Desktop and Agent changes.

Make Continue discussion show the complete editable message with a separated destination and shared action layout. Sending uses the visible draft without reattaching removed context.

Validation: existing focused MCP, runtime, controlled Linux Host, discussion component and Electron layout checks plus typechecks; staged diff check passed. Cross-scope fact reuse, historical reconstruction and full package acceptance remain deferred.

Release note: Agents can consult saved supervisor decisions and their sources when continuing work. Continue discussion now lets you review and edit the complete message before sending.
2026-09-28 12:38:00 +08:00
mesalogo 83a3ea261e fix: clarify site positioning and keep downloads prominent
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
Highlight domestic platform support and built-in coding agents, replace placeholder demos with task workflows, and add bilingual setup FAQs. Keep downloads directly after the hero and improve mobile demo layout.

Release note: Update the bilingual website with clearer product copy and demos, prominent downloads, and setup guidance. DeepSeek Harness and LoongArch retain their preview labels.
2026-09-28 12:10:22 +08:00
mesalogo 013e72f69e fix: simplify note capture controls and entry points
Remove duplicate quick-note shortcuts, redundant scope text and visible content labels. Right-align note action groups while retaining accessible input names and validation feedback.

Release note: Simplify the notes panel and align save and source actions to the right. Open notes from the workbar or conversation capture actions.
2026-09-28 11:45:52 +08:00
mesalogo 61590395ef fix: retain renderer exit details in diagnostic logs
Persist Electron renderer exit reasons and exit codes through diagnostic writes, reads, and exports so abnormal window exits can be investigated. Keep older records readable and existing log bounds unchanged.

Validation: 18 focused tests, typecheck, lint, and diff checks passed. Full test run timed out after 60 seconds with a SettingsPanel test failure.

Release note: Diagnostic logs now retain renderer exit reasons and exit codes to help distinguish out-of-memory exits from other window failures.
2026-09-28 11:36:01 +08:00
mesalogo 4e5239256d fix: keep bundled DS Web links relocatable
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
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
Preserve npm relative symlinks while preparing the desktop DS Web resources.
The previous copy rewrote CLI links to build-machine absolute paths, causing
both macOS targets to fail strict code-signature verification. The regression
runs the production preparation and builder matcher, removes the installation
and staging trees, and verifies the final link remains readable.

Recover the unpublished Desktop 0.15.0 as 0.15.1 without moving its tag.
Agent stays 0.15.0 and requires Desktop 0.15.1. Carry the unpublished notes
forward once and align the bilingual feature matrices and upgrade guidance.

Validation: complete frozen Linux tree 5372 passed, 51 skipped; Windows
packaging checks 45 passed and one POSIX-only test skipped. Typecheck, lint,
and bilingual release-note validation passed. Native macOS signing remains
an independent release check. Agent Runtime implementations are unaffected.

Release note: 修复本地 DS Web 随包依赖的链接复制,避免 macOS 安装包签名校验失败。
2026-09-28 09:51:01 +08:00
mesalogo 65a21390de test: retain created knowledge bindings across polling
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Model persisted library state in the external-binding fixture instead of returning pre-creation data to background refreshes. Keep all selected and excluded library assertions, without assuming the last snapshot request belongs to the new library. Windows App: 300 passed; complete frozen Linux suite: 5371 passed, 51 skipped. Typecheck, lint and bilingual notes validation passed.
2026-09-28 04:22:13 +08:00
mesalogo 706726bad6 test: wait for completed navigation before opening menus
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
Require the actual message article to own focus instead of matching body text. Wait for project runtime initialization before opening dismiss-on-blur composer options. Preserve all capture and native command assertions. Complete frozen Linux snapshot: 5371 passed, 51 skipped; focused Windows, typecheck and lint passed.
2026-09-28 03:47:29 +08:00
mesalogo 5cdffee9ee release: validate the complete 0.15.0 branch snapshot
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
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
Account for concurrent composer, conversation-order, desktop-notification,
and Supervisor discussion changes in bilingual notes and feature matrices.
Correct the removed entity-writeback description in the companion article.

Align the configuration fixture with the default-on notification setting,
scope note-source UI queries and wait for completed actions, and preserve
all real Obsidian child-process checks with a 15-second scenario allowance.

Validation: frozen complete tree passed 5371 Linux tests with 51 skipped;
19 focused Windows tests, typecheck, lint, and release-note validation passed.
The older 5345-test Host result did not cover all concurrent branch changes
and is not treated as validation of this complete candidate.
2026-09-28 03:06:41 +08:00
mesalogo d9e1e42288 release: finalize 0.15.0 validation and companion materials
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
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
Run conversation capture acceptance in a visible focused Electron window
and report concrete focus state on failure. Trigger Continue pre-spawn
cancellation at its existing environment callback instead of racing a timer.
Product implementation and approved bilingual note text remain unchanged.

Include the user's companion article updates and three reviewed draft images
as authorized. Keep their draft status and unverified build provenance clear.
Set the release date to September 28 and record the final real Linux result:
5345 passed, 51 skipped; Windows focused, typecheck, lint, and notes passed.
2026-09-28 02:26:06 +08:00
mesalogo d47f967781 fix: simplify continuing work from supervisor reviews
Let users continue discussions without opening sources first, edit a question, and send source-linked review context to the target conversation. Add explicit conversation navigation and remove redundant context disclosure and entity-writeback controls. Hide empty automatic supervision metrics until activity exists.

Release note: Continue work directly from supervision feedback or graph sources with relevant context, fewer controls, and clearer automatic supervision empty states.
2026-09-28 01:51:23 +08:00
mesalogo f35198df01 feat: add desktop notification toggle to general settings
Add a default-on persisted desktop notification switch that takes effect immediately for background task results while leaving in-app notifications unchanged. Group General settings into shortcut, conversation and notification, and workspace and download cards.

Validation: 756 related tests passed, 1 skipped; typecheck and lint passed. Full suite timed out with three supervision preview failures; native OS notification delivery was not manually verified.

Release note: 通用设置新增默认开启的桌面通知开关,可随时关闭系统任务提醒并保留应用内提示;设置项按用途分组,方便查找。
2026-09-28 01:43:21 +08:00
mesalogo 8e607366ca fix: keep conversation order stable during streaming
Sort pinned and unpinned conversations by their latest message creation time so parallel replies no longer repeatedly swap sidebar positions. Include the same timestamp in lightweight summaries while preserving updatedAt for persistence and snapshot merging.

Validation: 13 focused tests, typecheck and ESLint passed. The full test run timed out after ten minutes and reported configuration-summary and Agent offline-install failures.

Release note: Conversation order stays stable while replies stream or tools report progress; new messages still move conversations forward, with pinned conversations first.
2026-09-28 01:40:01 +08:00
mesalogo ee1bf8a187 fix: unify composer tool button styles
Use matching button and icon sizes, spacing, and hover feedback for attachment, voice input, and composer options while preserving recording and expanded states.

Release note: 修复输入框附件、语音和设置按钮的样式不一致问题,统一尺寸、间距和悬停效果。
2026-09-28 01:08:54 +08:00
mesalogo b8b6007b51 test: synchronize narrow notes keyboard acceptance on Linux
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
The real Linux Host reproduced Escape being sent to the collapsed workbar
trigger after resize. Wait for the narrow viewport, open the workbar through
its existing control, and confirm the append field owns focus before input.

Keep the migration fixture and all IO assertions, with a 15-second allowance
for its 10000-record scenario. Product behavior and approved notes are unchanged.

Validation: real Linux Host full suite 5345 passed, 51 skipped; Windows focused
3 passed; complete typecheck, lint, and release-note verification passed.
2026-09-28 00:49:24 +08:00
mesalogo 12594961fe test: stabilize conversation capture checks in release CI
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
Focus the real Electron window before native keyboard input and give the
two long-history App scenarios the existing 15-second per-test allowance.
Keep all capture, source navigation, persistence, and keyboard assertions.

Record the initial Linux CI failures and the revised local validation:
5317 passed, 79 skipped, with typecheck, lint, and release notes verified.
Product behavior and approved Desktop/Agent 0.15.0 notes are unchanged.
2026-09-28 00:00:19 +08:00
mesalogo 5a6ba6c14f release: prepare Desktop and Agent 0.15.0
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
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
Align Desktop and Agent candidates at 0.15.0 and include the approved
Chinese and English release notes. Document Obsidian, conversation capture,
local native-client preview boundaries, and knowledge and Supervisor fixes.

Desktop upgrades storage to schema 48. Agent 0.15.0 requires Desktop 0.15.0;
remote telemetry opt-outs apply to newly started Runtimes. Keep the two
release channels independent and exclude the experimental LoongArch build.

Validation: 5317 tests passed, 79 skipped; typecheck, lint, and release-note
verification passed. Current-source Linux x64 OpenCode and Continue passed
Ask/Execute and telemetry-environment checks. Record preliminary probe
limitations and model-call accounting in the release preparation document.

Release note: Desktop 与 Agent 同步准备 0.15.0,新增 Obsidian、会话笔记采集及本地原生客户端预览,并修复知识库、监督者、界面与 Runtime 启动体验;升级前请备份完整用户数据,远程用户先升级桌面端再更新 Host。
2026-09-27 23:01:02 +08:00
mesalogo 0f8bf405db fix: remove persistent DS service hint from composer
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
Remove the persistent independent-session and service-lifecycle description beneath the native client action. Keep the stop action and existing service lifecycle unchanged, and align the proposal documentation.

Release note: 移除 DS 浏览器入口旁常驻的会话和服务说明,简化输入区;停止服务操作保持可用。
2026-09-27 22:16:47 +08:00
mesalogo 5ecc19dfe0 fix: include DS Web dependencies in portable packages
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
Copy the prepared DS node_modules contents directly so electron-builder retains the CLI entry and nested Web assets. Fail portable output validation when the DS entry is absent, preventing browser launch from failing with ENOENT.

Add regression coverage using the actual builder matcher and include the pending Continue/OpenCode article screenshots.

Validation: 3 focused packaging tests passed; staged whitespace checks passed. No new full-suite or remote Host validation was performed for this commit.

Release note: 修复 Windows portable 包遗漏 DS Web 依赖导致无法在浏览器中打开的问题;构建时会检查启动入口是否完整。
2026-09-27 22:09:47 +08:00
mesalogo 4a2986ca5d test: correct HeartbeatCenter nested navigation assertions
Fix the two Chinese and English test failures by scoping page and graph tabs to their accessible names. Cover selection, tab/panel associations, keyboard focus and graph content while preserving non-graph navigation assertions.

Clarify the top-level navigation design and record the follow-up validation in the quality and workbar docs. No production code changes.

Prior validation: 96 tests passed across HeartbeatCenter (42), SupervisorWorkspace and WorkspacePrimitives; npm run typecheck and npm run lint passed. The full repository suite was not rerun. Commit-time staged diff review and whitespace checks passed.
2026-09-27 22:09:04 +08:00
mesalogo 2713a81bd9 fix: restore consistent controls, usable overlays and narrow layouts
Resolve 15 shared-control, overlay and responsive-layout issues plus the MCP editor being hidden behind settings. Restore theme contrast, danger and disabled states, visible keyboard focus, and consistent Continue fields. Keep the workbar add button reachable and model protocol values readable in narrow containers.

Make nested MCP and image dialogs usable above their parents with focus restoration, keep project Escape handling within the active modal, release native browser views for overlapping help, and restore keyboard access and Escape in sandboxed HTML previews without relaxing sandbox permissions. Add unit and real Electron regression coverage and update design and validation documentation.

Release note: 修复深浅主题下控件边框、危险操作、禁用状态和键盘焦点显示不一致的问题。MCP 编辑器与图片预览可在父弹窗上正常操作,HTML 全屏预览支持键盘进入和 Escape 返回;窄工作栏的添加按钮保持可用,模型连接协议文字完整可读。

Validation: the full suite recorded 5309 passed, 3 failed and 83 skipped. The ProjectSwitcher initial-focus regression was then fixed, and App, ProjectSwitcher and overlay Electron tests passed all 331 cases. The other two HeartbeatCenter failures were reproduced on isolated HEAD 11facf0 and remain pre-existing. Typecheck and lint passed after the fix; staged whitespace checks passed. The full suite was not rerun after the focused fix.

Limits: Electron evidence is from Windows development runs, with fixtures for unrelated services and native occlusion throttling disabled in some drivers. No production build, packaging, macOS/Linux, remote Agent host or external MCP connection validation; skipped tests and the documented scaling and native-select coverage gaps remain.
2026-09-27 21:57:35 +08:00
mesalogo 11facf0392 fix: restore destructive dialog buttons and document recent updates
Destructive confirmation buttons lost their styling outside the notes page, including the draft-discard Portal. Share button sizing, typography, disabled feedback and theme-aware hover colors across callers, with Electron and contrast regression coverage and design documentation.

Include the update article draft covering v0.13.8 through v0.13.16 and the separately identified 5116613 follow-up, its verification record and article index. The article remains unpublished with screenshots and release details pending.

Validation: 37 focused tests passed, including real Electron capture; typecheck, lint and staged whitespace checks passed. No model calls. The previously documented unrelated HeartbeatCenter full-suite failure was not rerun.

Release note: Destructive confirmation buttons, including the discard-note-draft dialog, now display consistent styling, keyboard focus and disabled feedback in light and dark themes.
2026-09-27 15:34:01 +08:00
mesalogo 0f0b6ac21d fix: skip Continue update status when updates are disabled
Continue still displayed Checking for updates even when version lookup was disabled. Skip the update service before it emits checking state and retain the lower-level lookup guard in the shared local and Agent adapter.

Add a regression for disabled and normal update paths and record real terminal and production Electron validation. Seven real model requests returned HTTP 200; Linux validation covered startup only. The reported UI freeze was not reproduced, full Agent/model acceptance remains open, and full-suite failures are documented.

Release note: 修复 Continue 禁用自动更新后仍显示 Checking for updates 的问题,终端现在保持正常空闲状态。
2026-09-27 15:23:41 +08:00
mesalogo eb53c827c6 fix: apply runtime telemetry settings to web and remote clients
DS Web and remote Agent launch paths omitted shared telemetry opt-out settings. Reuse one environment definition for local launches, remote runtime profiles, and the remote model bridge helper while preserving existing update controls and user-requested networking.

Validation: 16 focused tests passed at commit time. Prior local real-model validation passed; modified Agent real-Host validation and background network monitoring remain outstanding.

Release note: 补齐 DS Web 与远程 Runtime 启动时的遥测关闭配置,保留现有自动更新关闭设置,不影响模型、MCP 和用户主动联网请求。
2026-09-27 13:57:26 +08:00
mesalogo 8b15399577 fix: prevent workbar scroll arrow from overlapping add button
Hide disabled scroll arrows at tab strip boundaries for hover, keyboard focus, and touch input. Update the workbar specification to match.

Release note: Hide the workbar scroll arrow at the end of the tab strip so it no longer overlaps the add button.
2026-09-27 13:30:22 +08:00
mesalogo 67dedc2ed2 fix: simplify knowledge action menus
Remove repeated document names from action labels, shorten the reparse label, and narrow knowledge menus to 220px while preserving other menu widths and viewport bounds. Keep object context on menu triggers and removal confirmations.

Validation: 72 focused and Electron layout tests, typecheck, lint, and diff checks passed.

Release note: 知识库操作菜单去掉重复文件名,将操作简写为重新解析,并收窄菜单宽度,减少换行。
2026-09-27 13:11:09 +08:00
mesalogo ceb275bdb8 fix: clarify knowledge document actions and task history
Restore URL identity in merged rows and replace crowded document actions with a primary entry and accessible menu. Organize preview navigation and maintenance actions, and show task history as batches with matching counts and filters.

Keep preview task scope consistent for multi-document sources and track pending operations independently so one source does not block another source's pause action. Include responsive Electron layout and behavioral regressions, bilingual copy, and design documentation.

Validation: focused regressions, typecheck, lint, and diff checks passed. Electron fixtures cover light/dark and wide/narrow layouts. Full tests reported two previously known HeartbeatCenter assertions; real import and parsing integration was not rerun.

Release note: 知识库文档列表保留来源网址,统一查看入口和更多操作,并整理解析结果页。任务历史按批次展示,筛选计数与当前对象一致,不同来源的操作互不阻塞。
2026-09-27 12:55:50 +08:00
mesalogo 44cd944601 refactor: extract chat history pane and clarify knowledge ownership 2026-09-27 10:12:10 +08:00
mesalogo 40c108da1a fix: organize story graph lists into tabs
Replace the stacked event, entity and relation lists with shared tabs and an independently scrolling panel. Preserve graph selection when switching tabs and follow canvas selections in the sidebar. Update translations, UI documentation and focused interaction and Electron layout coverage.

Release note: Browse story graph events, entities and relations in separate sidebar tabs, with counts and synchronized canvas selection.
2026-09-27 10:06:35 +08:00
mesalogo 5177b47aeb fix: unify knowledge documents and sources
Merge single-document sources into one row, keep grouped directory documents and failed sources visible, and share search and task actions. Compact the drop zone and move secondary actions into a disclosure.

Include the existing knowledge workspace component extraction required by this implementation, focused regression tests, bilingual copy, and design documentation.

Validation: 94 focused tests, typecheck, lint, and diff checks passed. Full tests timed out after 200 seconds; two unrelated HeartbeatCenter tablist failures were reproduced separately. Electron visual acceptance remains unverified.

Release note: 知识库的内容来源与文档索引合并为统一列表,减少重复记录和操作;支持统一搜索,并保留目录分组和失败来源的恢复入口。
2026-09-27 10:05:50 +08:00
mesalogo 4edf06b685 refactor: extract model settings IPC handlers 2026-09-27 09:03:17 +08:00
mesalogo f81f342cde feat: open local runtime clients from the composer
Add composer shortcuts for local Continue and OpenCode terminals and the official DeepSeek Harness Web client, using the current project and model bridge. Manage browser service reuse, stop actions and terminal cleanup. Upgrade DeepSeek Harness to 0.1.7-rc.2 and Cordis to 4.0.4.

Include the coordinated IPC handler extraction and related tests and documentation. Remote native-client coordinator wiring, managed Node consolidation, Ask MCP mappings and full desktop/package acceptance remain unfinished; this commit records the implemented local workflow and supporting SSH groundwork.

Validation: typecheck and lint passed; 267 focused tests passed and 5 skipped. No new real-provider calls were made for this commit. Prior real-model evidence and remaining acceptance work are documented in the feature progress files.

Release note: 本地项目选择 Continue 或 OpenCode 后,可从输入框下方打开原生终端;选择 DS 后,可打开使用当前项目和模型的官方 Web 客户端。客户端使用独立会话,DS 网页关闭后服务继续运行,可在 GoodBuddy 中停止;远程入口尚未完成。
2026-09-27 08:38:00 +08:00
mesalogo 1094433a7d feat: connect conversations with Magic Notes
Let users retain useful assistant replies or complete conversations without
leaving their chat workflow. Preview and edit captured text, choose an existing
note or atomically create a note with its first entry, and return to the source
conversation or message from saved records.

Add an application-scoped Notes workbar tab with title/body search and quick
text appends. Preserve drafts across conversation switches and guard closing,
replacement and disabling. Schema 48 stores optional source metadata without
rewriting file-backed bodies; source deletion retains the saved note. Older
clients cannot reopen an upgraded database without a pre-upgrade backup.

Release note: 可将助手回答或完整会话预览、编辑后加入魔法笔记,并从笔记返回原会话或消息。右侧快速笔记支持搜索和追加文字,切换会话保留草稿;采集本身不调用模型,保存后自动评论仍遵循已有设置。

Validation: isolated HEAD-based candidate passed 22 files / 930 focused tests,
plus 20 gateway tests after including its create-result fixture adjustment.
Real Electron App/preload/IPC/SQLite acceptance passed for all 502 historical
messages, exact-entry/source navigation, native keyboard input in a 214px
panel, reopen and source-deletion retention, with zero capture model calls.
Repository lint and web TypeScript passed. Cached whitespace and secret-pattern
checks passed; native-client changes and unrelated IPC extraction are excluded.

Validation limits: the isolated source tree reuses installed node_modules,
where a parallel task upgraded DSH from the committed 0.1.2-rc.1 to 0.1.7-rc.2.
Full typecheck reports 14 DSH errors in three unchanged files; the full-suite
attempt with --bail=1 also fails in deepseek-harness-host.test.ts. These are not
all-green repository or release-package results. Earlier execution-time full
suite failures and fixture repairs remain recorded in the feature progress doc.
2026-09-27 08:17:54 +08:00
mesalogo 55efca3ee1 feat: connect local Obsidian vaults to conversations
Users can now read, search, and update existing local Obsidian notes through
built-in tools without configuring an external MCP server or Obsidian plugin.
Add vault discovery, an optional folder scope, and independent test, save,
enablement, and runtime assignment controls in MCP settings.

Bundle MCPVault 0.16.0 and its runtime dependencies, exposing 19 shared tools
to direct models and managed local OpenCode and Continue conversations.
Obsidian is disabled by default. Ask remains read-only; Execute permits
writes. Scope and assignment changes revoke existing Obsidian grants.
DeepSeek Harness and remote execution remain unsupported.

Validate the isolated staged snapshot with its own dependency installation:
typecheck, lint, production build, and 540 tests across 14 files passed.
No paid model calls were repeated. Physical-offline, installer, and other
platform acceptance remain pending as documented in the feature record.

Release note: Connect local Obsidian vaults in MCP settings to find notes and
write conversation results back to them. Test all registered vaults or a
chosen folder before enabling access; no Obsidian plugin is required.
2026-09-27 00:37:46 +08:00
mesalogo 5116613615 fix: keep supervisor controls usable and prevent overlapping reviews
Allow only one active review across manual starts, automatic runs and resume. Add cancellation that holds admission through runtime cleanup, preserves saved batches and prevents cancelled runs from resuming.

Keep review controls usable during execution, show a dismissible progress notice and remove redundant filter labels. Include focused regressions and lifecycle documentation.

Release note: Supervision reviews can now be cancelled from Activity records. Starting another review shows the active review instead of duplicating work, while scope, time and refresh controls remain available.
2026-09-26 15:55:50 +08:00
mesalogo 0759d5dcff release: prepare Desktop 0.13.16
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
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Package approved bilingual notes for history upgrade reclamation, continuous
Supervisor reviews, and clearer review/activity layouts. Align feature lists
and retain published notes. Agent remains 0.13.4; no schema bump or LoongArch.

Update two stale layout assertions to match removed repetitive copy.
Record candidate validation and the isolated Mermaid rerun without claiming
a clean full-suite result. Require main CI and production build before tagging.
2026-09-26 13:43:53 +08:00
mesalogo 325b702934 fix: clarify supervisor review and activity layouts
Work Review could place its empty state beside the controls, while redundant copy and secondary activity statistics obscured progress. Use a full-width vertical review flow with a separate run status, connected review stages, prominent saved-batch coverage, and expandable secondary details.

Validation: 33 component tests, 40 Electron fixture layout cases, typecheck, targeted ESLint, and diff checks passed. No model calls.

Release note: 修复工作回顾空态布局,精简顶部控件与重复说明。活动记录通过相连阶段和已保存批次展示进度,次要统计可在详情中展开。
2026-09-26 12:53:10 +08:00
mesalogo 4c7193e1ea fix: avoid history reclamation during structure-only upgrades
Ordinary database schema upgrades previously ran VACUUM on free pages and
showed misleading history-cleanup progress. Separate structure upgrades from
required legacy conversions, retaining reclamation intent only for retries
within the same startup. Preserve released data without new schema versions
or persistent migration markers; Agent and normal writes remain unchanged.

Add real SQLite and Worker regressions, bilingual operation-specific startup
copy, and migration documentation. Synchronize native Electron drag input
with actual resize state without weakening layout assertions.

Validation: 47 focused migration tests passed; typecheck, lint and diff checks
passed. Final full suite: 5137 passed, 74 skipped, 2 unrelated Electron failures.
Both failing Electron tests passed an isolated rerun; full CI remains unverified.

Release note: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
2026-09-26 12:42:08 +08:00
mesalogo e6ac0734de fix: keep supervisor reviews running until completion
Remove elapsed execution-budget pauses from extraction and navigation so long reviews no longer require repeated manual continuation. Remove the pause-duration setting and update activity copy and design documentation. Preserve explicit pause, saved batches, model timeouts and concurrency limits.

Validation: recorded 13 focused tests, typecheck and targeted ESLint passed; staged diff check passed. Tests were not rerun for this commit.

Release note: 监督回顾现在持续分批处理至完成,不再每隔固定时长自动暂停;仍可主动暂停并从已保存进度继续。
2026-09-26 11:02:30 +08:00
mesalogo e8d93b5bb6 release: prepare Desktop 0.13.15 recovery
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
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Carry the approved unpublished 0.13.14 release notes into 0.13.15 after
stabilizing the CI UI tests. Remove the duplicate unpublished entry and
update the bilingual feature matrices and recovery evidence.

Keep published Agent 0.13.4 unchanged. Its minimum Desktop requirement is
0.13.14; users should install this recovery release before updating Hosts.
Schema 47 still requires a pre-upgrade backup for rollback.

Release-note verification and nine focused tests passed. Require this
candidate main-branch validation and production build before tagging.

Release note: 承接监督回顾、待办布局、原生搜索和远程恢复更新;请先升级 Desktop 0.13.15,再更新 Host 至 Agent 0.13.4。
2026-09-26 02:05:59 +08:00
mesalogo cd27163660 test: stabilize asynchronous UI checks in release CI
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
Control the native inventory retry response and wait for the complete
browser-close result instead of asserting on intermediate renders.
Run native note-layout input in a visible isolated Electron window,
scan the fixture dependencies up front, and report timeout explicitly.
Keep all behavior and geometry assertions and Electron sandboxing.

The three focused regressions, full typecheck, and lint pass locally.
Linux CI must confirm the release failures are resolved. No product,
Agent, version, or release-tag changes are included.
2026-09-26 01:53:04 +08:00
mesalogo 107dc974e3 merge: retain published Desktop 0.13.13 history
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
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
Merge the published release preparation commit without rewriting either
branch. Keep the approved Desktop 0.13.14 and Agent 0.13.4 versions and
bilingual notes, and restore the historical release documentation.

The merged product source, tests, dependencies, and approved release notes
are identical to d23af99, which passed 5127 tests. Post-merge release-note
checks, nine focused tests, full typecheck, and lint passed.
2026-09-26 01:02:55 +08:00
mesalogo d23af99708 release: prepare Desktop 0.13.14 and Agent 0.13.4
Prepare the approved bilingual release notes for paged Supervisor reviews,
resizable to-dos, native direct-model searches, and remote recovery fixes.
Agent 0.13.4 requires Desktop 0.13.14; schema 47 requires a pre-upgrade
backup for rollback. Keep Desktop and Agent publication separate.

Include the existing validation fixture corrections and helper patches as
requested. Full validation passed 5127 tests with 74 skipped, typecheck,
lint, and release-note checks. Current-source Linux Host completion and
cleanup probes passed with two real model requests.

Release note: 改善监督回顾与续跑、待办布局和搜索;配套 Agent 修复远程完成状态并清理停用安装。请先升级桌面再更新 Host,升级前备份用户数据。
2026-09-26 00:59:27 +08:00
mesalogo ff746bdc1a fix: preserve native ripgrep capabilities for direct models
Replace the restricted workspace_rg parameter mapping with native args and optional cwd. Preserve rg output and exit codes, and reuse process cancellation and output_read pagination so large search results remain accessible. Ask remains read-only within the workspace; Execute uses current-account permissions.

Validation: focused regression 169 passed, 1 skipped; typecheck and lint passed. Real DeepSeek Ask and Execute pagination scenarios passed with six counted requests in the successful run; an earlier failed fixture run has no retained request count. Full-suite validation is not green: the recorded run had unrelated failures and the latest run timed out without a summary. The remote Agent does not use this local execution path.

Release note: 直连模型可使用 ripgrep 原生参数搜索文件并保留原生结果,大输出可通过 output_read 完整续读。Ask 保持工作区只读,Execute 使用当前账号权限。
2026-09-25 23:58:21 +08:00
mesalogo d4bc10c0f3 fix: use full width for supervisor recap and activity
Remove the extra reading and dashboard width caps so recap controls, results, and activity details fill the supervisor panel. Update layout assertions and UI documentation.

Release note: 修复监督者工作回顾与活动记录页面偏窄的问题,内容现在铺满页签下方的可用宽度。
2026-09-25 23:16:47 +08:00
mesalogo db5e43d7d5 fix: complete supervisor review coverage and execution workflow
Replace truncated review input with paged, resumable batches and retain extracted facts and source references through navigation summaries. Expose concurrency, timeouts, batch budgets and response capacity in supervisor settings.

Separate automatic supervision from review results, activity and settings; use a standard plan modal and show execution stages with saved batch details. Fix historical entity identity mapping, navigation output validation and arbitrary summary length failures.

Include focused regressions, isolated real-data validation scripts and updated design and verification records. Overall layout acceptance remains outstanding; this commit does not claim that all visual issues are resolved.

Release note: 监督回顾支持完整分页、分批保存和暂停后继续,可配置并发、超时及响应容量;修复实体引用、汇总校验和长摘要报错,并重新组织自动监督与活动记录入口。
2026-09-25 22:32:43 +08:00
mesalogo f68584b4e2 feat: resize todo panes and collapse note groups
Let users drag the to-do list/detail divider and remember its width. Note headings independently collapse task rows without clearing selected details. Use a 1px divider with a transparent drag target and compact spacing.

Validation: 115 focused tests including native Electron input; typecheck and lint passed.

Release note: Resize the Magic Notes to-do list with a thin draggable divider and collapse tasks by source note.
2026-09-24 16:17:16 +08:00
mesalogo 7adae97baa fix: restore remote conversation recovery and clean obsolete agents
Wait for model delivery acknowledgements before completing remote prompts and preserve completed outcomes when idle Runtime cleanup fails. Explicit messages after an unknown outcome now open a fresh binding with history instead of reusing the failed session; recovery remains attach-only.

Run obsolete payload cleanup through the current Agent after activation, retaining history and unresolved process ownership. Live legacy daemons and shared Runtime versions remain deferred. Includes focused regressions, local OpenCode simulation, and isolated Linux validation evidence. Typecheck and lint passed; the full test run timed out.

Release note: Remote conversations can continue after an uncertain result, and completed answers no longer fail solely because Runtime cleanup fails. Updates remove verified inactive Agent payloads and owned orphan processes while retaining active or uncertain installations and conversation history.
2026-09-24 10:39:28 +08:00
mesalogo 0ed1be1773 fix: widen project picker beyond the sidebar
Float the project picker at 380px with viewport bounds and preserve menu focus and dismissal. Align local package metadata and packaged release notes with Desktop 0.13.13.

Release note: 项目下拉列表可浮出侧栏,宽度调整为 380px,窄窗口自动收窄。
2026-09-24 00:34:08 +08:00
mesalogo d5f6dc6990 fix: hide queue attachment action for messages without attachments
Derive attachment presence from saved queue inputs and only show the preview action when attachments exist. Cover queue metadata and button visibility with regression tests.

Release note: Queued messages without attachments no longer show the View attachments button.
2026-09-23 23:44:08 +08:00
mesalogo c71c0adf9e fix: place supervision feedback below project overview
Order the task center as project overview, supervision feedback, then project tasks. Update the sidebar layout documentation to match.

Release note: 任务中心的监督反馈现位于项目概览下方、项目任务上方。
2026-09-23 23:11:33 +08:00
mesalogo 344008aaec fix: align note thumbnails with content edges
Remove extra top and left padding from the note record index so thumbnails align with the content area. Draw keyboard focus outlines inward to keep them visible within the scroll viewport, and cover alignment in the Electron layout regression test.

Release note: Note thumbnails now align with the content area, and keyboard focus outlines remain visible at the list edges.
2026-09-23 23:03:09 +08:00
mesalogo cebd513da8 fix: restore full workbar resize hit area
Keep the workbar divider's 9px hit target inside the sidebar so overflow clipping no longer makes it difficult to drag. Reserve matching space beside browser and terminal content, and remove it in browser fullscreen mode.

Validation: 68 sidebar tests, typecheck, lint, and Chromium hit tests passed. Full suite timed out after 10 minutes and reported three supervision test failures.

Release note: Make the divider between chat and the browser or terminal workbar easier to grab by restoring its full resize area.
2026-09-23 22:51:55 +08:00
mesalogo 2ca19ebf50 test: allow more time for large model bridge encoding
Give the 600 KiB request encoding round-trip test 15 seconds instead of the default 5 seconds after a CI timeout. Preserve the payload size, all assertions, global test timeout, and production behavior.

Validation: all nine model bridge contract tests and file-scoped ESLint passed. The full typecheck/lint command was cancelled before its completion could be confirmed.
2026-09-23 22:38:06 +08:00
mesalogo 7756231b3b release: prepare Desktop 0.13.13
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
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 a Desktop-only patch for repeated activity-history reads during streaming and the supervision feedback sidebar layout. Keep Agent 0.13.3 and the database format unchanged; no Host update is required.

Update bilingual release notes, feature matrices, and validation evidence. Isolate this candidate from the original worktree and exclude its uncommitted supervision timeout tests. Validation passed 192 focused tests, an additional six-case Electron sidebar scenario, typecheck, lint, and release-note checks.

Release note: 减少流式活动历史的重复读取,修复监督反馈侧栏的按钮布局和目标名称显示;Agent 保持 0.13.3。
2026-09-23 22:02:55 +08:00
mesalogo b16fc900bb fix: restore supervision feedback sidebar layout
Replace unstyled full-width pin and refresh controls with shared icon buttons in the feedback header. Show conversation and task titles instead of raw IDs, with responsive spacing and keyboard focus coverage.

Release note: 修复任务中心监督反馈按钮样式和布局,显示会话或任务名称,并改善窄侧栏下的阅读与操作体验。
2026-09-23 21:52:36 +08:00
mesalogo bf8ff7f78e fix: avoid repeated activity history reads during streaming
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
Reuse the last committed activity records when comparing streaming saves instead of reading the full history for every update. Invalidate comparisons on close, data clearing and commits from other SQLite connections; preserve rollback and retry behavior.

Validation: 3 related test files / 121 tests passed; typecheck and lint passed. Ten consecutive saves now perform at most one full-history scan while preserving the WAL write reduction. Installed-app physical disk throughput remains unverified.

Release note: Reduce repeated database reads while streaming activity updates in conversations with large activity histories.
2026-09-23 21:49:58 +08:00
mesalogo bb8c04af6c fix: use bundled OpenCode path in isolation tests
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
Linux release CI could not start OpenCode because the isolation test assumed a platform-specific npm executable name. Reuse the production runtime path resolver, which matches the installed bin/opencode.exe on all platforms.

Validation: nine focused tests, typecheck, lint, and bilingual release-note verification passed. Product behavior, versions, and release notes remain unchanged; main-branch CI must pass before tagging.
2026-09-23 20:09:05 +08:00
lofyerandGitHub 073aa45a30 Merge pull request #7 from mesalogo/jojocoding-patch-6
Update README.zh-CN.md
2026-09-23 19:29:05 +08:00
lofyerandGitHub 1cf74d3d2c Merge pull request #6 from mesalogo/jojocoding-patch-5
Update README.md
2026-09-23 19:28:44 +08:00
mesalogo 163e245b00 fix: stabilize release CI regression tests
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
Address Desktop CI 35849362694 failures with isolated OpenCode config dependencies, cleared IPC lookup counts, Supervisor default-off expectations, and polling-safe UI fixtures and timeouts.

Record the failed candidate CI and follow-up validation: 14 focused tests, typecheck, and lint passed. Product code, versions, and bilingual release notes are unchanged. The revised candidate requires approval and successful main-branch CI before tagging.
2026-09-23 19:15:08 +08:00
mesalogo bf8ece0590 release: prepare Desktop 0.13.12 and Agent 0.13.3
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
Complete the pending Supervisor changes with opt-in execution, incremental automatic reviews, and activity history. Preserve saved preferences and plans, skip model calls for unchanged input, and keep full replay and cancellation limitations explicit.

Update both release versions, bilingual notes, feature matrices, migration fixtures, and validation evidence. Clear stale browser errors after a successful close retry. Focused validation passed 690 tests across 35 files, plus notes verification, typecheck, and lint; full CI and native release acceptance remain pending. No local production build or packaging was run.

Release note: Enable Supervisor to review work, trace story graphs, and inspect manual or automatic activity without repeating model calls for unchanged input. Desktop 0.13.12 and Agent 0.13.3 also prepare idle remote Runtime cleanup and retirement of replaced Agents; update Desktop before the Host environment. Database upgrades require a pre-upgrade backup for rollback.
2026-09-23 18:20:23 +08:00
mesalogo 83a48a4079 fix: reclaim idle remote runtimes and retire replaced agents
Remote tasks could leave idle Runtime processes behind, and replaced Agent
installations lacked a drain-and-exit lifecycle. Keep the current Agent
resident, let displaced Agents finish existing work and reconnect for its
results, then retire them without restarting obsolete installations.

Reclaim Runtime processes when their last active task finishes, preserve
pending questions and shared active tasks, and restore conversation history
when the next turn starts. Remove the Remote Projects technical preview label
from settings, errors, tests, and current documentation; the feature remains
disabled by default.

Validation recorded during implementation: 213 lifecycle tests and 28 focused
UI/IPC tests passed, as did SettingsPanel and lint. Linux x64 real-Host checks
used 8 provider requests. Broader tests and final typecheck encountered
unrelated concurrent changes; details and coverage limits are documented.

Release note: 远程任务结束后自动释放空闲运行进程,下一轮对话恢复历史;更新后的旧 Agent 等待已有任务完成再退出,当前 Agent 保持常驻。远程项目移除“技术预览”标识,仍默认关闭。
2026-09-23 17:37:14 +08:00
mesalogo cdd573778c fix: show expandable project and conversation usage totals
Group usage into project or conversation totals with collapsed Runtime/model children. Keep model grouping flat and preserve expanded rows when usage refreshes. Include aggregation and interaction tests plus matching documentation.

Validation: 25 focused tests, typecheck, and lint passed. Full suite timed out after 200 seconds with four failures reported in ipc.test.ts and ApplicationMenu.test.tsx.

Release note: Project and conversation usage now show totals first, with model details available by expanding each row.
2026-09-23 16:33:13 +08:00
mesalogo 0265cfb762 fix: group and label note page controls
Move the record index toggle beside the back button and show text labels for both index and AI comment controls. Preserve pane visibility, keyboard focus, and narrow-window behavior; update tests and documentation.

Validation: 112 workspace tests and Electron layout regression passed; typecheck and lint passed. Full suite hit unrelated test failures and timed out.
2026-09-23 15:47:54 +08:00
mesalogo fb4b9bdd46 fix: float Git branch picker above workspace content
Render the searchable branch picker in the shared floating portal so opening it no longer pushes down changes or commit history. Clamp it to the viewport and retain search focus, outside dismissal, and Escape focus restoration.

Update existing migration fixtures for activity history and supervision schema changes. Focused suites passed: 149 storage tests, 41 UI tests, and the Electron overlay test; typecheck and lint passed. The full suite did not complete cleanly and reported failures outside this change.

Release note: The Git branch picker now opens over the workspace without displacing the file list or commit history.
2026-09-23 15:27:33 +08:00
mesalogo 984f16f178 fix: reduce activity history disk writes during streaming
Save only changed activity records instead of rewriting the entire history snapshot. Migrate existing history to schema 44 while preserving order and duplicate IDs.

Run unit tests with up to four workers and keep the configured integration phase serial. Include activity-history I/O, storage-upgrade, settings, and OpenCode isolation regression coverage.

Validation: 4 related test files / 123 tests passed; typecheck and lint passed.

Release note: Reduce disk writes while agent activity updates stream in, preserving existing activity history.
2026-09-23 14:54:21 +08:00
mesalogo 7d487f9326 feat: add supervisor workspace and traceable story graph
Extend the existing heartbeat workspace with scoped work reviews, result-specific story graphs, source inspection, and pinned conversation or task feedback. Connect confirmed continuation and local knowledge entity previews through the desktop APIs, and preserve result history and user corrections in schema 43.

Include focused regression tests, an isolated visual fixture, and implementation evidence. Full event-by-event replay, Experiment targets, no-change handling, and cancellation remain incomplete; this commit does not claim complete feature acceptance. Existing automatic plans remain active when the application entry is disabled.

Release note: Review work progress in the Supervisor workspace, trace graph events to saved sources, and open a matching review from pinned conversation or task feedback.
2026-09-23 13:49:13 +08:00
mesalogo 785b1ee468 fix: keep Mermaid diagram edges reachable and add PNG export
Fix oversized diagram alignment in the large-diagram viewer and provide complete PNG downloads independent of zoom and pan. Cover the full window and shared native drag handling, with regression checks for both themes and narrow windows.

Validation: 58 focused tests passed, including 9 Electron scenarios and 10 PNG downloads; typecheck and lint passed. Full-suite runs did not complete.

Release note: Mermaid 大图放大后可浏览全部边缘,并可从弹窗导出完整 PNG 图片。
2026-09-23 11:53:09 +08:00
mesalogo be84534e77 fix: simplify notifications and shorten display time
Use compact neutral notification cards with vertically aligned icons, text, and close controls. Dismiss success and info messages after two seconds; keep errors visible until dismissed.

Release note: Notifications now use a cleaner compact layout, and success and info messages disappear after two seconds.
2026-09-23 11:51:17 +08:00
mesalogo f7541203e4 fix: restore cached reply duration when switching conversations
Keep execution statistics by conversation and project so returning to a viewed conversation immediately restores its reply duration while refreshing in the background. Preserve late-result isolation and clear failed scopes.

Validation: 15 focused tests, typecheck and lint passed. Full suite: 4878 passed, 9 navigation settings assertion failures and 1 Agent package timeout outside this change.

Release note: Switching back to a viewed conversation now immediately displays its cached cumulative Agent reply duration while refreshing the statistics.
2026-09-23 01:00:18 +08:00
jojokeeandGitHub 24bde0c2e3 Update README.zh-CN.md 2026-09-22 09:51:43 +08:00
jojokeeandGitHub 4366bdc716 Update README.md
更新社群码
2026-09-22 09:50:42 +08:00
mesalogo 598c68f955 release: prepare Desktop 0.13.11 2026-09-22 09:13:56 +08:00
mesalogo e6a25b779c ci: use OSS transfer acceleration for uploads 2026-09-22 09:09:05 +08:00
mesalogo a0b35d1192 release: prepare Desktop 0.13.10 2026-09-21 22:53:44 +08:00
mesalogo 6c4c054180 fix: improve document parsing settings spacing 2026-09-21 16:11:51 +08:00
mesalogo 38d0923d79 fix: stop repeated history migration after restart
Desktop 0.13.8 still treated SQLite freelist pages left by normal chat and note writes as evidence of pending migration when a file-backed note existed. Migration detection now checks only the schema version and legacy note payloads, while the startup worker reuses its confirmed pending decision during retries so actual migrations can still finish compaction.

Release note: 修复已有笔记用户正常聊天后重启仍重复显示历史迁移的问题;已完成迁移的数据继续按新格式保存,普通写入产生的空闲页不再触发迁移。
2026-09-21 12:31:14 +08:00
mesalogo 8dce1d7007 feat: improve model connections and settings workflow
Model settings can now duplicate saved connections with encrypted API-key copying, while settings navigation stays usable across long forms and preserves the separate Platform Features tabs. The assistant storage startup worker now gates historical migration and compaction on actual pending legacy data, so ordinary chat free pages do not reopen the migration screen. Desktop and Agent release metadata is prepared for 0.13.8 and 0.13.2.

Release note: 改进模型连接复制、长表单设置导航和历史数据迁移判断;普通聊天退出并重开后不再重复显示迁移页面。
2026-09-21 09:19:30 +08:00
mesalogo 6739e8e902 fix: simplify task center status selection and rendering 2026-09-20 22:03:27 +08:00
mesalogo fe093bc15f ci: allow full desktop validation to finish
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-09-20 14:15:27 +08:00
mesalogo 3cd6f44d7d ci: rehydrate Electron sandbox before validation
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-09-20 12:20:01 +08:00
mesalogo 9e8c98290e release: prepare Desktop 0.13.7 and Agent 0.13.1
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
Prepare the approved bilingual Desktop 0.13.7 and Agent 0.13.1 release
notes, version locks, feature matrices, and Agent release notes. Package both
OpenCode and Continue in the compound Agent release and publish native
execution checklists for remote projects.

Fix Linux source validation by configuring Electron's chrome-sandbox helper
and running the full test suite under Xvfb without disabling sandboxing.

Release note: Desktop 0.13.7 adds paged canvas notes, HTTP OCR, managed app
navigation, remote Continue and execution checklists, while preserving
conversation history and improving remote/runtime recovery. Agent 0.13.1
ships OpenCode and Continue together with checklist delivery.

Validation: npm test passed; npm run typecheck passed; npm run lint passed;
npm run release:notes:verify passed; 22 release workflow tests passed. No
local production build or package was run. LoongArch preview is not included.
2026-09-20 11:54:58 +08:00
mesalogo 9836a4cc9d fix: keep question order and streamline contextual help
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
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
Keep interactive questions and answered cards at their original position in
conversation history, including remote reception, recovery, and reopening.
Preserve legacy history placement without a database migration.

Move supplementary guidance into shared accessible contextual-help popovers
across pages and settings. Add keyboard, modal, accessibility, and Electron
regressions, plus bounded opt-in live canvas-analysis validation.

Remove unused Agent, Runtime, and in-memory graph-search helpers, obsolete
mocks and browser copy; reuse the existing package-size formatter. Keep
production execution paths, protocol schemas, and released data migrations.

Validation: targeted cleanup tests, typecheck, lint, shared Linux Host
attach/read/re-attach, and added help/Electron checks passed. Final full-suite
verification passed 4,858 tests with 67 skipped and zero failures (412 files
passed, 9 skipped). Focused help checks passed 54 tests, including Electron
style comparisons and Chromium accessibility descriptions. Details and
validation boundaries are recorded in feature docs.

Release note: 修复交互式问答在回答后排到后续输出末尾的问题,重新打开会话仍保留提问位置。页面和设置中的补充说明改为标题或字段旁的按需帮助,支持鼠标与键盘操作。
2026-09-20 01:26:36 +08:00
mesalogo bf1d34e897 feat: configure canvas pages sent for note analysis
Canvas analysis previously sent every page. Add a Magic Notes setting for
1-8 canvas pages, defaulting to 1 for new and historical settings. Select
the first N pages in current canvas order before capture and use the same
text/image range for saved entries, drafts and canvas-source todos.

Keep full 50-page saving/export, expose the same setting through config
tools, and use the shared settings select with an inline help icon.
Include pagination, persistence, production IPC and isolated Electron
regressions, including text-only input and scaled page boundaries.

Validation: focused feature tests and isolated Electron regressions passed;
the final compact UI passed 14 tests. Typecheck and lint were reported
passing by the coordinating agent. No full-suite pass is claimed because
the completed full test command exposed no summary. No live model calls
were made for this commit preparation.

Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
2026-09-20 00:07:07 +08:00
mesalogo ecf09f4e95 docs: consolidate product PR workflow and draft update article
Keep feature inventory, user-focused prioritization, draft confirmation,
article templates, screenshot guidance and publication checks in one guide.
Use separate new-feature, improvement and bug-fix sections, with explicit
image, replacement and verification markers in drafts.

Archive the unreleased update article and its evidence review, retaining
pending screenshots and release details. Remove the split guide files and
update release-runbook references.

Validation: documentation structure, draft markers, 59 local links and
anchors, and staged whitespace checks passed. No product code changed;
product tests and builds were not run.
2026-09-19 23:45:28 +08:00
mesalogo a3b7a27045 fix: preserve drafts, canvas edits, and remote Continue tools
Keep paragraph formatting and checklist todos when removing canvas pages,
and preserve accepted undo/redo history after oversized input is rejected.
Merge restored queued text into the latest conversation draft and retain
image delivery when OCR returns image references and generated warnings only.

Deliver session MCP servers to bridged remote Continue Execute while keeping
Ask read-only and preserving local model-profile isolation. Add regression
coverage and update feature documentation. Correct stale conversation-limit
and forced-color assertions and the measured Electron layout test deadline.

Validation: 4,826 tests passed, 67 skipped; full typecheck and lint passed.
Current-source Linux x64 Execute/Ask/Execute MCP delivery passed, using 10
real text-provider requests and no image-provider requests. Image generation
itself was not exercised. Staged whitespace checks passed before commit.

Release note: 修复恢复队列时覆盖新输入、画布删页丢失清单格式及超限输入清空撤销历史的问题;图片 OCR 无正文时保留原图发送方式,远程 Continue Execute 正确保留会话 MCP 工具。
2026-09-19 23:09:48 +08:00
mesalogo 375b2159c8 fix: remove conversation list and message history limits
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
List and search all active conversations instead of only the latest 100. Preserve message history across consecutive sends instead of truncating it to 500 messages. Existing detail loading and context compression remain unchanged.

Validation: 7 focused tests, typecheck and lint passed. Full test run timed out after reporting failures in App completion visibility and WorkspacePrimitives forced-color contracts.

Release note: 会话列表和搜索不再限制为最近 100 个会话;发送消息不再截断超过 500 条的历史记录。
2026-09-19 21:34:42 +08:00
mesalogo b5d8204986 fix: remove graph extraction budget and unify image download buttons
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
Graph extraction could exhaust its fixed 8192-token budget before returning
structured content. OpenAI requests now omit output-token limits, while
Anthropic uses the model connection limit required by its protocol.
Provider limits, timeout, cancellation and bounded response handling remain.

Use the shared accessible icon button for image downloads in chat and the
image viewer. Add graph request regression coverage and document the limits.

Release note: 知识图谱抽取不再额外限制为 8192 tokens;OpenAI 由模型服务决定输出上限,Anthropic 沿用模型连接配置。聊天和图片预览中的下载按钮统一为带提示的图标按钮。
2026-09-19 21:02:43 +08:00
mesalogo c98eb69cbd feat: refine notes workspace and add canvas zoom
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
Simplify sidebar selection and note record framing, compact the empty composer, and emphasize note titles. Replace redundant AI comment headings with an accessible panel toggle.

Add view-only canvas zoom and fit width, correct PDF import/export icons, and allow image files up to 20 MiB. Include regression coverage and updated design documentation.

Release note: Notes have clearer navigation and a more compact writing area. Canvases support zoom and fit width, and image imports accept files up to 20 MiB.
2026-09-19 20:56:03 +08:00
mesalogo df89d96803 fix: remove redundant topbar runtime status
Keep model selection and switching feedback in the composer. Report automatic runtime status failures through notifications and move the send button accessible description into the composer.

Release note: Remove the duplicate top-right model/runtime badge while retaining switching feedback and error notifications.
2026-09-19 20:24:00 +08:00
mesalogo ec08109a09 fix: keep menus and notifications usable above modals
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
Parsing test menus and application notifications could appear behind modal overlays or become unreachable while the background was inert. Share a stable top-layer portal that belongs to the owning or active modal, preserving notification timers, keyboard navigation and focus restoration.

Add unit and Electron regression coverage for settings, nested parsing results, application center, inference monitoring and native dialogs, with matching UI guidance and validation evidence.

Validation recorded in the preceding implementation: 4,793 tests passed and 67 skipped; typecheck, lint, isolated production build and Electron UI checks passed. Staged diff whitespace checks passed before this commit.

Release note: Menus and notifications remain visible and usable while settings or other dialogs are open, including keyboard navigation and notification dismissal.
2026-09-19 18:11:39 +08:00
mesalogo 6f06fce039 docs: record attachment UI behavior and OCR validation
Document the compact import cancellation control and progress-card layout. Record the prior full-suite results, focused attachment and Electron checks, and production probe review with their validation boundaries.

Release note: 附件查看结果、更多操作、移除和导入取消采用紧凑图标按钮,并提供悬停提示和键盘焦点说明,减少窄窗口中的布局拥挤。
2026-09-19 17:26:46 +08:00
mesalogo b1eb5d00d5 test: update app regression coverage 2026-09-19 16:53:43 +08:00
mesalogo a9b59ceb1c chore: include document probe and attachment UI refinements 2026-09-19 16:53:21 +08:00
mesalogo 935a8f56bf feat: add persistent document handling and protect note drafts
Add HTTP PaddleOCR-VL settings and shared document previews for chat and knowledge imports. Preserve attachment originals and parsed assets across drafts, messages and queues; support explicit image OCR, selecting document images into a conversation draft, and restoring queued input for editing.

Protect unsaved note bodies and titles during navigation and asynchronous updates. Match canvas analysis to source revisions, invalidate affected comments, preserve annotations during pagination, and provide a resizable record index with saved width preferences.

Existing OCR settings retain local behavior. Historical attachments without saved originals are not reconstructed. Advanced OCR options depend on the configured service; image sending depends on the effective model and Runtime.

Validation: full typecheck and lint passed; 477 focused tests passed across 12 files; staged whitespace checks passed. The full repository suite was not rerun for this commit.

Release note: Keep note drafts safe while switching workspaces, resize the record index, and revisit saved document attachments with OCR previews and selected images in chat.
2026-09-19 16:51:10 +08:00
mesalogo 17186b0c11 feat: add file-backed canvas notes
Magic Notes previously supported rich-text records without a paged drawing or
PDF annotation workflow. Add the integrated PeopleLib Fabric and Quill editor
with flowing text, pen/highlighter, object transforms, images, paper templates,
PDF backgrounds with native text extraction, and raster PDF export. Saves are
manual; undo/redo remains scoped to an annotation page or Quill mode.

Analyze drafts, saved canvases and canvas-source todos with page images and
text when the default model supports images, or an explicitly labelled
text-only fallback. Keep successful saves when automatic analysis fails and
prevent MCP plain-text replacement of canvas records.

Move entry bodies and binary assets into local note files with schema 38
migration and verified readback. SQLite retains metadata, indexes, revisions,
todos and comments. Backups must coordinate SQLite and the adjacent notes
directory while writes are paused or the app is closed; no one-click backup
action is included.

Show a continuous record stream with independent collapsible record index and
AI pane, real first-page thumbnails, and scroll-only record navigation. Open a
blank text/canvas composer by default and reset it after creation. Protect
unsaved drafts and establish normalized editor baselines after saving so clean
records do not prompt again when leaving.

Validation: latest recorded focused run passed 149 tests across five files;
typecheck, lint and git diff --check passed. The prior user-reported full run
passed 4,713 tests before later corrections and UI changes; it is not a full
validation of this final snapshot. Tests were not rerun for this commit, as
requested; independent systematic review follows the commit.

Release note: Create paged notes with handwriting, flowing text and PDF
annotations, browse records through thumbnails, and request AI comments with
an explicit text-only fallback. Existing notes migrate to local file storage;
backups now need both the database and notes directory.
2026-09-19 11:54:23 +08:00
mesalogo c7a93803c9 feat: show runtime checklists and support remote Continue
Display native OpenCode and Continue checklists alongside scheduled tasks, preserving request ownership, explicit clears and saved progress. Bundle both remote runtimes and support managed Continue installation, model routing, cancellation and restart continuation.

Validation: real local and Linux remote model checks, full Electron remote Continue flows, typecheck and lint passed. Full suite: 4707 passed, 67 skipped, one OpenCode compaction failure passed targeted rerun.

Release note: Display native execution checklists above conversations and support managed remote Continue projects with saved progress and cancellation recovery.
2026-09-19 10:47:27 +08:00
mesalogo 164e89253a fix: reduce repeated rendering during subagent progress
Reuse unchanged task and activity state, preserve activity nodes across reordering, and isolate historical tool rendering. Add regression coverage for progress, terminal states, copying, and component reuse.

File-level snapshot also includes existing runtime checklist and remote Continue UI changes in the selected files. Their remaining dependencies are still uncommitted in the worktree; this commit is not a standalone validated build.

Validation on the combined worktree: typecheck and lint passed; full tests had one packaging timeout that passed separately. Real-model UI runs verified parallel child tasks, progress, details, and persistence with existing internal-network certificate trust unchanged.

Release note: Reduce unnecessary UI updates while subagents stream progress and preserve activity list state when records reorder.
2026-09-19 01:39:17 +08:00
mesalogo a8d1dbc11f fix: give notes and todos dedicated workspace layouts
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
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
Replace the crowded note sidebar with a card overview and full note detail. Keep tasks in a stable split view with source content and AI comments, compact header navigation, and search-adjacent status filters. Preserve drafts and return context.

Validation: 93 focused tests, typecheck, lint, and isolated Electron renderer checks passed. Full suite was interrupted; renderer probes used in-memory APIs.

Release note: Notes now open from a card overview, while todos keep a stable task list beside their details. Compact view and filter controls leave more room for reading and editing.
2026-09-18 21:22:58 +08:00
mesalogo 5aad0cf5f7 fix: use a compact single-column application center 2026-09-18 16:31:13 +08:00
mesalogo 9bd0dae865 fix: prevent knowledge search icons from overlapping text
Preserve icon spacing in graph and document search fields against shared input styles. Update scheduled approval tests to expand the collapsed project task list before interacting with task approvals.

Release note: Knowledge search fields now keep the search icon clear of placeholder and entered text.
2026-09-18 16:03:10 +08:00
mesalogo daa14d0b7e fix: keep project tasks compact with visible running counts
Collapse the project task list by default while retaining create and filter actions. Move the project name into the conversation statistics card and keep the running task count visible independently of list filters.

Validation: 60 sidebar tests, typecheck, and lint passed. The earlier full test run encountered an offline dependency package failure and timed out.

Release note: 项目任务默认收起,标题旁显示运行中任务数量;项目名称整合到当前会话统计卡片中。
2026-09-18 15:48:26 +08:00
mesalogo ca3c919a34 feat: unify app ordering and simplify app settings access 2026-09-18 15:29:00 +08:00
mesalogo bbae893c45 feat: add app management, local inference controls, and task statistics 2026-09-18 08:20:45 +08:00
mesalogo 6dd6297414 fix: prevent subagent progress content from being clipped
Constrain nested subagent grids so long tool summaries cannot widen progress and final output beyond the card. Preserve text wrapping and local scrolling for code and tables, and extend the layout regression test.

Release note: Subagent progress and results now stay within the card width, keeping long text and tool status controls readable.
2026-09-17 13:06:28 +08:00
mesalogo 04d9ea9eba fix: use monochrome brand icons for messaging channels
Show distinct WeChat, WeCom, and DingTalk marks in channel settings and the project dropdown. Bundle the SVGs locally and inherit the interface foreground color.

Release note: 远程通道设置和项目下拉列表统一使用微信、企业微信、钉钉各自的黑白品牌图标。
2026-09-17 13:01:39 +08:00
mesalogo 94852acf82 fix: keep consecutive requests running and clear finished stop state
Wait for OpenCode session cancellation before reusing a conversation, and replace the session after unconfirmed cancellation. Preserve terminal message state when stale streaming snapshots arrive so finished conversations return to Send.

Validated with full production Electron UI and the configured real text model: queued requests complete, cancellation followed by immediate resend completes, and Stop clears. Focused tests: 362 passed; typecheck and targeted lint passed. Live dispatch counting limitations are recorded in the feature evidence.

Release note: Consecutive OpenCode requests no longer inherit a previous cancellation, and finished conversations no longer retain the Stop button after stale updates.
2026-09-17 10:11:31 +08:00
mesalogo 3639566e1b fix: align magic note actions with conversation list menus
Move pin and whole-note deletion into hover and focus menus on note rows. Preserve editor drafts when operating on notes and reuse destructive confirmation with keyboard navigation.

Commit the eight affected files in full, including existing application-settings integration and UI design edits in those files. Validation was performed against the shared worktree; unrelated changes remain uncommitted.

Release note: 魔法笔记的置顶和删除移至列表项的更多菜单,与会话列表保持一致,并保留未保存的编辑内容。
2026-09-17 00:15:23 +08:00
mesalogo 1698d9fd3e fix: keep valid older agent packages usable after desktop updates
Accept package-declared OpenCode, Node and koffi versions instead of desktop release pins. Preserve signature, hash, protocol and package-internal version consistency checks.

Validate both cached 0.11.18 packages and Linux x64 installation, bootstrap, health and stop. Related regression suite: 326 passed, 1 skipped; unrelated full-workspace check failures are recorded in the remote Host design.

Release note: Desktop updates no longer reject intact older Agent packages solely because their bundled runtime versions differ.
2026-09-16 19:04:54 +08:00
mesalogo 441f14b5f7 docs: organize PR articles by date and version tag 2026-09-16 16:35:44 +08:00
mesalogo cc9ea7c718 fix: prevent send queue flicker when the agent is idle
Claim idle conversation inputs before publishing queue changes so immediately dispatched messages do not briefly appear as pending. Preserve busy conversation queueing and add a regression assertion for notification ordering.

Validation: 369 focused tests, typecheck, and changed-file lint passed. Full-suite runs timed out with failures in other modules; repository lint reports errors in the existing story-graph demo.

Release note: Fix a brief pending-queue flash when sending a message while the agent is idle.
2026-09-16 16:35:13 +08:00
mesalogo 7e11d1099a docs: add product PR workflow and article templates 2026-09-16 15:17:16 +08:00
mesalogo 0a8439a855 fix: allow larger note searches and recoverable MCP validation errors
Raise note_search limit to 100 while preserving the default and output budget. Return scoped-tool argument validation failures as tool errors so models can correct and retry instead of receiving MCP internal errors.

Validation: 64 focused tests, typecheck and changed-file lint passed. Full tests timed out with two heartbeat migration failures; full lint reports unrelated story-graph demo errors.

Release note: 笔记搜索单次最多支持 100 条结果;内置 MCP 参数错误会返回明确提示,便于模型纠正后重试。
2026-09-16 15:02:11 +08:00
mesalogo da23810205 fix: show task approvals in their task cards
Task Center previously separated task approvals from their tasks and showed an empty approval section. Associated approvals now appear with allow-once and deny actions inside task cards, and those tasks appear under Active. Only unassociated approvals retain a separate section; no approval section or badge is shown when there are no pending requests.

Release note: Review and handle pending approvals directly in their Task Center cards. Tasks awaiting approval appear under Active, while requests without a matching task remain separately accessible.
2026-09-16 14:31:28 +08:00
mesalogo cb245a6402 fix: simplify task center scopes and status filters
Task Center previously split unbound tasks into a global scope and tasks needing attention into a separate filter. Keep Current project and All projects, label unbound tasks, and restore legacy global scope preferences as all-projects without changing task data.

Keep Active, Paused, and Finished filters. Active includes idle, queued, running, waiting_approval, failed, and interrupted tasks; task details and project-scoped approvals remain available.

Release note: Task Center now offers Current project or All projects and groups tasks needing attention under Active, while retaining project labels and approval actions.
2026-09-16 14:05:36 +08:00
mesalogo e84d7bd185 feat: retain completed project activity until viewed
Keep successful background conversations in project activity until their chat is visible and loaded. Track completion in memory for the current client session; initial history, failures and cancellations do not create completion notices.

Include whole-file companion changes already present in App and shared UI: preserve the workspace beneath Settings, show inline reply-copy feedback, and keep project settings inside the project menu. Include their component tests and documentation; navigation demo source changes remain unstaged.

Validation: 77 focused activity tests, typecheck and lint passed; Electron component fixtures passed 112 checks. Full suite: 4365 passed, 3 failed, 67 skipped (offline inventory timeout and two heartbeat migration failures). Commit-time staged whitespace check passed; tests were not rerun.

Release note: 后台会话完成后保留已完成提示,进入对应聊天后自动清除,避免错过任务结果。
2026-09-16 12:51:58 +08:00
mesalogo 6c045bfe2e fix: preserve remote task success after recoverable tool errors
Align remote ACP tool recovery with local runtime behavior when the model responds after a tool failure and completes normally. Preserve original errors and derive recovery state from committed remote events so reconnecting safely replays partial terminal events.

Validation: 461 focused tests, typecheck, lint, and shared Linux Host recovery scenarios passed. Full-suite validation has two pre-existing heartbeat migration fixture failures involving the pinned column.

Release note: 修复远端 Agent 在读取等工具失败后已正常回复,任务仍被误判为失败的问题,并修复断线重连时的工具状态恢复。
2026-09-16 12:49:17 +08:00
mesalogo eaf71ce16f fix: refine workbar tab overflow controls
Keep the add button after the last tab in the scrollable strip. Show transparent floating scroll arrows on hover without reducing tab space, preserve keyboard and touch access, and reveal activated tabs.

Update tab interaction documentation and regression coverage. Exclude documentation demos from ESLint.

Validation: 66 focused tests, typecheck and lint passed. Full tests reported Agent package and runtime reuse failures before timeout; desktop visual verification remains pending.

Release note: Workbar tabs now keep the add button beside the last tab and reveal floating scroll arrows when hovering over an overflowing tab bar.
2026-09-16 00:07:39 +08:00
mesalogo f1d7197480 fix: open browser popup links in managed tabs
Links requesting a new window were silently blocked. Open them in managed browser tabs with the existing session restrictions, clean up script-closed tabs, and follow click-created tabs in subsequent direct and MCP browser tools.

Validation: 232 targeted tests and Windows Electron E2E passed; typecheck and lint passed. The full suite retains six failures outside the popup path.

Release note: Browser links that open a new page now open in a managed tab, and AI browser actions follow the new page after a click.
2026-09-15 23:55:52 +08:00
mesalogo 625182c3dc feat: pin conversations and float their action menus
Persist conversation pins across restarts and show pinned conversations first, including older history. Anchor action menus above the list without shifting neighboring rows, with keyboard dismissal and sidebar-close cleanup.

Add schema 36 migration, focused regression coverage, and feature documentation. File-level staging also includes existing workbar styles and browser contract adjustments in shared files.

Validation: 145 backend tests and 237 App tests passed; typecheck and lint passed. Full test run timed out with Runtime and Agent package failures; native Electron visual validation remains pending.

Release note: 会话菜单新增置顶和取消置顶,重启后仍保留;操作菜单现在悬浮显示,不再挤动下方会话。
2026-09-15 23:39:06 +08:00
mesalogo b16b0d10e7 fix: update workbar tab titles when switching language
Derive application and default terminal titles from the current locale while preserving renamed and unmarked legacy terminal titles. Add locale-switch and layout-restoration regression coverage.

File-level scope also includes existing browser popup selection and close handling in the sidebar, its regression test, and workbar documentation updates.

Validation: 74 focused tests passed; typecheck and lint passed. Full test runs timed out with failures in settings navigation, Agent packaging, and database migration tests. Electron verification remains outstanding.

Release note: Workbar application and new default terminal titles now follow the interface language, including after restoring a layout; custom and legacy terminal names are preserved.
2026-09-15 23:21:51 +08:00
mesalogo a383b2d11f fix: reduce conversation history memory use
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
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
Load lightweight conversation summaries and hydrate full history only when
needed, preserving search, copy, export, queued requests, and remote questions.
Keep unchanged image reconciliation messages stable so list refreshes no
longer resend the entire saved history.

Prepare Desktop 0.13.6 with the approved bilingual notes and aligned feature
matrices. Agent remains 0.13.0; no LoongArch preview or navigation demo edits
are included. Development validation passed 4301 tests with 67 skipped;
the user requested CI validation instead of repeating local release checks.

Release note: 历史会话按需加载,减少切回应用时的重复保存和内存占用,同时保留完整历史、搜索与续聊。
2026-09-15 20:33:47 +08:00
mesalogo f47049c40d Merge github/main README updates into release candidate
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
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (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
Preserve the published English and Chinese README community QR-code updates. Desktop 0.13.5, Agent 0.13.0, approved bilingual release notes, and the validated product code remain unchanged.
2026-09-15 17:29:33 +08:00
mesalogo 4c18d8d9c6 chore: prepare Desktop 0.13.5 and Agent 0.13.0 releases
Record the approved bilingual release notes and align Desktop and Agent version metadata. Refresh both feature lists with image tools, PPTX OCR, Runtime process reuse, and the completed/to-implement status convention.

Agent 0.13.0 requires Desktop 0.13.5. Keep Node 24.19.0 and OpenCode 1.18.29 unchanged, preserve published release history, and exclude the LoongArch preview.

Validation: bilingual release notes verified; 4293 tests passed and 67 skipped; typecheck, lint, version consistency, and 63 documentation links passed. No new real model calls, Host reruns, local production build, or packaging. Full UI-driven image invocation and local/remote switching are not covered by the existing validation records.
2026-09-15 17:25:12 +08:00
mesalogo 8a63ac9ee8 fix: recognize PPTX images and release idle OCR memory
Image-only PPTX attachments failed with no indexable text even when a local
OCR model was configured. PPTX parsing now recognizes embedded PNG, JPEG
and WebP images through the existing local OCR path, preserves native text
and slide locators, and honors the selected parsing workflow.

OCR workers previously retained model sessions and WASM memory indefinitely
after successful parsing. Release the worker after 60 seconds with no active
or queued requests, reuse it for continuous work, and reload on demand.
Fast text/index modes still skip OCR. This does not render Office layouts,
recognize inherited master images, or change Agent execution permissions.

Validated four real PPTX attachments (40 pages), idle memory release and a
nine-page reimport after model reload on Windows x64 with PP-OCRv6 Tiny.
49 local OCR requests and zero text-model calls. All 4293 tests passed
with 67 skipped; typecheck, lint and the isolated Electron build passed.

Release note: 修复已配置本地 OCR 时图片型 PPTX 仍无法导入的问题,识别结果保留幻灯片定位。OCR 空闲 60 秒后自动释放模型运行内存,连续解析复用模型,下次使用时自动重新加载。
2026-09-15 16:58:28 +08:00
mesalogo 02e59427a1 docs: plan private app market and local model management
Record YAML application import/export, configurable market URLs, and future ShareServer distribution responsibilities. Define a local model management entry with managed-process and external-service boundaries; both remain planned capabilities.
2026-09-15 16:52:57 +08:00
mesalogo 34215c72a1 docs: define runtime checklist display scope
Specify read-only OpenCode and Continue checklists above conversations, including remote Agent delivery and validation requirements. Record native runtime test evidence and distinguish planned work from implemented behavior.
2026-09-15 15:59:13 +08:00
mesalogo f8d07419d2 feat: generate and edit conversation images across runtimes
Add model-controlled image invocation and read-only runtime assignments. Route image tools through Main so accepted operations retain their originating conversation when chat runtimes change, with saved references, cancellation, and manual recovery.

Keep Node hashing outside browser-shared contracts to restore production builds. Validation includes the production build, typecheck, lint, focused regressions, real Host transport checks, and two successful real image requests. Full UI-driven runtime switching acceptance remains documented as pending.

Release note: 开启图片模型的会话调用后,聊天助手可调用图片生成与编辑工具,Runtime 分配自动管理。图片成果保存在原会话,可继续修改,并独立显示生成状态和取消入口。
2026-09-15 15:51:52 +08:00
lofyerandGitHub d898f420fd Merge pull request #4 from mesalogo/jojocoding-patch-3
Update README.md
2026-09-15 08:20:55 +08:00
lofyerandGitHub c4492e4a92 Merge pull request #5 from mesalogo/jojocoding-patch-4
Update README.zh-CN.md
2026-09-15 08:20:34 +08:00
mesalogo e5aa9ed688 fix: reuse runtime processes across project sessions
Multiple projects could spawn duplicate heavyweight runtimes, while settings
changes and remote reconnection could cancel or strand native conversations.
Share compatible local OpenCode and DeepSeek Harness process owners and
Agent-owned ACP connections, retaining workspace routing, per-session modes,
model attribution, question ownership, targeted cancellation and idle cleanup.

Restore remote pending questions and terminal task projections across reconnect
and desktop restart, and preserve accepted ordinary requests during settings
replacement while cancelling expert-team parents with their replaced scheduler.
Extend regression coverage and Agent type checks. Record measured resource and
real-model validation results, including failures and limitations.

Also include the conversation media-generation design drafts (not an implemented
feature), documentation entry links and dedicated Linux test-directory guidance.
Desktop and Agent changes must be delivered together for shared process results.

Validation: typecheck and lint passed; staged content and documentation links
checked. Prior full regression and live-model evidence are recorded in progress
notes; no new full test suite or model calls were run for this commit.

Release note: 多项目会话现在复用兼容的 OpenCode 和 DeepSeek Harness 进程,减少重复启动并保持工作区、工具和取消隔离。修复远端问答在断线、重启及配置更新后的恢复与状态同步问题,Desktop 与 Agent 需配套更新。
2026-09-15 08:14:42 +08:00
mesalogo b755951ecd fix: offer recovery for blank desktop windows
React render failures now show a localized reload page, and renderer crashes offer native reload confirmation. Record renderer and load failures in existing diagnostics without automatic retry loops. Reload may lose unsaved input; the intermittent failure cause remains unconfirmed.

Release note: Recover from desktop rendering failures using a visible Reload action instead of being left without a recovery entry point.
2026-09-15 08:07:08 +08:00
mesalogo d4f1668c9b feat: paste local files into chat attachments
Copied local files previously required the attachment picker. The chat
composer now imports one or more supported files directly from paste,
sharing parsing progress, limits, and batch rollback with file selection.
Text and screenshot paste remain available, and failures preserve drafts.

Validation: prior 166 focused tests, typecheck, and lint passed; 28 targeted
tests passed again before commit. The prior full suite had 4214 passes,
67 skips, and 6 window-recovery failures; window tests passed 10/10 alone.
Native clipboard behavior was verified on Windows; macOS/Linux remain unverified.

Release note: 现在可在文件管理器中复制一个或多个支持的本地文件,直接粘贴到聊天输入框添加附件;可查看读取和解析进度,失败时保留草稿与已有附件。
2026-09-15 08:05:31 +08:00
mesalogo e1fe1cf10a fix: refresh magic notes after external updates
Subscribe the open Magic Notes workspace to committed database changes from desktop and Agent/MCP writes. Preserve selections and unsaved drafts during refresh and external deletion.

File-level commit includes existing runtime edits in shared files; their remaining dependencies are still in the working tree.

Release note: Magic Notes now updates automatically after Agent/MCP edits while preserving your selection and unsaved drafts.
2026-09-15 00:31:21 +08:00
mesalogo fbba2e3abf fix: place workbar toggle beside theme switch
Move the assistant workbar toggle from the floating content corner to the topbar immediately before the theme switch. Preserve toggle behavior and update layout tests and documentation.

Release note: The workbar toggle now stays beside the light/dark theme switch instead of floating over the content area.
2026-09-14 23:37:37 +08:00
mesalogo 511f50aa63 fix: keep remote recovery actions inside the project menu
Remove the duplicate recovery button beside New project and its extra layout column. Keep the collapsed failure status and the retry action on the affected project inside the dropdown.

Stage complete files as requested, including existing App snapshot regression coverage and remote runtime design updates in those files.

Validation: 29 ProjectSwitcher tests and the targeted App recovery test passed; typecheck and lint passed. Full test run timed out after 60 seconds.

Release note: Remote recovery retries now stay in the project dropdown, keeping the project header stable when a connection fails.
2026-09-14 22:16:13 +08:00
mesalogo 6c1e8105dd fix: keep project activity submenu height independent
Opening long conversation lists stretched and repositioned the project menu. Give the conversation submenu its own surface and positioning, aligned to the selected project row with independent viewport clamping and scrolling. Use compact navigation when neither side has room.

Validation: 13 component tests, typecheck and lint passed; 108 Electron fixture checks passed across 14 light/dark scenarios. Full test run timed out after 60 seconds.

Release note: Project activity menus now keep the project list stable when opening long conversation submenus.
2026-09-14 22:10:35 +08:00
jojokeeandGitHub 3b83c90d0f Update README.zh-CN.md
更新社群二维码
2026-09-14 16:43:20 +08:00
jojokeeandGitHub 327b06587b Update README.md
更新社群二维码
2026-09-14 16:42:30 +08:00
mesalogo de14a6157a fix: hide project activity summary when idle 2026-09-14 09:06:36 +08:00
mesalogo 81ae9e0324 chore: prepare Desktop 0.13.2 and Agent 0.11.25
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
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 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
Prepare bilingual notes and aligned feature lists for the schema-35 history repair, retained tool summaries, Windows DeepSeek workspace paths, and compact project activity navigation.

Agent 0.11.25 is a maintenance release with unchanged protocol and runtime pins. Its compound packages require Desktop 0.13.2. Desktop targets the standard six platforms without a LoongArch preview.

Make activity interaction tests wait for the loaded conversation and enabled send button, reset response mocks between cases, and wait for completed answer rendering. Accept bilingual maintenance-only Agent notes without inventing features or fixes.

Validation: 4,155 tests passed, 66 skipped; typecheck, lint, and release notes verification passed. The affected 20 tests passed three consecutive runs; all 227 App and Agent release-note tests passed. No local production build or packaging was run.
2026-09-14 01:18:42 +08:00
mesalogo f49d6678a3 fix: refine project activity spacing and conversation rows
Add 8px below the activity summary to separate it from new conversation. Remove redundant conversation-row arrows while keeping project submenu arrows, and update the UI documentation.

Validation: 11 ProjectActivity tests and typecheck passed. Lint was interrupted before completion.

Release note: 调整项目活动摘要与新建会话的间距,移除会话列表末尾的多余箭头,保留项目子菜单指示。
2026-09-14 00:40:10 +08:00
mesalogo 0d7ba55a50 fix: preserve tool details and prevent repeated history growth
Show bounded operation summaries for Continue, DeepSeek Harness and direct-model tools, retaining input details across incremental updates. Use one canonical workspace path for DeepSeek Host startup and ACP sessions so equivalent Windows paths reuse a working Runtime.

Compare persisted subagent tool fields by value, release terminal task caches, and avoid rebuilding caches for duplicate remote events. Upgrade to schema 35 to recompact repeated schema-34 tool blocks while preserving events and provenance. Include regression coverage, validation records and temporary-resource test exclusions.

Validation: 4155 tests passed, 66 skipped; typecheck and lint passed. Full desktop real-model validation covered all three tool-summary paths and both Windows path spellings. Storage validation evidence includes local and remote tool workloads and real database upgrade checks.

Release note: Tool records now show paths, commands and search details across runtimes. Fix DeepSeek Windows workspace failures and repeated subagent history growth; schema 35 reclaims redundant history data while preserving execution records, and older clients cannot reopen the upgraded database.
2026-09-14 00:34:51 +08:00
mesalogo c71da4b6ae fix: browse project activity through cascading menus
Replace the production activity dialog with anchored project and conversation menus, preserving exact conversation navigation and adding keyboard and narrow-window coverage. Include the interactive demo and design documentation.

The demo hides idle activity and removes conversation arrows; production still retains those two elements. Validation: typecheck, lint and 11 ProjectActivity tests passed. The full suite timed out after 240 seconds with an agent-package offline inventory test failure.
2026-09-13 23:06:20 +08:00
mesalogo ba6f6b8f22 fix: show operation details in OpenCode tool records
Preserve OpenCode operation titles in main-session tool summaries, including permission waits, failures and interrupted calls. Keep title trimming and length limits aligned with subagent records.

Update tool display documentation and include the activity-menu wording in UI-DESIGN.md. Validation: 132 focused tests passed; main-process typecheck and changed-file lint passed. Full-suite checks were affected by concurrent activity-menu changes and a test timeout.

Release note: OpenCode main-session tool records now show file paths, commands or operation descriptions beside tool names, matching subagent records.
2026-09-13 22:55:12 +08:00
mesalogo 49377206cc fix: stop subagent history from exhausting disk space
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
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
Repeated full subagent progress snapshots could grow the desktop database to
many gigabytes during ordinary long-running tasks. Local and managed SSH
writes now retain lossless progress changes while preserving conversation
snapshots, event identities, remote replay deduplication and transaction rollback.

Upgrade existing history in bounded worker-thread batches, show progress and
retry controls, and reclaim freed disk space after conversion. Schema 34 cannot
be opened by older clients; a pre-upgrade backup is required for client rollback.
Document reusable database migration guidance and the verified large-database,
portable, local Runtime and Linux Host evidence.

Prepare Desktop 0.13.1 with the approved bilingual notes and aligned feature
lists. Agent remains 0.11.24. Final candidate validation and production builds
will run on main CI, as requested, without additional local release testing.

Release note: 修复长任务和子任务造成的数据库异常膨胀;升级时自动整理旧记录并回收空间,保留聊天、执行详情和结果,并支持退出后继续处理。
2026-09-13 20:50:33 +08:00
mesalogo 1c22e1a06d fix: keep browser overlays and knowledge controls usable
Native browser pages could cover application dialogs and overlapping menus
or notifications. Suspend only the viewport while covered and restore the
same active page afterward, including nested dialogs and terminal prompts.

Keep knowledge source controls at their full height, constrain long library
cards and titles, and update automatic names when switching remote targets
without overwriting edited or saved names. Restore packaged release-note
loading now that the history exceeds its previous size limit.

Add focused regressions and real Electron UI evidence. Clean up temporary
Harness fixture directories at test completion instead of accumulating them.

Release note: 修复内嵌浏览器遮挡弹窗和浮层、知识库来源选项被压缩及长名称显示异常;切换远端知识库时自动名称正确更新,并恢复更新说明显示。
2026-09-13 19:46:37 +08:00
mesalogo a0456f687a chore: prepare Desktop 0.13.0 and Agent 0.11.24 releases
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
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (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
Record the approved bilingual release notes against Desktop v0.12.12 and
Agent agent-v0.11.23, rather than listing intermediate development fixes.
Desktop adds external knowledge connections and cross-project activity;
remote native questions remain classified as fixes in both release notes.

Align both feature lists, compatibility guidance and validation evidence.
Upgrade Desktop to 0.13.0 before Agent 0.11.24. Keep OpenCode at 1.18.29,
retain existing published notes, and exclude the LoongArch preview.

Allow fixes-only Agent notes while checking bilingual section consistency.
Validation: 4089 tests passed with 66 skipped; release-note verification,
typecheck, lint and documentation link checks passed. Full remote-provider
acceptance and candidate CI/native release validation remain outstanding.
No release tags or publication are part of this commit.
2026-09-13 17:29:37 +08:00
mesalogo 59a1624396 fix: close OpenCode event streams before ending requests
Completed parallel conversations could leave buffered event connections open.
Chat and native context compaction now cancel their own subscriptions before
ending SDK iteration, without cancelling another conversation or its Server.

Strengthen cleanup-order regressions and verify real OpenCode parallel runs,
cancellation and compaction. The remote Agent uses ACP stdio and transcript
polling rather than this SDK event stream and needs no corresponding change.

Validation: 99 focused tests passed; the final candidate passed 4089 tests
with 66 skipped, typecheck and lint. No external model calls were made.
2026-09-13 17:28:40 +08:00
mesalogo a44de1441a docs: record recurring defect types and the mechanism covering each
The titlebar drag-region defect was fixed three times in a row, each time
only for the dialogs someone reported, and the same batch introduced two more
instances. Repeated full-table lookups and production reuse of a verification
helper showed up in several places at once for the same reason.

Add docs/quality/recurring-defects.md describing each type that has actually
recurred here, the shared mechanism or automated test that now covers it, and
the history that justifies the entry. Require reading it from AGENTS.md before
touching overlays, store lookups, retrieval entry points, cancellation, or
render-time derived values, and require extending the shared mechanism rather
than patching one reported case.

Correct the external knowledge technical design where it described the removed
configuration-snapshot checks, the shared retrieval entry point and the
unreachable probe status, and record the refactor and its validation in the
knowledge base progress log.
2026-09-13 16:02:15 +08:00
mesalogo f7f0332143 chore: drop unreachable external knowledge states and helpers
Remove code with no production path: the probeStatus value 'healthy' that no
probe can produce, the external.transportConfirm string and the
remote-missing and config-invalid state strings that nothing renders,
KnowledgeService.searchHybridMany which lost its last caller when chat search
moved to retrieveMany, and the sourceUrl field that no provider parser
populates.

Keep sourceUrl in externalKnowledgeLocatorSchema because that schema parses
already-persisted citations. Point the affected fixtures at catalog-ready,
the status testInstance actually writes.

Document why ProjectActivity adjusts state during render: the guarded call is
the recommended pattern for resetting state on prop changes here, and the
repository lint rule rejects the useEffect alternative.
2026-09-13 16:02:01 +08:00
mesalogo 4222351f3c fix: keep every full-window overlay clickable over the titlebar
The drag-region fix applied to six dialogs left seven overlays uncovered,
including two added in the same batch, so their controls still stopped
responding to hover and clicks where they overlapped the native titlebar.
Three overlays used inset: 38px 0 0 to dodge the titlebar instead of
releasing the drag region, leaving an unmasked strip.

Replace the per-dialog declarations with one shared selector covering all
fifteen overlays and give each one inset: 0. Add a regression test that
discovers every fixed overlay from the stylesheet and fails when a new one
misses the shared rule, so this stops being fixed one dialog at a time.

Release note: 修复 MCP 编辑器、发布说明、远端目录选择、HTML 预览、设置、跨项目活动和外部
知识库实例等弹窗与标题栏重叠时,控件无法悬停和点击的问题。
2026-09-13 16:01:41 +08:00
mesalogo 8ad26d43bb fix: remove repeated full-table scans from knowledge and activity paths
Binding checks loaded and parsed every external knowledge binding to answer a
single-row question. requireLibrary did this on more than thirty call sites
including one inside a task reconciliation loop, and the knowledge snapshot
scanned the table twice per library across its seven callers.

Add keyed store lookups (getBinding, hasBinding, getBindingsForInstance) that
use the existing primary key, build the snapshot's bindings once into a Map,
and count bindings per instance in a single pass.

Memoize the conversation activity index in the renderer. It was rebuilt on
every render, including once per streaming token, which discarded half the
benefit of the memoized activity summary directly above it.

Release note: 知识库页面、对话预检索和跨项目活动列表在绑定较多时响应更快,流式回答期间的
界面开销降低。
2026-09-13 16:01:26 +08:00
mesalogo 74a2f187ba fix: stop external knowledge chat retrieval from running binding checks
Conversation retrieval called the binding verification helper, so every
question sent the user query as a test query and RAGFlow bindings with graph
or knowledge compilation enabled spent an extra detail request before
retrieving. A failed capability check could also surface
EXTERNAL_KB_CAPABILITY_UNAVAILABLE during a normal conversation.

Retrieval and verification are now separate service entry points sharing one
snippet-bounding helper. The capability pre-check stays in the verification
path and keeps using the binding's configured request timeout.

Replace the five configuration snapshot comparisons and the
EXTERNAL_KB_CONFIG_CHANGED code with the existing per-instance cancellation.
A connection test cancelled by a configuration change now rejects without
writing back a probe result, so disabling an instance no longer records it as
a failed probe. Duplicate binding safety relies on a re-check plus the
existing unique index.

Release note: 外部知识库对话检索不再执行仅用于保存校验的能力预检,RAGFlow 图谱绑定每次
提问少一次请求;停用实例时不再把它错误标记为连接失败。
2026-09-13 16:01:12 +08:00
mesalogo ca4a58c762 fix: keep full-window dialogs responsive to mouse input
Render feedback, parsing diagnostics, knowledge confirmations, WeChat binding and image preview overlays through body portals so native titlebar drag regions cannot intercept their controls. Preserve themed form styles and modal focus isolation.

Validation: 320 targeted tests, typecheck and lint passed. Native Windows hover and close checks passed for feedback and parsing diagnostics in light and dark themes at two sizes. Full suite timed out without a summary.

Release note: 修复反馈等弹窗与窗口标题栏重叠时,关闭按钮无法正常悬停和点击的问题。
2026-09-13 14:19:46 +08:00
mesalogo ea45533137 fix: allow follow-up messages after cancelling remote OpenCode questions
Cancelling a managed SSH prompt while answering a question could leave the conversation bound to its previous operation and reject the next message. Record the Agent cancellation outcome and reconcile and close the desktop binding before the next prompt.

Add two cancellation regressions and document real SSH/Electron question validation across eight theme and viewport combinations. Local queues and remote question routing are already included in 3b560c9.

Validation: prior focused checks passed 275 tests with 5 skipped; typecheck and lint passed. The final full run passed 4087 tests with 66 skipped and one packaging timeout; that packaging file separately passed 22 tests with 1 skipped. Staged diff whitespace check passed.

Release note: After cancelling a pending OpenCode question on a managed SSH task, you can send another message in the same conversation without an operation identity error.
2026-09-13 13:57:57 +08:00
mesalogo e9679fecf1 fix: keep project dialog close controls clickable
Render project creation and settings dialogs in a body portal so the Electron title-bar drag region does not intercept their close controls. Cover dialog placement, scrolling, and focus restoration, and document the layout rule.

Release note: Project creation and settings dialogs keep their close buttons clickable after scrolling and restore focus to their trigger when closed.
2026-09-13 13:31:42 +08:00
mesalogo 3b560c9a5e feat: add external knowledge retrieval and cross-project activity
Connect Dify, FastGPT and RAGFlow through read-only bindings, encrypted local credentials, shared retrieval and persistent citations. Configuration and removal affect GoodBuddy only.

Show active conversations across projects, retain older live work, and improve project dialog scrolling. Queue concurrent questions and route managed SSH OpenCode questions through the Agent. Preserve recoverable read/search errors and partial search results.

Document the Electron UI automation standard, real model and external knowledge validation, and remaining acceptance limits.

Validation: earlier full regression passed 4081 tests with 66 skipped; subsequent focused remote-question checks and real Linux evidence are recorded in feature progress. Precommit typecheck and lint passed; latest project dialog and shared UI suites passed 58 tests. The full suite was not rerun for the final dialog changes.

Release note: Use external knowledge in conversations, find active work across projects, and answer concurrent local or managed SSH OpenCode questions. Managed SSH questions require the updated Agent adapter.
2026-09-13 13:10:01 +08:00
mesalogo b84250f62a fix: allow closing browser tabs during active requests
Allow users to close leased browser tabs without cancelling the AI request or revoking its other tools. Closed-page operations report a recoverable error; explicit navigation creates a dedicated replacement tab for direct-model and MCP calls.

Add regression coverage and real Electron lifecycle checks, and update browser behavior documentation.

Release note: 浏览器标签页被 AI 请求占用时也可关闭,其他任务继续运行;再次导航会打开独立新页。
2026-09-13 11:21:37 +08:00
mesalogo 032ebc313f fix: simplify conversation composer options
Consolidate composer settings into a compact popover while preserving runtime, mode, sending, and queue controls. Place the settings icon after voice input, limit the panel to 300px, and show knowledge expansion state.

Include the navigation demo, design guidance, localized labels, and composer regression coverage.

Release note: Simplify the conversation input toolbar with compact settings, clearer knowledge expansion, and visible active selections.
2026-09-13 00:05:05 +08:00
mesalogo 719c9fd6cd fix: add inline clear action to conversation search
Show a clear button when the conversation search contains text. Clearing restores the scoped conversation list and returns focus to the input, with regression coverage for visibility and filtering.

Release note: Conversation search now includes an X button to clear keywords and return to the current project's conversations.
2026-09-12 22:10:25 +08:00
mesalogo d36a05f7ad fix: prevent blank browser reloads and clear stale errors
Require a committed page URL before enabling reload. Clear sidebar action errors when leaving or closing a panel, and ignore late failures from its previous scope.

Add regression coverage and document browser error lifecycle and validation results.

Release note: 修复空白浏览器页刷新时报错、关闭浏览器后错误提示仍残留的问题;切换或关闭面板后,旧操作的延迟失败不再重新显示。
2026-09-12 21:13:44 +08:00
mesalogo fc36433d76 fix: simplify tool execution records in conversations
Replace full-width tool cards with compact disclosure rows in conversations and subagent progress. Remove redundant runtime summaries, keep expansion under user control, and show successful calls with green checks.

Prioritize errors and results, fold inputs, and provide original-text copying through application notifications. Detail text wraps in bounded scroll areas with no extra display controls.

Validation: 3927 tests passed, 66 skipped; typecheck and lint passed.

Release note: Tool activity is now more compact, with clearer status icons and less repeated text. Expand a record to read and copy results, errors, or input parameters.
2026-09-11 14:54:11 +08:00
mesalogo f6287589a3 chore: prepare Desktop 0.12.12 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
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
Bump the desktop patch version and package matching Chinese and English notes
for image follow-up context, retained questions and answers, simpler Runtime
selection, and docked Git history with automatic project detection.

Review and align both feature matrices with the verified release range.
Agent stays at 0.11.23; no Agent release or LoongArch preview is included.
External knowledge probe tooling remains development-only, not a shipped
external knowledge-base feature.

Validation: 3911 tests passed, 66 skipped; typecheck, lint, and bilingual
release-note verification passed. Production builds use main-branch CI before
the immutable desktop tag; no local production package was built.
2026-09-11 01:21:50 +08:00
mesalogo b2c85b8ece fix: describe saved fixed project models accurately
Projects with a saved fixed Runtime model could incorrectly claim to follow
the global Runtime configuration. Restore the fixed-model description while
keeping the simplified menu and the ability to return to inherited settings.
Model resolution, permissions, and remote Agent behavior are unchanged.

Validation: 3911 tests passed, 66 skipped; typecheck, lint, and release-note
validation passed. Added bilingual regressions for all three Agent Runtimes.

Release note: 项目已固定 Runtime 模型时,现在明确显示所用固定模型,不再误报为跟随全局设置。
2026-09-11 01:21:24 +08:00
mesalogo 073eb0d2f5 fix: dock workspace history and detect Git on project switches
Keep commit history at the sidebar bottom, expand it to half the Git content area, and preserve independently scrolling panes when resizing. Use the shared refresh action and remove the redundant changed-files border.

Load Git status on project changes regardless of restored sidebar selection, and keep status and error fallback scoped to the selected project.

Release note: 提交历史默认贴底,展开占用一半内容区,支持拖拽调整并统一使用顶部刷新。切换项目后自动识别 Git 工作区,无需手动刷新。
2026-09-11 00:51:25 +08:00
mesalogo 3bf720a32c fix: retain submitted questions and answers in conversations
Preserve read-only question and answer reviews after successful submission or skipping, including multiple rounds and local conversation reloads. Keep failed submissions retryable and preserve newer questions and terminal states when responses arrive late.

Validation: typecheck and lint passed; full suite had 3900 passes, 66 skips and one Agent package timeout that passed on isolated rerun. Real OpenCode UI acceptance was not run. Previously discarded answers cannot be recovered.

Release note: 修复 OpenCode 提交回答后会话仅显示已提交的问题;现在保留原问题及对应答案,多轮问答和跳过记录在重新打开会话后仍可查看。
2026-09-10 23:47:44 +08:00
mesalogo 1274c762e8 fix: simplify runtime model choices in conversations and projects
Remove redundant provider and model combinations from conversation and project Runtime selectors. Runtime model configuration stays in system settings, while saved fixed project selections remain visible for compatibility.

Release note: Conversation and project Runtime menus are simpler; change Runtime models in system settings.
2026-09-10 23:00:01 +08:00
mesalogo c0ad27ab44 fix: preserve image context across follow-up requests
Image requests previously sent only the current prompt and attachments, so
follow-up edits could lose the previous image and conversation requirements.
Reuse the latest successful image from the existing artifact store together
with text history, including after reopening the app. Explicit attachments
take priority, and configured model IDs remain unchanged.

When image editing is explicitly unsupported, continue with text-based image
generation and retain a quiet message footer explaining the missing image
context. Keep genuine provider errors and cancellation visible. Add focused
regressions, persisted footer metadata, and aligned feature documentation.

Validated with four real sub-gpt-image-2 image requests and a simulated
unsupported-edit response. Image regressions, typecheck and lint passed;
the full working-tree run had two unrelated concurrent Runtime-menu failures.

Release note: 连续改图会自动带上上一张图片和文字上下文,重开对话后仍可继续修改。上游明确不支持编辑时仍可按文字生图,仅在回复底部轻提示未使用参考图片。
2026-09-10 22:39:41 +08:00
mesalogo 4483c92b57 fix: distinguish external knowledge probe failures from empty results
Validate Dify and RAGFlow outcomes, normalize FastGPT URLs, and report response envelopes, score structures and actual rerank state. Align integration documentation with official APIs and archive 33 redacted read-only requests with strict HTTPS verification.

Validation: 43 focused tests, typecheck and lint passed. Full test runs exceeded 60s and 240s without a summary; this limitation is recorded in the feature progress document.
2026-09-10 22:19:30 +08:00
mesalogo d9114c20bf test: make workspace history fixtures independent of Git identity
Cross-platform packages / Validate source (push) Canceled after 0s
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 / 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
Write fixed commit objects from the fixture index rather than invoking
Git commit with an implicit machine identity. Preserve real history,
rename diff, pagination and dirty branch-switch checks without changing
Git author or committer configuration or skipping any affected test.

Add fixture checks for valid objects, parent linkage, unchanged dirty
content and unchanged repository configuration. Record the CI regression
introduced when the prior test identity override was removed.

Validation: workspace tests passed 11 with 2 filesystem skips; release
notes verification, typecheck and lint passed. Full tests passed 3845 with
66 skipped and one unrelated ActivityPanel timeout; all 16 ActivityPanel
tests passed on isolated rerun. Product source and approved bilingual
Desktop 0.12.11 / Agent 0.11.23 release notes are unchanged.
2026-09-10 19:49:33 +08:00
mesalogo 877cf581f6 chore: prepare Desktop 0.12.11 and Agent 0.11.23 releases
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
Record the approved bilingual release notes, classify long-task limits and
premature truncation as fixes, and align both feature matrices with the
verified workspace, browser, scheduling and Runtime behavior.

Advance the Agent lock to 0.11.23 for remote file and Git management, optional
model limits, larger bridge exchanges and the unlimited-duration connection
fix. Agent publication reads version-specific approved notes and remains
separate from Desktop Latest. Upgrade Desktop to 0.12.11 before updating Hosts.
No LoongArch preview is included.

Validation: release notes verification, all 3845 tests (66 skipped), typecheck
and lint passed. Current-source Linux x64 workspace and bridge checks passed;
managed ACP Ask and Execute completed two real model requests, both HTTP 200,
with delivered results and cleaned test resources. Native production packages
and publication remain subject to GitHub Actions after candidate approval.

Release note: 准备 Desktop 0.12.11 与 Agent 0.11.23 发布;远程文件和 Git 管理、长任务限制及连接超时修复需先升级桌面端,再更新 Host 上的 Agent。
2026-09-10 14:27:41 +08:00
mesalogo 0eb5205b1c fix: simplify workspace controls and branch search
Group the workspace view switch and refresh action in one header, show only
file or Git actions for the active view, and remove the isolated root icon.
Subdirectory breadcrumbs retain root navigation and restore keyboard focus.
Use the shared form style for branch search in a bounded, scrollable panel.

Confirm that fresh settings already default to automatic Execute tool
authorization, and add regression coverage without changing saved policies.
Update the workbar design and record isolated production Electron validation.

Validation: 354 focused tests, typecheck, lint and development bundle passed.
The full suite passed 3843 tests and skipped 66; an unchanged OpenCode stream
lifecycle timing test failed once and passed its isolated rerun.

Release note: 整理工作区文件与 Git 工具栏,移除孤立的根目录图标并保留子目录导航;分支搜索框、文字和按钮统一样式,宽窄工作栏下均保持紧凑布局。
2026-09-10 13:54:34 +08:00
mesalogo c037d7fb20 fix: complete runtime and workspace workflows
Restore Ask workspace reads and output paging without granting write access,
and let programming Subagents reuse their parent's browser page. Allocate
browser resources only when used, keep Agent pages aligned with workbar tabs,
recover released bindings, and remove unused automatic screenshot work.

Preserve inherited Runtime selections during remote usage persistence and
manual compression. Keep viable channel model fallbacks, display the actual
scheduled-task mode and Harness model source, and avoid rediscovery delays
when calling healthy MCP tools. Fix directory/history refresh, case-only
renames, rename-aware commit diffs, and FastGPT probe success reporting.

Add regression coverage and align feature documentation. The Agent-side
connection timeout fix requires deploying an updated Agent; existing Ask
read-only and Execute authorization boundaries remain unchanged.

Final validation: 3839 tests passed, 66 skipped; typecheck, lint, development
build, and diff checks passed. Isolated Electron UI/MCP workflows passed with
a loopback model fixture. Current-source Linux x64 Host checks passed with
three successful real model requests. Chromium loadURL rejection was not
reproduced in the UI; released-binding recovery has automated coverage.

Release note: 修复 Ask 读取项目文件、浏览器页面显示与恢复、文件管理及模型配置跟随中的不一致,并减少无用截图和 MCP 等待。定时任务显示实际工作模式,远程会话可正常保存继承 Runtime 的用量;远端连接超时修复需更新 Agent 后生效。
2026-09-10 12:48:30 +08:00
mesalogo 020a9ad0dc fix: retain conversation retrieval settings for scheduled messages
Persist the conversation knowledge retrieval mode across restarts so timed messages use the saved setting. Clarify shared conversation authorization in the scheduled-task documentation and correct the renderer test query type.

Validation: 374 focused tests passed; typecheck and lint passed. The full suite had two failures that passed on focused reruns. Real remote validation remains blocked by the temporary harness SDK module loading error; no model requests were made.

Release note: 修复会话知识检索模式在重启后丢失的问题,定时消息继续使用会话保存的检索设置,并同步任务授权说明。
2026-09-10 10:36:26 +08:00
mesalogo 444902d8ad fix: preserve conversation settings for models and scheduled messages
Preserve explicit Runtime model selections and Harness configuration rules without pinning resolved defaults. Expose fixed Runtime models in project settings and keep SSH conversations inheriting their project selection.

Dispatch scheduled instructions through ordinary conversation requests using current history, Runtime, work mode, and knowledge scope. Persist conversation work modes and update scheduling documentation and regression tests.

Validation checkpoint: typecheck, lint, and 104 focused tests passed. The SSH inheritance regression passed in isolation, but its process timed out on exit. Full-suite completion, remaining model documentation review, and real end-to-end validation are still pending.

Release note: Preserve fixed model choices and let scheduled messages use their conversation's current settings and history.
2026-09-10 10:17:45 +08:00
mesalogo fedafa21bd fix: pin workbar tab order and preserve browser titles 2026-09-10 08:46:17 +08:00
mesalogo c1848a9dc4 test: align runtime fixtures across platforms
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
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
Account for Electron-as-Node when probing the current executable and update the Linux workspace registry scenario to validate the writable handle behavior shipped with remote file management.
2026-09-10 07:01:48 +08:00
mesalogo 21ac8330ea fix: preserve runtime configuration references
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
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
Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves.

Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline.

Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
2026-09-10 06:27:48 +08:00
mesalogo b01a856bae fix: remove avoidable model runtime limits
Preserve long histories and parsed attachments, expose paged process and Subagent output, isolate MCP discovery failures, and expand browser snapshots and knowledge result selection. Remove fixed Harness output and updated remote Runtime duration limits.

Keep released Agent profile and v1 bridge compatibility while enabling larger framed exchanges and optional limits for updated peers. Document remote coverage requirements and real-model boundary validation.

Release note: Long model tasks can retain more context and continue reading complete tool output; remote execution preserves compatibility with released Agents. Updated remote limits require an updated Agent and Runtime bundle.
2026-09-10 03:01:21 +08:00
mesalogo 80b0b18712 feat: add persistent workbar browser tabs and scoped MCP routing 2026-09-10 01:26:56 +08:00
mesalogo 428b4c0d08 fix: synchronize scheduled task status and run actions
Reflect queued and active schedule runs in task status, refresh tasks on queue changes, and prevent duplicate Run Now actions until the current run finishes. Add lifecycle regression coverage and update scheduled task behavior documentation.

This file-level commit also includes concurrent conversation history, browser workbar, and runtime selection changes in the affected files. Focused schedule regressions passed; full workspace validation has unrelated failures as recorded during development.

Release note: Scheduled tasks now refresh their queued and running status and keep Run Now disabled while an occurrence is active.
2026-09-10 01:24:51 +08:00
mesalogo ec100f8176 feat: improve direct model workspace tools
Direct models now use bundled ripgrep for workspace discovery, read files through resumable pages, and edit multiple files with apply_patch. Ask can search and read while Execute retains write access, and fixed total input limits no longer constrain these tools.

Release note: 直连模型现可使用随包 ripgrep 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
2026-09-10 00:45:03 +08:00
mesalogo c1f8ba80ee test: validate external knowledge provider contracts
Add read-only Dify, FastGPT, and RAGFlow probes, a redacted live capability baseline, and focused adapter tests. Document version-specific API and UI behavior, including Dify 0.x/1.x differences and RAGFlow GraphRAG evidence.
2026-09-10 00:33:09 +08:00
mesalogo f3b695214b fix: make workbar browser fullscreen 2026-09-09 21:15:28 +08:00
mesalogo abd3d5c6bb fix: keep workbar tab close background transparent 2026-09-09 20:01:03 +08:00
mesalogo f669e490b8 feat: add workspace file management and embedded browsing
Users previously had to leave the workbar to manage project files and inspect Git branches or commit history. Add file and directory creation, rename, move, delete and properties, plus searchable branches, branch creation and switching, explicit fetch, and commit file diffs.

Keep file previews in the right workspace pane with directory expansion, selection, scroll and focus preserved on return. Add compact action menus, directory pickers, preview refresh and source switching. Embed the shared browser session directly in the workbar with navigation, resizing and fullscreen controls instead of a separate interaction window.

Moves stay within the current workspace, deletion is permanent after confirmation, and Git switch reports conflicts without discarding or stashing changes. Typecheck, lint and staged whitespace checks pass; existing full-suite failures and the blocked real remote directory validation remain recorded in the workbar progress document.

Release note: 可在右侧工作区管理文件、切换 Git 分支并查看提交历史,返回预览后保留浏览位置;浏览器支持在工作栏内直接交互和全屏显示。
2026-09-09 15:47:24 +08:00
mesalogo b6d4633a33 docs: design ShareServer Office editing 2026-09-09 12:43:00 +08:00
mesalogo 5782667a40 fix: accept equivalent remote runtime lock formatting 2026-09-09 11:29:37 +08:00
mesalogo e35ea7692f test: avoid disk-bound history limit fixtures
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
Use real in-memory SQLite for the remote-message ordering and active-task
retention query tests. Both exceeded the CI five-second limit while seeding
502 independently committed records on disk.

Keep the record counts, public database calls, and assertions unchanged.
Disk persistence, transactional migration, and reopen tests remain disk-backed.
Production database behavior is unchanged.
2026-09-09 03:33:25 +08:00
mesalogo 5a77dfdb93 fix: prevent managed runtime stalls during file changes
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
Automatic native Git snapshot diffs could block concurrent OpenCode sessions
after broad rewrites or line-ending conversions. Managed desktop and Agent
profiles now skip snapshots that GoodBuddy does not use, while preserving
file tools, native subagents, workspace Git operations, and Ask/Execute rules.

Drain otherwise-unused Continue utility-host stdout to prevent console output
from filling the pipe and blocking requests. Correct the lifecycle regression's
body observer to watch the underlying reader rather than a buffered wrapper.

Prepare Desktop 0.12.10 and Agent 0.11.22 with aligned bilingual notes and
feature documentation. Existing runtimes require restart or Agent update.
Full desktop UI before/after and Linux Agent production-path checks passed;
3626 tests passed, with 61 existing platform/manual skips.

Release note: 修复批量文件改写或换行转换后的托管 OpenCode 并行停顿,以及 Continue 控制台输出阻塞;本机升级重启后生效,远程项目请配套更新 Agent。
2026-09-09 03:07:55 +08:00
mesalogo 177ab2140e fix: align chat messages and composer without horizontal overflow
Move message avatars into outer gutters and align message content with the composer while preserving existing colors and styling. Keep scrollbar space in the outer gutter to prevent horizontal scrolling.

Release note: Chat avatars now sit outside message content, with the composer aligned to message edges and no extra horizontal scrollbar.
2026-09-09 01:51:27 +08:00
mesalogo cbb6398851 fix: resolve npm for portable runtime packaging 2026-09-09 01:43:53 +08:00
mesalogo b1ba14f98e fix: move todo source entry to detail pane 2026-09-09 01:26:10 +08:00
mesalogo e95c7a60d4 chore: prepare 0.12.9 desktop 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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / Validate source (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
Record the approved bilingual notes for todo source entries, accurate
request status, parallel OpenCode reliability, image editing, native
questions, longer Continue requests, reasoning formatting, and activity
history notices. Keep all previously published release entries intact.

Synchronize package and feature-matrix versions for the standard six-target
desktop release. Agent remains 0.11.21; no Agent release or LoongArch preview
is included. Release tags remain pending exact-commit approval and CI.

Validation: 3618 tests passed, 61 skipped; typecheck, lint, and bilingual
release-note verification passed. No local production package was built.
2026-09-08 23:01:14 +08:00
mesalogo c82d0161a9 fix: show accurate request status in message footers
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
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
Requests could appear to keep working while OpenCode waited to retry, and
retry backoff was described as an active attempt. Report native OpenCode
retry timing, distinguish direct-model backoff from dispatch, and show
preparation, tool activity, pending input, and terminal states accurately.

Keep only the message-footer indicator static. Conversation-list activity
indicators, send/stop controls, retry limits, and command replay behavior
remain unchanged. Align the feature documentation with verified behavior.

Validation: 3618 tests passed, 61 skipped; typecheck and lint passed.
Isolated desktop validation used five real text-model calls across three
profiles, with controlled local fault injection for retry transitions.

Release note: 消息底部现在准确区分请求准备、等待重试、开始重试和需要回答的状态,不再用闪烁暗示仍有进展;会话列表闪点及发送、停止行为保持不变。
2026-09-08 22:47:06 +08:00
mesalogo 9b7cf730d0 fix: raise Continue host body limit 2026-09-08 21:58:51 +08:00
mesalogo 22a60497cb fix: preserve reasoning markdown boundaries 2026-09-08 21:55:47 +08:00
mesalogo 78418a56fa fix: remove native question limits 2026-09-08 21:52:10 +08:00
mesalogo fbc0f5e9f3 feat: show source entries in todo view 2026-09-08 21:38:33 +08:00
mesalogo c1f4fd0580 fix: prevent parallel OpenCode conversations from hanging
Starting another conversation could replace a busy local OpenCode server
after a brief health-check failure, leaving the existing conversation
reconnecting to a dead endpoint.

Keep active conversations and native Compact operations on their current
server. Propagate server exit and shutdown to bound requests and cancel
event-stream retry waits so failures are explicit instead of hanging.
Do not automatically replay commands.

Release note: 修复同时使用多个本机 OpenCode 会话时旧会话可能一直等待的问题。服务异常退出时会明确提示失败,不会自动重复执行命令。
2026-09-08 21:10:35 +08:00
mesalogo 28de0c45ba fix: restore image editing requests
Image editing requests included a generation-only response_format field that some compatible providers reject. Editing now sends only supported defaults while preserving explicitly configured custom request fields.\n\nRelease note: 修复部分图像服务商修改图片时报错的问题;参考图编辑现在可以正常提交。
2026-09-08 18:53:15 +08:00
mesalogo 024e6fb4b2 fix: remove legacy activity history warning 2026-09-08 15:56:45 +08:00
mesalogo f95acdc000 fix: restore packaged runtimes and ship macOS DMG only
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
Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource
copier excludes node_modules at its source root. Copy from the parent and
verify every prepared configuration file in the final package. Bundle DSH
session projection with the unpacked Host instead of leaving an ASAR-only
external import, and reject external DeepSeek imports in shipped chunks.

Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake
in Windows x64 CI before removing unpacked output. Add regressions using the
actual electron-builder copier and Vite externalization configuration.

Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG
only. Update aggregation, website downloads and update readers for 12
installers and 20 assets. Current readers retain historical DMG/ZIP support;
the user accepts that older macOS GitHub readers and all older mirror readers
need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag.

Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files;
typecheck, lint, release-note validation, website static checks and 18 website
tests passed. No local production build or packaged-app launch was performed
at the user's request; actual packaged startup validation runs in CI.

Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
2026-09-08 14:17:38 +08:00
mesalogo 451b30d434 docs: design application and system tool navigation
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
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-09-08 12:35:00 +08:00
mesalogo 25e897c22b chore: prepare GoodBuddy 0.12.7 and Agent 0.11.21 releases
Set the approved Desktop and Agent versions and package the approved Chinese
and English notes without changing published release history. Keep OpenCode
pinned to 1.18.29 and require Desktop 0.12.7 before the new Agent package.

Align both feature lists and affected design documents with reference-image
editing, inherited expert work modes, preview boundaries, and remaining
release acceptance. Application navigation remains design-only. No LoongArch
preview is planned for this release; Agent publication remains non-Latest.

The first candidate full run exposed a browser test clicking Send before it
was enabled. Wait for startup readiness using the existing test convention;
production code is unchanged by this release-preparation commit.

Validation: final full suite 3597 passed, 0 failed, 61 skipped across 338 files.
Typecheck, lint, bilingual release-note validation, version consistency,
unchanged historical notes, whitespace and relative-link checks passed.
No additional real model calls, local production builds, pushes or tags.
Concurrent navigation, workbar and roadmap design edits are not included.
2026-09-08 12:29:49 +08:00
mesalogo c8db340cc7 fix: prevent runtime permission stalls and Agent reconnect failures
Execute could stall on workspace-external access or native child-session
permissions, while structured questions could be lost or answered incorrectly.
Route owned OpenCode descendant interactions, correct Continue question and
custom-answer handling, and resolve DSH Execute ACP permissions without a
second approval. Ask remains read-only and unrelated sessions stay isolated.

Restore the required DSH session projection service. Reuse offline OpenCode
configuration by plugin version, remove unused npm shims from remote payloads,
normalize dependency modes, and accept real compound descriptors up to 4 MiB.
Fix intermittent Agent attach rejection and closed-handshake waits.

Add focused and real-Runtime regressions, document interaction boundaries and
validation evidence, and include the existing application/tool navigation
design drafts. Those navigation changes are design-only, not implemented UI.

Validation: full suite 3597 passed, 0 failed, 61 skipped across 338 files;
typecheck, lint, release-note verification, whitespace and relative-link checks
passed. Current-source Linux x64 installation, Ask/Execute, native subagent
external writes, reconnect and stop/bootstrap passed with 10 completed real
model calls, 13 native endpoint tests and 20 consecutive handshakes.
System sleep/wake and release CI packaging remain unverified.

Release note: 修复 Execute 访问工作区外目录或运行原生子代理时卡在权限等待的问题,并恢复 OpenCode 与 Continue 结构化问答的正确转交。修复远程 Agent 安装与重连失败;Ask 仍保持只读,任意终端密码或业务确认不会自动代答。
2026-09-08 11:34:45 +08:00
mesalogo 9aed926cda fix: stabilize OpenCode reuse and session switching
Check cached embedded servers before reuse, isolate concurrent cancellation, and serialize request MCP registration and cleanup. Preserve native controls across session switches and recover usable partial inventories.

Share the bundled offline OpenCode configuration with desktop and Agent launch paths, including model bridge environment forwarding.

Validation: focused tests and typecheck passed; lint passed before the final fixes. The full test run exceeded 120 seconds and reported a DS stdio MCP failure. Current-source Agent host and real suspend/resume validation remain pending.

Release note: 修复切换会话后 OpenCode 原生控件消失及并发取消相互影响的问题,并完善内嵌服务复用检查和桌面、Agent 的离线依赖配置。
2026-09-08 01:09:14 +08:00
mesalogo 86daae8be2 fix: remove terminal emulator padding 2026-09-07 18:08:52 +08:00
mesalogo 19086d656e fix: reuse bundled agent runtime resources 2026-09-07 16:57:59 +08:00
mesalogo d3947c79dd fix: avoid rerendering chat history while typing 2026-09-07 16:46:18 +08:00
mesalogo 6a1a671d3b fix: support reference image editing 2026-09-07 15:48:47 +08:00
mesalogo dbb7c2fc80 fix: align message content below avatars 2026-09-07 14:02:59 +08:00
mesalogo 237b8a8cee chore: prepare GoodBuddy 0.12.6 and Agent 0.11.20 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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
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 / linux arm64 (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 / 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
Record approved bilingual notes for complete subagent progress, workspace diffs, knowledge selection, OpenCode stream cleanup, external command directories, and restored GitHub Agent updates. Remote progress and diff improvements require the new Agent package. No LoongArch preview is included.
2026-09-06 22:39:41 +08:00
mesalogo d08ba33c3e fix: complete subagent output and file workspace interactions
OpenCode child cards now show ordered text, reasoning and tool progress,
with the final result displayed separately. Local SDK and remote ACP paths
share child tracking, including progress retained across recovery and cancellation.
Remote live progress requires an Agent package containing the event plugin;
older Agent packages still expose final results without inventing missing history.

Knowledge-library rows remain clickable when native label activation changes
focus. File workspace changes use colored Git status letters and per-file
staged/unstaged diffs, including deleted, renamed and untracked files.
Refreshing reloads the selected diff and expanded directories, and more than
50 changes remain accessible through incremental loading.

Validated with 3552 passing tests, typecheck, lint, desktop build, current-source
Linux Host scenarios and isolated Windows desktop UI interactions.

Release note: 展开 OpenCode 子代理卡片可查看执行过程、工具详情与独立最终结果;知识库支持整行勾选。工作区支持单文件暂存与未暂存差异,刷新会更新当前差异,更多变更可继续加载。远端实时过程需要更新 GoodBuddy Agent。
2026-09-06 22:13:45 +08:00
mesalogo b03c25685c fix: clean up OpenCode streams and allow external working directories
Completed OpenCode requests left their event streams open across repeated and
parallel conversations. Each request now closes its own subscription on exit
without cancelling peer sessions, and cancellation preserves its original cause.

Direct-model Execute commands no longer restrict cwd to the workspace. Absolute
paths, parent-relative paths, and symlinks to external directories are accepted.
The workspace remains the default and relative-path base; Ask stays read-only,
and local execution binding, directory validation, and process cleanup remain.

Add real OpenCode lifecycle and real Shell tool-loop regressions, plus matching
feature documentation. Focused checks: 182 passed, 1 skipped; typecheck and lint
passed. Full shared-worktree suite: 3542 passed, 57 skipped, 1 unrelated sidebar
heading failure, reproduced separately and documented.

Release note: 修复 OpenCode 连续及并行对话结束后事件连接未释放的问题;直连模型 Execute 命令现在可直接指定工作区外的绝对路径、相对路径或符号链接目录,Ask 只读边界保持不变。
2026-09-06 19:53:42 +08:00
mesalogo 7003141e56 fix: restore GitHub Agent update checks with paginated discovery
The GitHub release history exceeded the catalog response limit, preventing users from checking Agent updates. Read ten releases per page and stop at the first eligible catalog while preserving the existing 100-release search window and signature checks.

Release note: 修复选择 GitHub 来源时 Agent 更新检查提示发布目录超出大小限制的问题;镜像更新和已有本地包保持不变。
2026-09-06 19:25:40 +08:00
mesalogo e692362414 chore: move ShareServer development to a standalone repository
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
Remove the extracted ShareServer prototype and its server-specific documents
from Desktop. Link the shared network architecture to goodbuddy-shareserver,
while retaining Desktop ownership of the cross-product architecture.

Preserve local generated artifacts and ignore the old server build output.
Desktop runtime behavior and released user data are unchanged. Desktop tests,
typecheck, and lint passed; the standalone service was validated separately.
2026-09-06 19:02:41 +08:00
mesalogo ee804edcae fix: isolate desktop lint from ShareServer configuration
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
Exclude the independent prototype from root lint and explicitly anchor TypeScript parser configuration to the Desktop repository. This fixes clean CI runners detecting both root and ShareServer configuration directories. Product behavior and approved release notes are unchanged.
2026-09-06 17:49:44 +08:00
mesalogo 85a4a374fa fix: keep ShareServer prototype tests outside desktop releases
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
Exclude the unfinished standalone ShareServer prototype from the Desktop root test suite and remove its CI dependency installation. Preserve its independent test configuration and document the release boundary. Approved Desktop and Agent release notes remain unchanged.
2026-09-06 17:37:37 +08:00
mesalogo 89a81dd07c fix: install ShareServer test dependencies in release CI
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
The root validation suite includes ShareServer tests, but clean CI runners only installed Desktop dependencies and could not resolve react-router-dom. Install the independently locked ShareServer dependencies before running validators. Release content and approved bilingual notes are unchanged.
2026-09-06 17:01:50 +08:00
mesalogo 0eb664c0b6 chore: prepare GoodBuddy 0.12.5 and Agent 0.11.19 releases
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
Record approved bilingual release notes and align Desktop and Agent version metadata. Publish Desktop first so users can upgrade before the shared Agent catalog gains macOS arm64 entries. No LoongArch preview is included.
2026-09-06 16:44:46 +08:00
mesalogo 9c6d1611e2 feat: support macOS arm64 remote Agent hosts
Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation.

Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release.

Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded.

Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
2026-09-06 16:14:02 +08:00
mesalogo 1f01c26e04 fix: keep replies streaming while switching windows
Switching away from the desktop window and back could mark a reply that was
still being generated as "上次运行意外中断", freeze its output partway through,
and push the next message into the pending queue even though nothing had
actually failed. The conversation area also shifted on each switch because the
OpenCode runtime controls were unmounted and re-detected on every refresh.

Reading the conversation store no longer treats an in-progress reply as
interrupted; only startup recovery marks a run that an application exit truly
cut off. Runtime feature detection is now keyed on the resolved runtime scope,
so the OpenCode toolbar and manual Compact control stay in place across
refreshes. No request timeout or recovery behavior changed.

Release note: 修复来回切换桌面窗口时,正在生成的回复被误判为"上次运行意外中断"、输出中途停住、之后发送的消息只能进入待发送队列的问题。使用 OpenCode Runtime 时,会话区也不再因重复检测专属功能而抖动。
2026-09-05 16:28:45 +08:00
mesalogo dd8289fdef fix: make local knowledge bases ready for daily use
Knowledge selection was shared globally, document readiness overstated usable content, and important import and source-management paths exposed technical details or lacked safe recovery actions. Knowledge scope now persists per conversation, readiness reflects real processing channels, and the workspace guides users from creation and import into cited chat with controlled source actions.

Release note: 知识库现按对话保存使用范围,准确区分可用、处理中和失败文档,并提供更简洁的创建、检索测试、来源打开、删除确认和失败重试流程。
2026-09-05 15:51:07 +08:00
mesalogo d5fa0ec67c fix: keep long conversation content scrollable
Large messages containing reasoning, tool lists, or long replies could use an estimated off-screen height that left later content outside the reachable scroll range. Conversation messages now participate in layout with their actual height while the existing 80-message render window keeps long histories bounded.

Release note: 修复偶发的长会话滚动条提前到底问题;包含较长推理、工具记录或正文时,现在可以直接滚动查看全部已加载内容,无需切换会话刷新。
2026-09-05 14:51:21 +08:00
mesalogo 6eb73657e0 chore: ignore Python bytecode caches 2026-09-05 14:02:53 +08:00
mesalogo 4041edefa8 feat: add ShareServer console prototype
ShareServer previously existed only as design documents. The repository now includes a standalone Hono service and a responsive React management-console prototype covering administrator and member workflows, with local build, test, and container entry points.

The sharing design now includes organization-scoped digital agents and explicit local partitioned-memory selection for @ calls. Memory documentation consistently uses partitioned memory and treats time or internal events only as optional recall triggers.

This remains an interactive prototype: fixture actions do not persist, and production authentication, PostgreSQL isolation, device connections, routing, relay, and federation still require implementation.

Release note: 新增可独立运行的 ShareServer 管理控制台交互原型,并完善组织数字人与分区记忆共享设计;当前原型数据和操作不会写入生产系统。
2026-09-05 14:01:20 +08:00
mesalogo 482cd0c467 chore: prepare GoodBuddy 0.12.4 releases
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 Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (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
Bump Desktop to 0.12.4 and the separately published GoodBuddy Agent to 0.11.18. Add the approved bilingual release notes for HTML previews, long-run and queue reliability, remote terminal-state convergence, and faster OpenCode follow-up on Linux ARM.

This candidate uses the standard six-platform x64/arm64 matrix and does not request a LoongArch experimental preview.
2026-09-05 10:01:45 +08:00
mesalogo 5a1afb00f7 fix: reuse Runtime after native inventory load
GoodBuddy Agents / Validate Agent source (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
Native capability loading previously cold-started a temporary Runtime and disposed it immediately, so Linux ARM users paid the OpenCode startup cost again on their first request. Native inventory loading now uses the cached execution Runtime, preserving normal idle eviction and lifecycle cleanup.

Release note: 优化 Linux ARM 上切换 OpenCode 后的响应速度;原生能力清单与后续对话现在复用同一 Runtime,避免重复冷启动。
2026-09-05 09:45:23 +08:00
mesalogo a4b64a1641 feat: preview Agent HTML in conversations and unblock long replies
Agent replies that produced HTML could only be read as source, so users had
to copy the markup into an external file to see the result. Completed Agent
and Subagent output now renders complete HTML documents, single-root HTML
replies, and `html` / `htm` code blocks as an isolated static preview in
place. Each preview keeps a persistent notice that scripts and network are
disabled and offers the same icon actions as Mermaid diagrams: a source
toggle and a full-screen preview. The new "render HTML in conversations"
switch under Platform Features / General is on by default, applies to the
open conversation immediately, and falls back to HTML source when disabled.
Previews cannot run scripts, reach the network, submit forms, open windows,
or load remote media; author stylesheets and inline styles are preserved so
the intended layout survives, and remote resource loading is refused by the
preview CSP instead.

Direct-model replies no longer fail once they grow large. The byte caps on
streaming responses, single stream blocks, non-streaming replies, model tool
arguments, tool request context, aggregate tool results, and accumulated
answer text are removed, so users stop seeing "模型接口流式响应超过安全限制"
on long answers. Response length is now governed by the request timeout,
cancellation, and context compression. Image generation responses keep their
decodable-image validation.

A message sent after a failed or interrupted run stayed in the pending queue
instead of running. The conversation is now released whenever a run reaches
any terminal state, not only when the renderer reports readiness again, and a
dispatched queue item whose run never starts releases its reservation and
returns to pending. Later messages dispatch without extra user action.

Release note: Agent 回复中的 HTML 现在可直接在会话内预览,支持查看源码和全屏预览,默认开启并可在“平台功能 / 通用设置”关闭;直连模型的长回复不再因大小限制中断;上一次运行失败或被中断后,新发送的消息会立即执行而不再排队等待。
2026-09-05 09:10:44 +08:00
mesalogo 9588c4b901 fix: preserve long run activity and large previews
Long Runtime sessions could silently stop retaining tool and Subagent activity, truncate Run history details, and reject workspace previews over 256 KB. Run history now persists in Main SQLite without the former fixed record or detail caps, surfaces possible loss from legacy caches, and renders long histories in batches.

Local and remote workspace previews now load UTF-8-safe 256 KiB pages on demand instead of rejecting larger text files. The legacy migration, clear-data path, IPC contracts, tests, and feature documentation are updated with the resulting behavior.

Release note: 长时间 Runtime 运行现在会完整保留工具和 Subagent 活动;运行记录迁移到 SQLite,并提示旧版可能已丢失的内容。超过 256KB 的本地及远程文本文件可通过分页面预览继续加载。
2026-09-05 00:31:42 +08:00
mesalogo d180f63abd fix: converge remote run state on persisted terminal messages
After a managed SSH disconnect the renderer kept showing the remote
Agent as processing even though the recovered task had already been
persisted as completed. Local streaming copies shadowed the persisted
terminal state in mergePersistedConversations, and recovery completion
never asked the renderer to re-read conversations.

- Prefer the persisted terminal state over a local streaming message
- Settle active runs whose messages already reached a persisted terminal
  state and release the conversation queue once
- Refresh conversations on window focus and visibility changes
- Publish a conversation change before recovery reports completed/failed
2026-09-04 16:54:35 +08:00
mesalogo 670171ce5d merge: preserve GitHub README updates
GoodBuddy Agents / Validate Agent source (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
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (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 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 / 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
2026-09-04 08:21:17 +08:00
mesalogo 4490e5fd15 fix: stabilize Runtime workflows for 0.12.3
OpenCode status checks could cold-start throwaway servers, unrelated
conversations could block each other, and model, Continue, or remote paths
could replay or duplicate work under failure and backlog conditions. Settings
could also lose edits or overwrite unrelated model fields.

This update makes status checks passive, keeps per-conversation ordering while
allowing concurrency, preserves native long-running work with bounded display
capture and backpressure, narrows retries to known pre-dispatch failures, and
aligns remote Ask permissions without reducing Execute. It also keeps
Embedding updates narrow, improves settings and browser guidance, removes the
unused legacy Main gateway state, and records the validated Desktop 0.12.3 and
Agent 0.11.17 release metadata.

Release note: 改善 OpenCode 切换、多会话长任务、模型重试、远程 Ask/Execute 与设置保存可靠性,并完成 Desktop 0.12.3 / Agent 0.11.17 发布准备。
2026-09-04 08:05:29 +08:00
mesalogo 374968d50b docs: add desktop pet feature design
Adds the full documentation set for the planned desktop pet feature under
docs/features/desktop-pet: README, PRD, user stories, logic design, UI
design, and technical design. The pet is scoped as a lightweight always-on
entry point for quick drafting, drag-and-drop staging, and task status at a
glance; it does not introduce a second Ask/Execute channel, task execution
path, or independent task-state store. Design only, no implementation yet.
2026-09-03 23:22:33 +08:00
mesalogo 4e60a4ca61 feat: customize model provider requests
Model connections can now save bounded JSON request headers and top-level body fields. Direct requests and Continue support both; local OpenCode and DeepSeek Harness use only native header support, while the managed remote OpenCode gateway applies both.

Runtime, protocol, and authentication fields keep precedence. Custom values are ordinary connection settings, not credential storage. This migrates Runtime settings from v20 to v21 and updates the DeepSeek control and managed Runtime ACP capabilities.

Release note: 模型连接详情新增自定义请求头和请求体;直连与 Continue 支持两者,本地 OpenCode 和 DeepSeek Harness 仅应用原生支持的请求头,受管远程 OpenCode 支持两者,界面会明确提示生效范围。
2026-09-03 22:34:01 +08:00
mesalogo 3765a937bd docs: clarify website product highlights
Reframe the feature section around the broader desktop product rather than only Agent Runtimes. Clarify official x64 and arm64 coverage, the separate LoongArch preview, and that compatibility claims are not vendor certification.
2026-09-03 22:31:49 +08:00
mesalogo feaebdf221 feat: share built-in browser across runtimes
The built-in browser was limited to direct-model tools, and its Agent switch also blocked manual Browser workbar actions. It is now a request-scoped GoodBuddy capability assignable to direct models, managed OpenCode, and Continue, while manual browser controls remain available when Agent access is off.

Existing browser enablement migrates to capability settings version 6. DeepSeek Harness remains unsupported.

Release note: 内置浏览器现在可分配给直连模型、OpenCode 和 Continue;关闭 Agent 浏览器能力不会影响用户在浏览器工作栏中的手动操作。
2026-09-03 19:36:06 +08:00
mesalogo 70ec2bf792 fix: restore rounded workbar tabs
The workbar tabs, close controls, and add button now use the shared rounded control styling again. The terminal panel and its toolbar remain square as requested.

Release note: 恢复工作栏页签、关闭按钮和加号入口的圆角外观;终端面板与终端工具栏继续保持直角。
2026-09-03 19:32:00 +08:00
mesalogo 8b9dbf55df fix: allow concurrent and unrestricted Runtime sessions
Local embedded OpenCode serialized otherwise independent conversations, and GoodBuddy could stop long Runtime tasks after fixed tool, activity, model-call, or accumulated display-output limits.

Different conversations can now run concurrently while requests in the same conversation stay ordered. Dynamic MCP tools remain isolated to the current request. OpenCode, Continue, managed ACP, and the Agent model gateway no longer stop native execution at GoodBuddy-defined call counts or prompt-wide output totals; display copies truncate and transport queues apply backpressure instead. Prompt-wide quota fields are removed from the current contracts.

Per-call model settings, provider request timeouts, cancellation, and same-conversation ordering remain unchanged.

Release note: 修复本机 OpenCode 不同会话互相等待的问题,并移除 GoodBuddy 对单次 Runtime 工具、活动和模型调用次数的人为上限;长输出现在只截断展示或施加传输背压,不再取消原生任务。
2026-09-03 17:17:09 +08:00
mesalogo b1e3101857 fix: restore terminal paste and square workbar chrome
Terminal paste previously relied on the Renderer browser Clipboard API, which could be denied by Electron and leave the toolbar action unusable. Text clipboard access now goes through the trusted Main/Preload bridge, and the workbar tabs plus terminal toolbar use straight corners for a continuous docked appearance.

Release note: 修复终端工具栏粘贴可能因剪贴板权限被拒绝而失败的问题,并将工作栏页签与终端工具栏调整为直角样式。
2026-09-03 17:12:27 +08:00
mesalogo 7369c13bcf docs: clarify remote runtime backpressure 2026-09-03 16:34:57 +08:00
mesalogo fba5e1ad30 docs: design device sharing network 2026-09-03 16:34:28 +08:00
mesalogo 468cf0483f docs: design external knowledge providers 2026-09-03 16:29:51 +08:00
mesalogo 6586192222 fix: enforce remote Ask at tool boundary
Managed SSH Ask previously required bubblewrap and applied a process-level filesystem sandbox, while Execute launched directly. Ask and Execute now share the same direct signed Runtime startup; Ask remains read-only through the Agent's existing tool permission mediation, which only permits native read requests and rejects other tool kinds.

Release note: 远程 Ask 与 Execute 现在都可直接启动签名 Runtime,无需 Host 安装 bubblewrap;Ask 仍在工具权限层仅允许读取操作。
2026-09-03 16:23:14 +08:00
mesalogo 987a08ad78 fix: allow remote Execute without bubblewrap
Remote Hosts without the Ask-only bubblewrap tool could report matching Agent and Runtime versions but fail project creation because the Agent hid the Runtime ACP capability entirely. The Agent now advertises a verified Runtime independently of bubblewrap, so Execute can start directly while Ask keeps its existing read-only bubblewrap boundary.

Release note: 修复远程 Host 缺少 bubblewrap 时 Execute 项目无法创建的问题;更新到 Agent 0.11.16 后,Execute 不再受 Ask 专用系统工具影响。
2026-09-03 15:59:05 +08:00
lofyerandGitHub 35a93e61cd Merge pull request #3 from mesalogo/jojocoding-patch-2
Update README.md
2026-09-03 14:25:23 +08:00
lofyerandGitHub 0cba3855ea Merge pull request #2 from mesalogo/jojocoding-patch-1
Update README.zh-CN.md
2026-09-03 14:24:28 +08:00
mesalogo 74c3ec19b4 fix: retry transient direct model failures
Direct model conversations previously stopped after the first transient network error or request timeout, including TLS setup ECONNRESET failures. Text requests now retry up to three times with cancellable backoff before visible output, while partial streamed responses are never replayed.

Release note: 直连模型遇到临时网络故障或请求超时时,会在尚未输出内容的情况下自动重试最多 3 次,并支持随时取消。
2026-09-03 14:24:09 +08:00
jojokeeandGitHub cdc8f6f57a Update README.md
更新英文README
2026-09-03 14:22:33 +08:00
jojokeeandGitHub b8c1d33e7b Update README.zh-CN.md
更新社群入群信息
2026-09-03 14:20:34 +08:00
mesalogo f18e4abb3c fix: simplify topbar Runtime status
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
The topbar repeated a textual state after the Runtime or model name even
though it already displayed a compact status indicator. It now uses green,
yellow, and gray dots for ready, connecting, and unavailable states while
keeping detailed status available to assistive technology and tooltips.

Release note: 顶栏 Runtime 状态改用绿、黄、灰三色标记,不再重复显示“就绪”“连接中”或“不可用”文字。
2026-09-03 13:42:41 +08:00
mesalogo c52732441a chore: coordinate Agent 0.11.15 and Desktop 0.12.2
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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
Cross-platform packages / Authorize desktop packaging (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
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The published Agent 0.11.14 predates the registered-Runtime activation change included in the Desktop candidate. Bump the Agent lock to 0.11.15 and the unpublished Desktop recovery to 0.12.2 so the compound package minimum Desktop version, bilingual notes, and feature documentation describe one coordinated release.

Keep v0.12.0 and v0.12.1 immutable and unpublished, and document that Agent tags also require the exact main candidate workflow to pass before tagging.

Release note: 托管 Linux Host 使用 Agent 0.11.15,已登记 Runtime 激活时不再重复检查完整 OpenCode 二进制。
2026-09-03 11:51:30 +08:00
mesalogo ba000300d5 test: stabilize pre-tag main validation
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The Unix private-endpoint relay regression combines socket authentication, SQLite journaling, attach relay framing, and two protocol requests. Give only that end-to-end case a bounded 30-second allowance so a loaded serial CI runner does not fail its previous 5-second default.

Document the mandatory Desktop release order in BUILD.md: push the candidate to both main branches, wait for validators and the production bundle on that exact SHA, and only then create the annotated tag.
2026-09-03 10:56:42 +08:00
mesalogo 3b8d129693 fix: install managed Python on Linux filesystems
GoodBuddy Agents / Validate Agent source (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
Managed Python installation rejected valid case-distinct terminfo symlinks in the pinned Astral archives on Linux x64 and arm64. Archive uniqueness now follows the target filesystem: Linux preserves case-distinct paths, while every target still rejects exact duplicates and unsafe entries.

Move the unpublished 0.12.0 bilingual release notes to the immutable-tag recovery version 0.12.1 and record the failed tag evidence.

Release note: 修复 Linux x64/ARM64 上托管 Python 因官方归档包含大小写不同路径而无法安装的问题。
2026-09-03 10:31:14 +08:00
mesalogo fb5a9c6f43 test: allow bounded knowledge snapshot writes
Cross-platform packages / Validate source (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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
The complete-snapshot boundary test performs 501 synchronous SQLite source and document writes. It passed functionally but could exceed its 10-second case timeout on the serial Linux CI runner, so give this I/O-heavy case a bounded 30-second allowance without changing product behavior or global test limits.
2026-09-03 06:00:16 +08:00
mesalogo 26b87d7ed5 test: use the installed OpenCode entrypoint
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
The Linux routing test still referenced the obsolete bin/opencode name. With intentional path-only Runtime selection, that stale fixture was no longer masked by automatic discovery; use the locked opencode-ai 1.18.9 bin/opencode.exe entrypoint on every platform.

Keep the approved 0.12.0 notes and bilingual feature matrices explicit that local OpenCode paths are not prevalidated and fail only at actual launch.
2026-09-03 05:37:52 +08:00
mesalogo 93721d69b6 test: serialize full-suite validation
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
The Linux release validator still exhausted process and socket timing with two Vitest workers, causing unrelated OpenCode and private-endpoint tests to fail. Run test files with one worker so process-backed integration tests remain deterministic within the existing CI job budget.
2026-09-02 23:19:48 +08:00
mesalogo 8ecd87381f test: stabilize 0.12.0 candidate validation
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Full-suite resource contention could make asynchronous renderer assertions and short runtime timing tests fail even though focused runs passed. Limit Vitest to two workers and give the affected UI operations the existing three-second async window.

Also include the approved Linux ARM OpenCode startup note in both feature matrices and release-note languages, and record the successful isolated Linux x64 Host validation of the current-source Runtime activation path.
2026-09-02 22:57:31 +08:00
mesalogo dc36e7a967 fix: skip repeated OpenCode binary validation
OpenCode startup on Linux ARM could run a --version probe before launching the configured Runtime and recheck registered remote binary metadata. Execution now trusts the selected path and registered manifest identity after setup-time verification, so switching and cold launch avoid duplicate binary work.

Release note: 优化 Linux ARM 上切换 OpenCode 的启动速度;已安装或已登记的 Runtime 在执行时不再重复启动或检查二进制。
2026-09-02 22:41:50 +08:00
mesalogo cdecb584d5 test: wait for settings before appearance assertions
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
The Linux CI run could query the Appearance tab before the lazy Settings route finished loading. Appearance tests now wait for the Settings heading before interacting with its tabs, matching the stable navigation pattern used elsewhere.
2026-09-02 21:47:18 +08:00
mesalogo c4231c8f9c chore: prepare GoodBuddy 0.12.0 release
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Update the desktop version, approved bilingual release notes, and synchronized English and Chinese feature matrices for the 0.12.0 candidate.

Release preparation now runs local tests, type checks, and lint without a local production build, then requires the main-branch CI build before tagging. The package matrix uses a native Windows arm64 runner and validates managed Python on every target without adding extra license files to the desktop package.
2026-09-02 21:36:27 +08:00
mesalogo 8a14cf356a fix: hide unverified capability placeholders
The tool environment page always displayed an unverified capability-dependency state even though it had no Skill or MCP evidence behind it. The page now shows only actual interpreter and companion-tool diagnostics; capability dependency status remains reserved for capability details with real validation results.

Release note: 工具执行环境页不再显示没有实际验证依据的能力依赖占位状态。
2026-09-02 17:00:42 +08:00
mesalogo 19a3bafb63 docs: prevent dead-end selection flows
Document that options requiring an in-place prerequisite must remain operable, defer persistence until validation succeeds, avoid implicit first-candidate selection, preserve the previous value on cancellation, and show errors beside the action.
2026-09-02 16:47:24 +08:00
mesalogo 5fac1d3c41 fix: align DeepSeek Harness model selection
DeepSeek Harness previously required a separate Runtime model selection when no administrator-provided connection was available, even if the user had already selected a compatible GoodBuddy model. It now prefers the administrator connection, then follows the compatible default model or first compatible connection across conversations, projects, and messaging channels.

Release note: 修复 DeepSeek Harness 未自动使用已选兼容模型的问题;没有管理员预置连接时,现在会跟随当前默认或首个兼容模型,无需再到 Harness 页面重复指定。
2026-09-02 16:45:42 +08:00
mesalogo 42cbfa43f1 feat: add copy action to AI responses
Completed AI responses now expose a bottom copy action that copies their Markdown source without reasoning, tool logs, or citation metadata. Clipboard writes use a validated Preload/Main bridge so sandboxed Electron sessions no longer fail with a renderer clipboard permission error; full-conversation copy uses the same path.

Release note: AI 回复底部现在可以直接复制 Markdown 原文,并修复了沙箱环境下回复和完整会话可能无法写入剪贴板的问题。
2026-09-02 16:35:51 +08:00
mesalogo 4d9ab72778 fix: keep custom tool environments selectable
The custom Node.js and Python choices could appear unavailable until a candidate was already discovered, and selecting the source implicitly adopted the first candidate. The chooser now stays operable, waits for an explicit validated path, keeps cancellation quiet, and shows validation errors beside the affected runtime.

Candidate discovery now prioritizes active virtual environments and Conda before bounded user/system PATH and common Node/Python locations, while preserving canonical validation and deduplication.

Release note: 修复自定义 Node.js/Python 环境入口不可用的问题,并增加对 PATH、venv、Conda 和常见安装位置的解释器检测。
2026-09-02 16:29:55 +08:00
mesalogo be39665ca9 feat: add managed local tool environments
GoodBuddy can now provide managed Node.js and on-demand Python to local Skills and stdio MCP servers, or validate an explicitly selected custom interpreter. The Capabilities and tools settings page exposes source selection, diagnostics, installation progress, cancellation, and removal while new processes receive immutable PATH snapshots.

Application settings migrate to v10. Local terminal and remote Host environments remain unchanged. Windows x64 native-source Node and Python paths are validated; OSS mirror publication, redistributed license resources, and the remaining native platform probes are recorded as release blockers.

Release note: 新增工具执行环境,可为本机 Skills 和 stdio MCP 使用 GoodBuddy 托管的 Node.js、按需安装的 Python,或经过验证的自定义解释器。
2026-09-02 10:36:56 +08:00
mesalogo 1530fa116c fix: make product copy direct and factual
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
The website, README, and Smart Heartbeat interface used promotional superlatives, generic benefit labels, and personified growth language that obscured the actual product behavior. The English README is now the repository entry point with a separate Chinese version, and both website and in-app copy describe concrete capabilities, reports, suggestions, and execution boundaries.

Release note: 优化官网、README 与智能心跳界面的中英文文案,去除空泛宣传和拟人化表达,改为直接说明产品能力、运行记录与建议内容。
2026-09-02 01:14:16 +08:00
mesalogo b36d688900 feat: add direct model programming agents
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Direct model conversations can now execute bounded platform-native processes and delegate one level of programming work without duplicating tools in Agent Runtimes. Process and Subagent activity follows existing workspace, mode, approval, cancellation, and lifecycle boundaries, with chat attribution and complete feature documentation.

Remove the static Renderer bundle baseline gate and its build-only manifests; production builds continue to compile all processes and verify the control-plane installer. Also include the pending documentation link normalization.

Release note: 直连模型现在可在 Execute 模式运行本机命令,并可委派编程 Subagent 完成修改、测试和复核;构建流程不再因静态 Renderer 包体基线阻塞。
2026-09-02 00:50:54 +08:00
mesalogo d36889d9f6 fix: quiet resolved dependency reference logs
electron-builder listed shared, successfully resolved production dependencies as duplicate references during every package build. All desktop packaging entry points now lower only that non-actionable summary to debug while unresolved reference failures remain visible.

Move Renderer-bundled AntV G6 to development dependencies so its transitive graph is not copied into desktop packages.

Release note: 桌面打包不再显示无需处理的重复依赖引用摘要,也不再把已由 Renderer 打包的 AntV G6 依赖树重复复制到安装包中。
2026-09-02 00:45:15 +08:00
mesalogo 06c1123caa fix: focus built-in skill defaults
The Skills catalog exposed nine product-marketing recipes as independent
capabilities and enabled every marketing package for every Runtime by default.
It now keeps five top-level Skills, loads the nine recipes on demand through
the product-marketing package, and leaves the three marketing Skills disabled
until users enable them. Existing saved Skill settings remain authoritative.

Release note: 内置 Skills 已从 14 个顶层入口精简为 5 个;去 AI 味和长文工具默认启用,产品营销类默认停用,已保存的用户设置保持不变。
2026-09-02 00:44:44 +08:00
mesalogo 4f3e0bdcfd fix: unify capability settings navigation
Skills and MCP previously occupied separate top-level settings categories. They now share one Capabilities and tools category with accessible horizontal tabs, while the unimplemented tool environment remains hidden until its settings are usable.

Prevent nested PageTabs wrappers from collapsing to zero height in scrollable Flex layouts, add a CSS regression guard, and document the shared rule plus the planned local Node.js and Python environment.

Release note: 设置中心将 Skills 和 MCP 合并到“能力与工具”,并修复水平页签可能被压缩而不可见的问题;尚未实现的工具执行环境暂不显示。
2026-09-02 00:28:47 +08:00
mesalogo 2fca39ddb2 feat: expand deai writing checks
GoodBuddy Agents / Validate Agent source (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
The bundled deai-writing skill now detects additional template patterns,
unsupported attribution, role-slot prose, engineering metaphors, and absolute
claims. Its JSON report also summarizes findings by level and category while
preserving GoodBuddy's cross-platform Python guidance.

Release note: 中文“去 AI 味”审校新增分析套话、模糊归因、角色槽位、工程黑话和绝对化承诺等检查,并提供按分类汇总的扫描结果。
2026-09-01 20:48:58 +08:00
mesalogo 2f1881a414 docs: organize product design by feature
Define repository and feature-scoped documentation rules for PRDs, User Stories, logic, UI, technical design, and progress evidence.

Move feature-owned documents under docs/features, add one entry point per feature, remove the redundant docs index, and update all affected cross-references. Expand the local tool environment design with explicit product, story, logic, UI, technical, and progress documents.
2026-09-01 14:52:22 +08:00
mesalogo dba3380a06 fix: accept bare browser addresses
The Browser workbar required users to enter a complete HTTP(S) URL, so submitting a bare domain or host with a port failed URL validation. Enter and Go now prepend http:// before using the existing browser navigation path.

Explicit protocols remain unchanged so the Main browser URL policy continues to reject unsupported schemes with the correct error.

Release note: 浏览器地址栏现在支持直接输入域名或主机端口,回车或点击“前往”时会自动补全 http://。
2026-09-01 08:54:18 +08:00
mesalogo 77ce13aba3 fix: align the primary sidebar divider
The primary sidebar resize hit target still reserved four pixels in the flex row, leaving a visible gap between the divider and the content frame. Its opposing negative margins now keep the nine-pixel pointer target layout-neutral while preserving the aligned one-pixel divider.

Release note: 修复主侧栏拖拽分隔线与内容边缘未对齐的问题。
2026-09-01 08:37:05 +08:00
mesalogo 5127f7d045 feat: add browser workbar navigation controls
The Browser workbar previously showed the Agent-controlled page but did not let users navigate it directly. It now provides Back, Refresh or Stop Loading, an address field, and Go alongside the existing interaction and close actions.

User and Agent operations share the same per-conversation browser session and serialization path. Loading, committed URL, and history state keep the toolbar accurate, while Stop Loading interrupts page navigation without closing the session or releasing Runtime context.

Direct navigation continues to use the existing built-in-browser capability and URL policy. Closing the browser remains a distinct lifecycle action.

Release note: 浏览器工作栏新增返回、刷新/停止加载、地址栏和前往操作;用户与 Agent 共用同一浏览器会话,停止加载不会关闭会话。
2026-09-01 08:26:57 +08:00
mesalogo 27055693bf feat: add multi-terminal workbar and resizable sidebars
The assistant workbar now uses persistent application tabs with a permanent catalog entry point and supports multiple local or managed SSH terminals bound to the current project. Terminal sessions flow through the Main, preload, and renderer boundaries with bounded output, resize, lifecycle, and cleanup behavior.

The main sidebar and Magic Notes list now support pointer and keyboard resizing. Magic Notes can also hide and restore its list, remembers the chosen layout, and stacks its panes when the available width is narrow.

Closing a terminal tab ends its Shell session. Restored terminal tabs do not restart prior Shell processes and require an explicit reconnect.

Release note: 助手工作栏现可通过“+”打开多个本机或当前远程项目终端;主侧栏和魔法笔记列表支持拖拽调宽,魔法笔记列表可隐藏并记住布局。
2026-09-01 00:49:49 +08:00
mesalogo 73cc55bf75 fix: clarify local tools on remote hosts
The remote host settings page did not explain that locally configured Skills and MCP servers stay on the desktop. It now states that boundary in Chinese and English, with a focused UI regression test.

Document the planned local Node.js and Python environment for local Skills and stdio MCP servers, including managed and custom sources, compatibility migration, lifecycle, remote boundaries, and acceptance criteria.

Release note: 远程主机设置现在明确说明本机 Skills 和 MCP 不会作用于远程主机。
2026-08-31 23:28:41 +08:00
mesalogo 00a231da07 chore: prepare GoodBuddy 0.11.14 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (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
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
Set the Desktop package metadata and approved bilingual notes to 0.11.14,
aligned with the coordinated Agent 0.11.14 lock and release-status docs. This
candidate uses the standard x64/arm64 release matrices and does not include an
experimental LoongArch preview.

Release note: 准备 GoodBuddy Desktop 与 Agent 0.11.14 正式发布,包含托管 SSH 任务恢复、有界诊断和桌面反馈改进。
2026-08-31 16:02:53 +08:00
mesalogo 2833a27f83 feat: add bounded diagnostics and improve UX feedback
Ask and Execute guidance, Runtime state, live announcements, loading states,
retry paths, settings focus, and update failures now provide consistent and
accessible feedback across the desktop experience.

Add bounded redacted Desktop and Agent diagnostic logs, a read-only Agent
diagnostics command, and an explicit opt-in for attaching a small Desktop
diagnostic summary to in-app feedback. Diagnostics stay local by default and
the existing feedback v1 service contract remains compatible.

Release note: 新增可选桌面诊断反馈与 Agent 诊断导出,并统一 Ask/Execute 状态、错误恢复和无障碍反馈;诊断默认保留在本地,仅在用户明确勾选时附加桌面摘要。
2026-08-31 15:35:13 +08:00
mesalogo b1729125e1 fix: allow slower OpenCode startup on Linux ARM
OpenCode health polling restored a ten-second startup limit, causing Linux ARM systems and QEMU environments that became healthy just after that boundary to be reported unavailable. Managed local OpenCode now retains health-based readiness detection while allowing up to thirty seconds for startup.

Release note: 修复部分 Linux ARM 设备切换 OpenCode 时因启动超过 10 秒而误报超时的问题;健康检查现在最多等待 30 秒。
2026-08-31 13:25:49 +08:00
mesalogo 561de29f00 feat: keep managed SSH prompts running on Hosts
Managed SSH prompts previously depended on Desktop-owned provider and transcript state, so accepted work could stop or become uncertain when the Desktop, network, or power disappeared. The Agent now owns accepted OpenCode prompt execution, model and tool rounds, cancellation, and the durable semantic transcript, while Desktop remains authoritative for permanent projects, conversations, tasks, and messages.

Recovered work attaches to the original conversation without replay. The project switcher also groups managed SSH projects by saved Host and shows live Agent connectivity beside each group without opening a connection merely to read status. Remote project subtitles now show only the path, and the Agent lock advances to 0.11.14 with Runtime ACP v4.

Release note: 托管 SSH 任务在桌面端、网络或电源中断后可继续在主机上运行,并能回到原会话精确恢复;项目菜单现按主机分组并显示实时连接状态。
2026-08-31 11:04:41 +08:00
mesalogo f6f8d5e053 fix: release runtime status probes after checks
Chat startup and Runtime switching previously cached status-only Runtime instances, leaving managed OpenCode servers running without active conversations. Status checks now resolve the selected or configured default Runtime through a disposable probe and release it immediately.

Project execution Runtimes remain cached by model selection and execution space, preserving parallel work across different projects.

Release note: 修复启动或切换 Runtime 后空闲 OpenCode 服务持续驻留的问题;状态检查完成后会立即回收,多个项目仍可并行执行。
2026-08-31 01:35:04 +08:00
mesalogo 8c580a9174 fix: preserve remote environment checks across settings pages
The Hosts settings page discarded the last successful Agent and Runtime version check whenever users switched categories or restarted GoodBuddy, forcing them to refresh the same Host again. Validated results now persist locally and remain visible until users refresh, edit or delete the Host, or start a remote environment update.

Release note: 远程主机的 Agent 和 Runtime 版本检查结果现在会在设置页面切换和应用重启后保留,无需重复点击刷新版本。
2026-08-31 01:07:52 +08:00
mesalogo efd304fa1d feat: support native subagents and long-running prompts
GoodBuddy Agents / Validate Agent source (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
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 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
OpenCode Task calls now appear as first-class subagents in local and managed SSH conversations, with stable status, persistence, terminal handling, and true event ordering instead of duplicate generic tool entries.

GoodBuddy no longer applies a fixed outer wall-clock total to OpenCode, Continue, DeepSeek Harness, or managed SSH Prompts. Runtime-owned limits, user cancellation, bounded control operations, output limits, and released Agent protocol compatibility remain intact. Desktop and Agent release metadata are synchronized at 0.11.13 with approved bilingual notes.

Release note: OpenCode 原生 Task 现在以独立子 Agent 展示;长任务不再被 GoodBuddy 固定总时限中止,并修复 OpenCode 启动识别、可配置 HTTP 服务地址与发布说明读取问题。
2026-08-30 17:18:33 +08:00
mesalogo 7c6f15d3c0 fix: allow HTTP for configurable service endpoints
DeepSeek Harness and Weixin binding configuration rejected non-loopback HTTP endpoints even when users explicitly selected those internal services. Configurable endpoints now accept valid HTTP and HTTPS URLs without host-location restrictions, while retaining the required protocol and API-key checks.

Release note: 模型连接、DeepSeek Harness 与微信绑定现在可直接使用局域网或公网 HTTP/HTTPS 服务地址,不再强制公网 HTTPS 或本机回环地址。
2026-08-30 16:49:47 +08:00
mesalogo 6dc219f3a3 fix: detect OpenCode readiness by health
GoodBuddy previously treated one exact OpenCode stdout line as the startup signal, so harmless log formatting differences could report a timeout even when the server was already listening. It now polls the authenticated health endpoint on the loopback port that GoodBuddy selected, with bounded attempts and the original ten-second startup limit.

Release note: 修复 OpenCode 已启动但因监听日志格式未被识别而误报超时的问题;启动检测现在直接使用本机健康接口。
2026-08-30 15:30:43 +08:00
mesalogo acc5a8e2d8 fix: allow slower OpenCode server startup
The bundled OpenCode server could be reported unavailable when slower Linux ARM systems needed more than ten seconds to initialize. GoodBuddy now allows thirty seconds for startup before terminating the server and reports the updated timeout.

Release note: 修复部分 Linux ARM 设备上 OpenCode 冷启动较慢时被误报为启动超时的问题。
2026-08-30 14:42:18 +08:00
mesalogo 9f0f231e3b chore: prepare GoodBuddy 0.11.12 release
GoodBuddy Agents / Validate Agent source (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
GoodBuddy Agent Release / Authorize Agent release (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
Cross-platform packages / Authorize desktop packaging (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-08-30 11:08:55 +08:00
mesalogo 967796539f fix: recover remote conversations after uncertain prompts
An interrupted managed SSH prompt could leave its ACP binding as the permanent owner of the conversation, so every later user message failed with the same uncertain-recovery error. A later explicit prompt now retires that binding and starts a fresh ACP session without replaying the uncertain operation.

Ask-mode Runtime startup also waited on the wrong bubblewrap executable identity. The Agent now supervises the bounded transition to the signed Runtime or model-bridge executable, and the Agent version advances to 0.11.10.

Release note: 修复远程 ACP 请求中断后同一会话无法继续输入,以及 Ask 模式可能在模型调用前启动失败的问题。
2026-08-30 11:02:20 +08:00
mesalogo 8e08ab0bac feat: show Magic Notes update times
The Magic Notes list previously showed only each title, content preview, and
entry count, making similarly named or recently changed notes harder to scan.
Each note now shows its localized update time beside the entry count, with a
semantic timestamp and bounded single-line layout for the narrow list pane.

Release note: 魔法笔记左侧列表现在显示每篇笔记的本地化更新时间,便于快速找到最近修改的内容。
2026-08-30 10:09:39 +08:00
mesalogo edba31e873 chore: prepare GoodBuddy 0.11.11 release
GoodBuddy Agents / Validate Agent source (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 / 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
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Bump the desktop package metadata to 0.11.11 and package the approved Simplified Chinese and English release notes for complete remote conversation saves and discoverable Magic Notes list shortcuts.

Release note: GoodBuddy 0.11.11 修复远程项目长工具结果的会话持久化,并补充魔法笔记列表快捷输入提示。
2026-08-30 09:33:58 +08:00
mesalogo 0804f5ccd2 fix: preserve complete remote tool details
Managed SSH Runtime events could complete successfully and then fail local conversation persistence when tool details exceeded a second, smaller renderer-to-Main schema limit. Conversation persistence now stores the complete tool details already accepted from the Runtime, so long remote results no longer leave the final assistant message stuck in a streaming state.

Release note: 修复远程项目工具结果较长时会话持久化失败的问题;Runtime 已返回的工具详情现在会完整保存到本地会话。
2026-08-30 09:29:28 +08:00
mesalogo 29639ff516 fix: surface Magic Notes list shortcuts
Magic Notes already accepted Quill's native bullet, numbered, and checklist
autofill, but the editor did not make those shortcuts discoverable or protect
them with behavior coverage. The composer now explains the supported markers
in both languages, and tests verify they continue producing Delta list formats.

Release note: 魔法笔记编辑区现在提示项目符号、编号和待办清单快捷输入;输入标记后按空格即可创建对应列表。
2026-08-30 09:20:27 +08:00
mesalogo 22c80b2d94 fix: align Linux Agent verifier regression
GoodBuddy Agents / Validate Agent source (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
The Linux source validator still expected the full verifier's old payload mode
message after entrypoint metadata checks moved ahead of the payload loop. The
test now asserts the actual bounded entrypoint error returned by the optimized
verifier.

The unpublished desktop 0.11.9 notes move to 0.11.10. The Agent lock remains
on the successfully published Agent 0.11.9, and the recovery release still has
no LoongArch preview.

Release note: GoodBuddy 0.11.10 延续已批准的 Host 更新修复;未发布的桌面 0.11.9 由新版本替代。
2026-08-29 23:01:46 +08:00
mesalogo 85e14ca626 fix: make Host updates simple and fast
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (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 / 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
Host repair could fail while adopting the published OpenCode Runtime, then
repeat package and payload verification and carry durable metadata snapshots
for recovery. Host preparation now verifies each acquired compound archive
once, adopts signed Agent and Runtime metadata without rereading payload trees,
and retries failed work by cleaning staging and preparing fresh.

The desktop and Agent versions are advanced to 0.11.9 with the approved
bilingual release notes. Remote Projects remain a disabled-by-default technical
preview, Agent packages remain separate Linux x64/arm64 releases, and this
release has no LoongArch preview.

Release note: 修复 Host 更新采用失败,移除重复远端包体校验,并将中断恢复简化为清理暂存后重新准备。
2026-08-29 22:31:39 +08:00
mesalogo a99b90cc02 fix: make Host environment updates fast and recoverable
GoodBuddy Agents / Validate Agent source (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
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (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 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
Published Host updates could fail while the desktop reverified Runtime against
its bundled lock and repeatedly traversed remote payloads. Host preparation now
verifies and extracts once, adopts Agent and Runtime on one pinned connection,
and uses registered metadata for normal health, capability, and prompt paths.
Interrupted commits and adoption retain bounded recovery and rollback state.

Desktop and Agent versions are 0.11.8. Remote Projects remain a technical
preview; the Agent targets Linux x64 and arm64, with no LoongArch preview.

Release note: 修复正式环境 Host 更新失败并移除重复远端包体扫描;中断恢复与清理失败不再破坏已健康环境。
2026-08-29 17:20:25 +08:00
mesalogo ff4fde282f chore: prepare GoodBuddy 0.11.7 release notes
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 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
Add the approved matching Chinese and English notes for the desktop 0.11.7
release and its separately published Agent 0.11.7 requirement. Remote Projects
remain a technical preview, and no LoongArch preview is included.
2026-08-29 14:03:52 +08:00
mesalogo f327341b00 fix: prevent duplicate queue replay and use cached Agents
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
An interrupted dispatch could leave the same user input both in conversation
history and in the pending queue, causing it to be sent again after restart.
Persisted user messages now retain their queue identity, startup recovery drops
only matching materialized inputs, and conversation branches discard that
ownership marker.

Explicit GoodBuddy transfer now leases a verified local Agent package when the
catalog is unavailable or the local version is at least as new, while a known
newer online package still wins. Prepare both the desktop and Agent locks for
0.11.7; the experimental LoongArch preview remains excluded.

Release note: 修复对话中断后同一条排队输入可能重复发送的问题;GoodBuddy 传输现在可复用已验证的本地 Agent 包,同时不会阻止已知的线上新版。
2026-08-29 13:31:56 +08:00
mesalogo 459cc21e3d fix: make managed SSH sessions fast and resilient
GoodBuddy Agents / Validate Agent source (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
Managed SSH project selection repeatedly activated the remote Host and could
leave idle ACP bindings rejected as stale-controller after a controller
generation change. Host environment updates also coupled project state to
component installation evidence and presented a crowded control surface.

Project selection is now local-only, one Host connection serves compatible
project and conversation callers, idle bindings are safely replaced only
after proven controller takeover, and active prompts keep exact resume/replay.
Remote environment setup now uses explicit signed compound package
preparation with bounded transfer, rollback, recovery, and clearer settings.

Managed Hosts must be updated to separately distributed Agent 0.11.6 before
the daemon-side controller recovery fix takes effect. Desktop packages do not
embed the Agent or remote Runtime, and existing project data is preserved.

Release note: 托管 SSH 项目切换不再重复连接,同一 Host 可顺畅承载多个项目和对话;同时修复重连后的 stale-controller,并优化远程环境安装、更新与状态布局。
2026-08-29 10:58:19 +08:00
lofyerandGitHub eb947213c8 Merge pull request #1 from mesalogo/jojocoding-patch-1
Update README.md
2026-08-27 21:43:15 +08:00
jojokeeandGitHub c2095037ee Update README.md
更新社群二维码
2026-08-27 21:33:33 +08:00
mesalogo 93bf3b908a fix: keep Chinese downloads on OSS loading state
Cross-platform packages / Validate source (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (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
The Chinese download cards briefly exposed the GitHub fallback before OSS index validation completed. They now stay disabled while loading and only show GitHub if the OSS request or validation fails; English downloads remain unchanged.

Release note: 中文官网下载入口加载 OSS 索引期间不再短暂跳转 GitHub,仅在索引失败时回退。
2026-08-27 21:30:38 +08:00
mesalogo 38f6a23306 chore: prepare GoodBuddy 0.11.6 release notes
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agents / Validate Agent 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 / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Add the approved Simplified Chinese and English release notes for sidebar brand customization, independent Anthropic output limits, and restored composer and brand guidance.
2026-08-27 12:56:51 +08:00
mesalogo 12463ad593 feat: add sidebar brand customization
The primary sidebar brand was fixed to the built-in name, subtitle, and logo. Appearance settings now let users preview and save a custom brand name, localized Chinese and English subtitles, and a validated local PNG, JPEG, or WebP logo, with default-logo and full-reset actions.

Customization remains device-local and limited to the top-left brand area; installer, update, Agent, and protocol identities remain GoodBuddy. The desktop package version is now 0.11.6.

Release note: 新增“外观 → 品牌定制”,可修改左上角品牌名称、中英文副标题和本地 Logo,并支持实时预览、恢复默认及本机持久化。
2026-08-27 12:42:26 +08:00
mesalogo 618e505b8b fix: restore the workspace brand subtitle
The primary sidebar showed only the GoodBuddy name after its supporting brand line was removed. It now displays the localized “智能工作台” / “Intelligent Workspace” subtitle beneath the product name again.

Release note: 恢复左上角 GoodBuddy 标题下方的“智能工作台”副标题,使品牌区域的信息层级更加完整。
2026-08-27 10:30:15 +08:00
mesalogo 0a3bf6094a fix: restore composer guidance in input hint
The composer placeholder had been reduced to only the message prompt, hiding send, newline, and paste guidance unless users hovered the field. The full localized keyboard guidance now appears directly in the empty text area again for both chat and image generation.

Release note: 恢复聊天输入框中的发送、换行和粘贴快捷键提示,空输入框会再次直接显示完整操作说明。
2026-08-27 10:14:39 +08:00
mesalogo 957e2ecde7 fix: align homepage feature checklist
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
Wrapped feature rows were centered independently, which shifted the lower-right item away from the column above it. The Chinese homepage now uses a shared two-column grid and collapses to one aligned column on narrow screens.

Release note: 官网首页特性列表现在按列对齐,桌面与移动端布局更整齐。
2026-08-27 08:04:36 +08:00
mesalogo 9316fed3e3 fix: streamline homepage introduction
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
The homepage hero paragraph and page description now focus on GoodBuddy's desktop assistant and Agent Runtime capabilities. Download options and architecture support remain unchanged.

Release note: 官网首页产品介绍现更聚焦桌面助手与 Agent Runtime 能力。
2026-08-27 07:54:04 +08:00
mesalogo d6fea0c6c2 fix: simplify LoongArch download copy
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
The Linux download card now presents only the preview version, DEB format, size, and checksum. Preview index validation and fail-closed download behavior are unchanged.

Release note: 官网龙芯下载信息现仅展示版本、包格式、大小与校验值。
2026-08-27 07:45:03 +08:00
mesalogo d9a554b4b1 feat: add LoongArch preview download to the website
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The Chinese Linux download card now includes a LoongArch processor option and fixes its package type to DEB. It reads a separate strictly validated OSS preview index, while the formal six-target release index and English GitHub downloads remain unchanged.

The page keeps three symmetric platform cards, disables only the LoongArch selection when its index is unavailable, and labels the package as unverified on real LoongArch hardware.

Release note: 官网 Linux 下载新增龙芯 LoongArch 处理器选项,选择后提供独立 DEB 预览包;正式下载与更新索引保持不变。
2026-08-26 23:23:27 +08:00
mesalogo b20c10f820 fix: separate Anthropic output and context limits
Direct Anthropic connections reused the configured context window as max_tokens, which could impose an unintended output value or send a provider-invalid request. Anthropic connections now expose an independent per-response maximum output setting, defaulting to 32K for the protocol-required field, while OpenAI direct requests remain without an added output token cap and direct tool rounds remain unmetered.

Release note: Anthropic 直连模型现在可单独配置单次最大输出,不再复用上下文窗口;OpenAI 直连请求和工具调用轮次不增加额外配额。
2026-08-26 22:53:49 +08:00
mesalogo 49700bc7cf fix: make LoongArch preview checksum portable
Generate SHA256SUMS from inside the timestamped artifact directory so it records only the DEB filename and remains verifiable after copying the preview output elsewhere.
2026-08-26 17:12:20 +08:00
mesalogo fcdcc8e081 fix: finalize 0.11.5 compatibility paths
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 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
Legacy key-only environment deployments retain their historical model endpoint, name, and protocol while generic model variables continue to take precedence. Agent inventory scanning now overlaps the signed catalog check instead of waiting for the network first.

The separate LoongArch preview builder derives node-pty from the candidate lockfile and verifies the installed version before cross-compiling it, preventing stale preview-only pins.
2026-08-26 17:01:49 +08:00
mesalogo de64e9768a fix: make Agent updates reliable and visible
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Valid Agent packages could be rejected when packaged trusted-key JSON used CRLF, and Settings could not show whether a newer compatible Agent existed. Registry formatting is now parsed independently of cryptographic trust, and Settings reads only the signed catalog to report local and latest versions.

New installations use a credential-free local Ollama-compatible profile. The v20 migration changes only untouched historical defaults, while explicit profiles and legacy environment aliases remain compatible. Magic Notes count badges no longer shift navigation rows.

Release note: 修复 Agent 下载兼容性并在设置中显示更新状态;新安装默认连接本地模型,保留显式用户配置;魔法笔记待办数字不再改变侧栏菜单高度。
2026-08-26 15:43:41 +08:00
mesalogo f9137f0463 fix: restore cross-platform release packaging
GoodBuddy Agents / Validate Agent source (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
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 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
The unpublished 0.11.3 candidate could not validate the DeepSeek Harness node-pty prebuilds on every native target, and catalog signing could misread large compound Agent payloads. Release packaging now verifies the rc.8 native layout and writes deterministic stored .gbagent archives with bounded central-directory, mode, CRC, SHA-256, and signature checks.

The 0.11.4 recovery also pins reviewed dependency lifecycle scripts, packages the omitted ONNX Runtime license, and carries the approved unpublished notes forward without changing the immutable failed tags or adding a LoongArch package.

Release note: 修复跨平台桌面包和大文件 Agent 包的发行校验失败,并将未发布的 0.11.3 内容顺延至 0.11.4。
2026-08-26 01:15:34 +08:00
mesalogo 798ac814a9 fix: update bundled DeepSeek Harness to rc.8
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (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 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 / 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
The desktop runtime still pinned the rc.6 Harness family. It now uses the coherent rc.8 package set, adapts the revised image-attachment limits and error contract, and derives release checks from the lockfile. The managed SSH Agent package remains unchanged.

Release note: 内置 DeepSeek Harness 已升级至 0.1.0-rc.8,并适配新版图片附件限制;托管 SSH Agent 包不受影响。
2026-08-25 23:26:43 +08:00
mesalogo 6913e9ce16 fix: stabilize extension startup deadline validation
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
The total-deadline test relied on a timer firing at an exact millisecond boundary, so a shared runner could legally begin the second extension before observing expiration. Use the existing finite synchronous-work fixture to exceed the total budget deterministically while preserving the same production behavior assertion.
2026-08-25 22:30:52 +08:00
mesalogo 9aca3b2627 docs: trim GoodBuddy 0.11.3 release notes
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
Remove the Agent package verification and installation bullet from both language versions while keeping the remaining approved 0.11.3 notes aligned.
2026-08-25 22:19:53 +08:00
mesalogo 0cb7007429 fix: unify GoodBuddy release signing identity
Cross-platform packages / Publish GitHub and OSS release (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
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Use one generic GoodBuddy Ed25519 identity for the Agent bundle, bundled Runtime, outer package, and signed catalog while preserving each layer's existing signature domain.

Rename the production Environment contract to GOODBUDDY_SIGNING_*, register the matching production public key, and keep desktop and branch CI workflows isolated from the private key.
2026-08-25 22:02:47 +08:00
mesalogo bcd347e092 fix: align Unix endpoint tests with platform behavior
GoodBuddy Agents / Validate Agent source (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 / 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
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
Node and libuv remove a bound Unix socket path when the server closes. The removed test required protecting a same-user replacement inside the Agent-owned 0700 directory, which would need disproportionate custom socket publication machinery without crossing a real trust boundary.

Keep the practical live-endpoint and shutdown lifecycle coverage instead.
2026-08-25 21:31:47 +08:00
mesalogo 9c50de521e fix: stabilize Linux release validation
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 Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Ubuntu validation exposed a missing Unix socket test import, a platform-dependent cancellation-message assertion, and a maximum-frame test that could exceed the default timeout under shared-runner load. The tests now exercise the intended behavior consistently without changing product code.
2026-08-25 21:21:58 +08:00
mesalogo da00a850ec fix: report verified Agent CI archives
GoodBuddy Agents / Validate Agent source (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
The native Linux CI builder completed both deterministic package assemblies but read an obsolete nested return field while producing its result JSON. It now reports the archive name from the actual assembleAgentPackage result and guards that shape in workflow tests.
2026-08-25 21:13:46 +08:00
mesalogo fcedaf578e fix: recover managed SSH Agent package release
GoodBuddy Agents / Validate Agent source (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
Linux .gbagent extraction inherited permissive directory modes, so compound packages failed secure Runtime verification on POSIX runners. Package roots and nested bundle directories now use private modes.

The production workflow now uses one GoodBuddy Agent signing identity for the Agent, bundled Runtime, outer package, and catalog. The unpublished 0.11.2 candidate is carried forward as desktop and Agent 0.11.3 without a LoongArch preview.

Release note: 修复 Linux 上 Agent 包解压后无法通过权限验证的问题,并将复合包各层统一为一组 GoodBuddy Agent 发布密钥。
2026-08-25 20:53:28 +08:00
mesalogo 53269f8b1a chore: release GoodBuddy 0.11.2
GoodBuddy Agents / Validate Agent source (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
GoodBuddy Agent Release / Authorize Agent release (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 / 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
Synchronize the desktop package and lockfile versions and package the approved
Simplified Chinese and English release notes for GoodBuddy 0.11.2.

The managed SSH package lock uses the formal Agent 0.11.2 release. Its
independent Agent tag and publication remain separate from the desktop tag,
and this release does not build the experimental LoongArch preview.
2026-08-25 19:30:43 +08:00
mesalogo 9388eb79d4 fix: restore vector model download feedback
Clicking the built-in embedding model download could appear to do nothing while
Main waited for the full transfer to finish. The vector model UI now follows
the speech model workflow with an immediate preparing state, verified source
and package metadata, live progress, cancellation, unavailable-source guidance,
and success notifications.

Use a bounded progress-only IPC instead of overlapping full snapshot polling,
and share model package size and progress presentation across embedding,
speech, and OCR model settings. Promote the GoodBuddy Agent lock from the e2e
prerelease identifier to the formal 0.11.2 version for production publication.

Release note: 修复点击内置向量模型下载后界面无响应的问题;现在会立即显示下载来源、进度与取消操作,并将 GoodBuddy Agent 版本标识转为正式的 0.11.2。
2026-08-25 19:22:51 +08:00
mesalogo 500f7d2735 feat: add experimental LoongArch desktop preview
GoodBuddy can now be cross-packaged as an isolated Debian loong64 preview with verified Electron, node-pty, and Koffi binaries. The preview remains separate from the standard release matrix and excludes Agent, Remote Runtime, OpenCode, and native Canvas until those dependencies support LoongArch.

The Chinese website identifies GoodBuddy as the first open-source desktop AI assistant with LoongArch support while clearly labeling the independently built package as a preview.

Release note: 新增龙芯 LoongArch 独立编译预览版,并提供完整构建文档;当前预览版不包含 Agent、Remote Runtime、OpenCode 和原生 Canvas。
2026-08-25 18:41:35 +08:00
mesalogo e5aebaa67d feat: add independent managed SSH agent packages
Managed SSH previously tied remote Agent and Runtime artifacts to desktop
packaging and Host activation. Settings now manages verified Linux x64 and
arm64 .gbagent packages explicitly, with source-aligned download and update
plus offline import and export. Each package contains the signed Agent, pinned
Node, and compatible OpenCode Runtime, and project activation never downloads
one automatically.

Dedicated native CI and a protected release workflow publish deterministic,
signed cumulative catalogs to GitHub and Beijing OSS without replacing the
desktop Latest release. Desktop packaging is metadata-only, remains independent
of Agent artifacts and signing credentials, and Linux desktop releases add RPM
installers.

The managed SSH path also keeps model credentials in Main, enforces Ask as
read-only, verifies signed Agent and Runtime inputs, preserves ACP recovery, and
reports Host update cancellation and failures accurately.

Release note: 远程项目现可在“设置”中手动下载、更新或离线导入签名 Agent 包,桌面安装包与远程组件发布相互独立;Linux 桌面版同时新增 RPM 安装包。
2026-08-25 18:28:29 +08:00
mesalogo 88658aceaa fix: stabilize knowledge graph extraction for long documents
Long document imports could send oversized graph prompts, surface empty model
responses without useful diagnostics, and fail the entire new document even
when its full-text index was usable. Graph extraction now uses bounded token
batches with retry and timeout handling, preserves successful partial results,
and reports graph failures independently from new-document indexing.

Chinese graph retrieval now recognizes bounded CJK query terms, while graph
search keeps one-hop relationship context visible around matched entities.

Release note: 修复长文档知识图谱抽取易因空响应或超时导致整篇文档导入失败的问题;现在图谱按 Token 分批重试,失败时仍保留新文档全文检索,并改进中文图谱问答和关系上下文展示。
2026-08-25 08:03:15 +08:00
mesalogo 098f21b47a feat: add in-app feedback submission
GoodBuddy previously had no direct way to send feedback from the desktop app. The About & Updates page now opens a bilingual, accessible feedback dialog for problems, feature requests, experience notes, and other feedback, with an optional email address and one screenshot.

Submissions use the narrow preload and trusted IPC boundary, a fixed production endpoint with strict TLS, bounded screenshot processing, an isolated resettable installation ID, preserved drafts, and idempotent manual retries. GoodBuddy does not automatically attach conversations, logs, files, settings, or credentials.

Release note: 现在可在“关于与更新”中直接提交问题、建议与体验反馈;邮箱和截图均可选,提交失败时会保留草稿供重试。
2026-08-25 08:01:42 +08:00
jojokeeandmesalogo efecf88af7 Update README.md
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize production Agent signing (push) Canceled after 0s
Cross-platform packages / Acquire Linux arm64 Agent runtime (push) Canceled after 0s
Cross-platform packages / Acquire Linux x64 Agent runtime (push) Canceled after 0s
Cross-platform packages / Linux arm64 signed Agent bundle (push) Canceled after 0s
Cross-platform packages / Linux x64 signed Agent bundle (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-08-25 00:21:39 +08:00
mesalogo 6c74f057eb feat: gate remote projects as a technical preview
Remote Host and managed SSH project entry points were always exposed while the signed artifact workflow remains preview-stage. Remote Projects now defaults off behind its own Platform Features tab, preserves saved Hosts, projects, and credentials, and returns active remote work to a local project when disabled.

Main IPC enforces the same setting while still allowing in-flight cancellation. Application settings migrate to version 9 with the preview disabled.

Release note: 新增“远程项目(技术预览)”独立页签和开关,默认关闭;启用后显示 SSH Host 与远程项目功能,关闭不会删除已有配置。
2026-08-25 00:16:02 +08:00
mesalogo 607bb33249 feat: add managed SSH execution spaces
Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI.

Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts.

Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
2026-08-24 23:02:18 +08:00
mesalogo 3829a871f5 docs: design in-app feedback integration
Define the About-page feedback workflow, public API boundary, strict Main-process TLS path, screenshot handling, privacy rules, idempotency, and validation plan. The external feedback platform remains separately maintained and the client implementation remains gated on its final production HTTPS origin.
2026-08-24 17:50:47 +08:00
mesalogo 981a174e9f fix: keep Platform Features tabs visible
The Platform Features tab strip could shrink to zero height inside the scrollable Settings layout, leaving the Magic Notes tab present in the DOM but invisible and unusable. Prevent the strip from shrinking and add a CSS regression guard.

Release note: 修复设置中心“平台功能”中的“魔法笔记”页签不可见、无法点击的问题。
2026-08-24 08:30:51 +08:00
mesalogo 1154a6d8da fix: restore essential UI guidance
Settings navigation and several workspace surfaces had lost context that helps users scan, understand, and operate them. Restore concise bilingual descriptions, workflow cues, and visual hierarchy for the welcome message, composer, activity history, knowledge workflows, Magic Notes, assistant workbar, Runtime settings, and channel projects while keeping decorative or duplicate copy removed.

Release note: 恢复设置菜单副标题、新对话引导及知识库、运行记录、魔法笔记和助手工作栏中的必要说明,提升功能可发现性与页面信息层级。
2026-08-23 20:04:56 +08:00
mesalogo 076b3aaa7b feat: add managed Granite embedding models
GoodBuddy previously required an external OpenAI-compatible endpoint for vector retrieval. It can now install, verify, import, remove, and run the built-in Granite multilingual embedding model in an isolated local inference process, while retaining custom embedding connections and migrating existing settings.

The model connection UI now manages built-in and custom vector connections consistently, reports the actual retrieval path, preserves credentials across failed saves, and renders the vector tab synchronously. Related settings copy, switch styling, metadata typography, and renderer bundle guardrails are aligned with the resulting workflow.

Release note: 新增可管理的内置 Granite 多语言向量模型,支持本地下载、离线导入、连接切换与诊断;同时优化向量设置及相关界面的文案、开关和状态显示。
2026-08-23 11:26:44 +08:00
mesalogo 58d774ae0c fix: remove redundant context workbar tab
The assistant workbar duplicated attachment and knowledge-library controls already available in the composer, while its memory section exposed an unimplemented placeholder. The workbar now keeps Task Center, Files, Browser, and Results, with future task context and memory details assigned to Task Center.

Release note: 移除助手工作栏中重复且暂未完整实现的上下文页签;附件与知识库继续在会话输入区管理,工作栏保留任务中心、工作区、浏览器和成果。
2026-08-22 17:50:01 +08:00
mesalogo 7c34abdfbf fix: reveal conversation actions on hover
Conversation rows always reserved space for the more-actions button, and the active row kept it visible. The button now replaces the right-side timestamp only on hover, keyboard focus, or while its menu is open, leaving more room for conversation titles and timestamps by default.

Release note: 会话列表仅在悬停或键盘聚焦时显示更多操作按钮,更新时间默认更靠右,为会话标题留出更多空间。
2026-08-22 15:52:04 +08:00
mesalogo fb51793595 feat: show incomplete Magic Notes todo counts
Magic Notes can now show a default-on incomplete todo badge in the primary
navigation. The badge hides at zero, caps its visual label at 99+ while
retaining the exact accessible count, and refreshes after user or Agent/MCP
note changes.

Platform Features > Magic Notes includes a persistent switch that hides only
the count without changing notes or the Magic Notes entry. Existing settings
migrate with the count enabled.

Release note: 左侧“魔法笔记”入口现在可显示未完成待办数量;可在“平台功能 → 魔法笔记”中关闭,超过 99 项显示“99+”。
2026-08-22 08:32:57 +08:00
mesalogo fee4686750 fix: sync todo completion with source notes
Completing a Magic Notes todo updated the stored checklist but left the cached
source note stale until the workspace was remounted. Todo updates now return
the refreshed source note and update the active note cache immediately for
both completion and reopening.

Release note: 修复在待办视图勾选或取消待办后,源笔记必须切换页面才显示最新状态的问题;现在会即时同步。
2026-08-21 23:27:41 +08:00
mesalogo 63f0f7f0b0 fix: restore full-height primary sidebar
The full-width titlebar pushed the brand, project switcher, navigation, and account controls down from the window edge. The primary sidebar now spans the full window height while the titlebar belongs only to the right content frame, preserving the docked assistant workbar alignment.

Release notes now use the same full-window backdrop as other application modals, including over the titlebar.

Release note: 恢复左侧主导航的全高布局,并让版本更新说明的遮罩完整覆盖顶栏。
2026-08-21 17:20:34 +08:00
mesalogo 97419f3ef7 fix: align assistant workbar controls
Existing titlebar controls could be pushed into a second row, while task controls left uneven or redundant surfaces. The window titlebar is restored, the shared workbar toggle stays directly below the close control, empty conversation task rows disappear, and Task index owns creation plus full-width filters.

Release note: 修复助手工作栏顶部错位与任务入口重复问题;空会话不再显示任务栏,任务索引统一提供新建入口和全宽状态筛选。
2026-08-21 08:43:17 +08:00
mesalogo b29f2d7b0d fix: keep assistant workbar docked and resizable
The right assistant workbar could switch to an overlay, duplicate its close control, and let resizing crowd or clip the chat composer. It now uses one directional toggle below the window controls, remains a docked sibling, and preserves equal 300px minimums for the chat and workbar with symmetric composer gutters.

Release note: 修复右侧助手工作栏遮挡聊天区及拖动边界不一致的问题;工作栏现在使用单一方向按钮,并在左右区域达到相同最小宽度后停止拖动。
2026-08-20 23:58:48 +08:00
mesalogo a0977645a3 fix: enable verified ModelScope speech downloads
Selecting ModelScope could leave supported speech models unavailable because their fixed targets were incomplete and the official LFS redirect host was not allowed. Speech models now resolve complete per-source artifact fingerprints while preserving integrity checks, ZIP compatibility, and source isolation.

Release note: 修复选择 ModelScope 后部分语音模型仍无法下载的问题;六个语音模型现均可从已验证的固定 ModelScope 来源下载。
2026-08-20 23:57:38 +08:00
mesalogo aa339a73d6 feat: add independent conversation branching
Local conversations can now continue in a new branch after stable content is persisted. Each branch uses new conversation and message IDs, keeps its source visible, and remains independent without copying Tasks, queue items, runs, memories, or artifact ownership.

Conversation Task rows are also denser, align status markers at the right edge, and use an accessible checked success marker without redundant completion text.

Release note: 本地会话现在可从“在新会话中继续”创建独立分支并显示来源;Task 子项布局更紧凑,状态统一显示在右侧。
2026-08-20 17:13:57 +08:00
mesalogo 65c3ab9ab0 fix: improve magic notes placeholder contrast
The Magic Notes editor inherited Quill's dark placeholder color, making the hint difficult to read in dark mode. It now uses the shared muted text token in both themes, with regression coverage for the editor style.

Release note: 修复暗色模式下魔法笔记输入提示文字过暗的问题,现在提示内容清晰可读。
2026-08-20 14:31:45 +08:00
1622 changed files with 420283 additions and 50375 deletions
+9
View File
@@ -0,0 +1,9 @@
from pathlib import Path
import difflib
files = ['src/main/assistant/assistant-database.test.ts', 'src/main/assistant/activity-history-io.test.ts', 'src/main/assistant/assistant-storage-upgrade.test.ts', 'src/main/assistant/heartbeat-database.test.ts', 'src/main/magic-notes/magic-note-storage.test.ts']
patch = ''
for name in files:
old = Path(name).read_text(encoding='utf-8')
new = old.replace('DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;', 'DROP VIEW IF EXISTS supervision_review_current;\n DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;\n DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;\n DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;')
patch += ''.join(difflib.unified_diff(old.splitlines(True), new.splitlines(True), fromfile='a/'+name, tofile='b/'+name))
Path('.validation-fixtures.patch').write_text(patch, encoding='utf-8')
+620
View File
@@ -0,0 +1,620 @@
name: GoodBuddy Agent Release
on:
push:
tags:
- 'agent-v*'
permissions:
contents: read
concurrency:
group: agent-release-${{ github.ref }}
cancel-in-progress: false
jobs:
authorize:
name: Authorize Agent release
if: >-
github.ref_type == 'tag' &&
startsWith(github.ref_name, 'agent-v')
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
authorized: ${{ steps.policy.outputs.authorized }}
version: ${{ steps.policy.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Verify immutable Agent release tag
id: policy
shell: bash
run: |
set -euo pipefail
version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
expected="agent-v$version"
test "$GITHUB_REF_NAME" = "$expected"
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = "tag"
test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
git fetch --no-tags origin main
git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main
echo "authorized=true" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.platform }} ${{ matrix.arch }} compound package
needs: authorize
if: needs.authorize.outputs.authorized == 'true'
strategy:
fail-fast: false
matrix:
include:
- platform: linux
arch: x64
runner: ubuntu-24.04
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 25
environment:
name: agent-signing
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify native build target
run: node -e "if(process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Native Agent target mismatch')"
- name: Resolve locked native inputs
id: inputs
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { createHash } = require('node:crypto')
const agent = require('./agent-runtime-lock.json')
const remote = require('./remote-runtime-lock.json')
const node = agent.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
const runtime = remote.runtimes?.opencode
const runtimeKey = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH : `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const opencode = runtime?.targets?.[runtimeKey]
if (
!node ||
!opencode ||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(node.archive) ||
!/^[a-f0-9]{64}$/u.test(node.sha256) ||
!/^[a-z0-9][a-z0-9._-]*$/u.test(opencode.package) ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(opencode.integrity)
) {
throw new Error('Locked native Agent package inputs are invalid')
}
const nodeUrl = new URL(node.archive, agent.node.source)
if (
nodeUrl.protocol !== 'https:' ||
nodeUrl.hostname !== 'nodejs.org' ||
nodeUrl.username ||
nodeUrl.password ||
nodeUrl.search ||
nodeUrl.hash
) {
throw new Error('Agent Node runtime must use the official HTTPS origin')
}
const opencodeArchive =
`${opencode.package}-${runtime.version}.tgz`
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`node-archive=${node.archive}`,
`node-sha256=${node.sha256}`,
`node-url=${nodeUrl.href}`,
`opencode-package=${opencode.package}`,
`opencode-version=${runtime.version}`,
`opencode-integrity=${opencode.integrity}`,
`opencode-archive=${opencodeArchive}`,
`cache-key=${createHash('sha256')
.update(`${node.sha256}:${opencode.integrity}`)
.digest('hex')}`,
''
].join('\n')
)
NODE
- name: Cache verified native inputs
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-release-inputs
key: goodbuddy-agent-release-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
- name: Acquire and verify native inputs
shell: bash
env:
NODE_ARCHIVE: ${{ steps.inputs.outputs.node-archive }}
NODE_SHA256: ${{ steps.inputs.outputs.node-sha256 }}
NODE_URL: ${{ steps.inputs.outputs.node-url }}
OPENCODE_ARCHIVE: ${{ steps.inputs.outputs.opencode-archive }}
OPENCODE_INTEGRITY: ${{ steps.inputs.outputs.opencode-integrity }}
OPENCODE_PACKAGE: ${{ steps.inputs.outputs.opencode-package }}
OPENCODE_VERSION: ${{ steps.inputs.outputs.opencode-version }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-release-inputs"
mkdir -p "$directory"
node_archive="$directory/$NODE_ARCHIVE"
if [[ ! -f "$node_archive" ]]; then
curl --proto '=https' --tlsv1.2 \
--fail --silent --show-error --location \
--retry 3 --retry-all-errors \
--connect-timeout 15 --max-time 300 \
--max-filesize 104857600 \
"$NODE_URL" --output "$node_archive.partial"
mv "$node_archive.partial" "$node_archive"
fi
node - "$node_archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
opencode_archive="$directory/$OPENCODE_ARCHIVE"
if [[ ! -f "$opencode_archive" ]]; then
npm pack \
--ignore-scripts \
--registry https://registry.npmjs.org/ \
--pack-destination "$directory" \
"$OPENCODE_PACKAGE@$OPENCODE_VERSION"
fi
node - "$opencode_archive" "$OPENCODE_INTEGRITY" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync, statSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
const size = statSync(archive).size
if (size <= 0 || size > 536870912) {
throw new Error('OpenCode Runtime archive exceeds its safety limit')
}
const actual = `sha512-${createHash('sha512')
.update(readFileSync(archive))
.digest('base64')}`
if (actual !== expected) {
throw new Error('OpenCode Runtime archive integrity mismatch')
}
NODE
- name: Preflight production GoodBuddy signing identity
shell: bash
env:
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
run: |
set -euo pipefail
node build/agent-bundle.cjs preflight
- name: Build production compound Agent package
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
NODE_ARCHIVE: ${{ steps.inputs.outputs.node-archive }}
OPENCODE_ARCHIVE: ${{ steps.inputs.outputs.opencode-archive }}
run: |
set -euo pipefail
output="$RUNNER_TEMP/goodbuddy-agent-output"
mkdir -p "$output"
continue_archive="$(node build/acquire-continue-runtime.cjs "$RUNNER_TEMP/goodbuddy-continue-runtime" "$AGENT_ARCH" "$AGENT_PLATFORM")"
node build/agent-package.cjs build \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--minimum-desktop-version "$(node -p "require('./package.json').version")" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$NODE_ARCHIVE" \
--runtime-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$OPENCODE_ARCHIVE" \
--continue-archive "$continue_archive" \
--output "$output/goodbuddy-agent-$AGENT_VERSION-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" \
> "$output/result.json"
node - "$output/result.json" "$AGENT_ARCH" "$AGENT_VERSION" <<'NODE'
const { readFileSync } = require('node:fs')
const [resultPath, architecture, version] = process.argv.slice(2)
const result = JSON.parse(readFileSync(resultPath, 'utf8'))
if (
result.descriptor?.architecture !== architecture ||
result.descriptor?.platform !== process.env.AGENT_PLATFORM ||
result.descriptor?.version !== version ||
result.archive !==
`goodbuddy-agent-${version}-${process.env.AGENT_PLATFORM}-${architecture}.gbagent` ||
!/^[a-f0-9]{64}$/u.test(result.sha256)
) {
throw new Error('Production Agent package result is invalid')
}
NODE
- name: Upload verified compound package
uses: actions/upload-artifact@v7
with:
name: goodbuddy-agent-package-${{ matrix.platform }}-${{ matrix.arch }}
path: ${{ runner.temp }}/goodbuddy-agent-output/goodbuddy-agent-*.gbagent
if-no-files-found: error
compression-level: 0
retention-days: 7
catalog:
name: Sign cumulative Agent catalog
needs:
- authorize
- build
if: needs.authorize.outputs.authorized == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
environment:
name: agent-signing
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
- name: Install dependencies
run: npm ci
- name: Download compound packages
uses: actions/download-artifact@v8
with:
pattern: goodbuddy-agent-package-*
path: dist/agent-release
merge-multiple: true
- name: Fetch previous signed catalog when available
id: previous
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
root="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases"
current_catalog_status="$(
curl --silent --show-error \
--output dist/agent-release/previous.json \
--write-out '%{http_code}' \
"$root/v$AGENT_VERSION/agent-catalog.json"
)"
current_signature_status="$(
curl --silent --show-error \
--output dist/agent-release/previous.sig \
--write-out '%{http_code}' \
"$root/v$AGENT_VERSION/agent-catalog.sig"
)"
if [[ "$current_catalog_status" == 200 && "$current_signature_status" == 200 ]]; then
echo "arguments=--previous-catalog dist/agent-release/previous.json --previous-signature dist/agent-release/previous.sig" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$current_catalog_status" != 404 || "$current_signature_status" != 404 ]]; then
echo "Current Agent catalog objects are inconsistent: $current_catalog_status / $current_signature_status" >&2
exit 1
fi
rm -f dist/agent-release/previous.json dist/agent-release/previous.sig
pointer_status="$(
curl --silent --show-error \
--output dist/agent-release/previous-pointer.json \
--write-out '%{http_code}' \
"$root/latest.json"
)"
if [[ "$pointer_status" == 200 ]]; then
readarray -t paths < <(
node - dist/agent-release/previous-pointer.json <<'NODE'
const { readFileSync } = require('node:fs')
const pointer = JSON.parse(readFileSync(process.argv[2], 'utf8'))
if (
pointer?.formatVersion !== 1 ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(pointer.version) ||
pointer.catalog !== `v${pointer.version}/agent-catalog.json` ||
pointer.signature !== `v${pointer.version}/agent-catalog.sig`
) {
throw new Error('Previous Agent catalog pointer is invalid')
}
process.stdout.write(`${pointer.catalog}\n${pointer.signature}\n`)
NODE
)
curl --fail --silent --show-error \
"$root/${paths[0]}" \
--output dist/agent-release/previous.json
curl --fail --silent --show-error \
"$root/${paths[1]}" \
--output dist/agent-release/previous.sig
echo "arguments=--previous-catalog dist/agent-release/previous.json --previous-signature dist/agent-release/previous.sig" >> "$GITHUB_OUTPUT"
elif [[ "$pointer_status" == 404 ]]; then
rm -f dist/agent-release/previous-pointer.json
echo "arguments=" >> "$GITHUB_OUTPUT"
else
echo "Agent catalog pointer request failed: $pointer_status" >&2
exit 1
fi
- name: Sign cumulative Agent catalog
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
PREVIOUS_ARGUMENTS: ${{ steps.previous.outputs.arguments }}
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
run: |
set -euo pipefail
generated_at="$(git show -s --format=%cI "$GITHUB_SHA")"
read -r -a previous <<< "$PREVIOUS_ARGUMENTS"
node build/agent-catalog.cjs create \
--x64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-x64.gbagent" \
--arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-arm64.gbagent" \
--darwin-arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-darwin-arm64.gbagent" \
"${previous[@]}" \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig \
--generated-at "$generated_at"
node build/agent-catalog.cjs verify \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig
- name: Upload signed release set
uses: actions/upload-artifact@v7
with:
name: goodbuddy-agent-release-${{ needs.authorize.outputs.version }}
path: |
dist/agent-release/goodbuddy-agent-*.gbagent
dist/agent-release/agent-catalog.json
dist/agent-release/agent-catalog.sig
if-no-files-found: error
compression-level: 0
retention-days: 7
publish:
name: Publish Agent to GitHub and OSS
needs:
- authorize
- catalog
if: needs.authorize.outputs.authorized == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 25
environment:
name: aliyun-oss-release
permissions:
contents: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
- name: Install verification dependencies
run: npm ci
- name: Download signed release set
uses: actions/download-artifact@v8
with:
name: goodbuddy-agent-release-${{ needs.authorize.outputs.version }}
path: dist/agent-release
- name: Verify signed release set
run: |
node build/agent-catalog.cjs verify \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig
- name: Verify OSS release configuration
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OIDC_PROVIDER_ARN: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
ROLE_ARN: ${{ vars.ALIYUN_ROLE_ARN }}
run: |
set -euo pipefail
test "$OSS_BUCKET" = "goodbuddy"
test "$OSS_ENDPOINT" = "https://oss-cn-beijing.aliyuncs.com"
case "$OIDC_PROVIDER_ARN" in
acs:ram::*:oidc-provider/*) ;;
*) echo "OIDC Provider ARN 无效" >&2; exit 1 ;;
esac
case "$ROLE_ARN" in
acs:ram::*:role/*) ;;
*) echo "RAM Role ARN 无效" >&2; exit 1 ;;
esac
- name: Authenticate to Alibaba Cloud
uses: aliyun/configure-aliyun-credentials-action@v1
with:
role-to-assume: ${{ vars.ALIYUN_ROLE_ARN }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
role-session-name: goodbuddy-agent-${{ github.run_id }}
role-session-expiration: 3600
audience: sts.aliyuncs.com
- name: Install ossutil
shell: bash
run: |
set -euo pipefail
version="2.3.0"
archive="$RUNNER_TEMP/ossutil.zip"
directory="$RUNNER_TEMP/ossutil"
curl --fail --silent --show-error --location \
"https://gosspublic.alicdn.com/ossutil/v2/$version/ossutil-$version-linux-amd64.zip" \
--output "$archive"
mkdir "$directory"
unzip -q "$archive" -d "$directory"
binary="$(find "$directory" -type f -name ossutil -print -quit)"
test -n "$binary"
chmod +x "$binary"
echo "$(dirname "$binary")" >> "$GITHUB_PATH"
- name: Upload immutable Agent release to OSS
id: oss
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_UPLOAD_ENDPOINT: https://oss-accelerate.aliyuncs.com
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
endpoint_host="${OSS_ENDPOINT#https://}"
region="${endpoint_host#oss-}"
region="${region%.aliyuncs.com}"
test "$region" = "cn-beijing"
for file in dist/agent-release/*; do
name="$(basename "$file")"
ossutil cp "$file" \
"oss://${OSS_BUCKET}/agent-releases/v${AGENT_VERSION}/${name}" \
--endpoint "$OSS_UPLOAD_ENDPOINT" \
--region "$region" \
--update
done
echo "region=$region" >> "$GITHUB_OUTPUT"
- name: Verify public immutable OSS packages
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
root="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/v$AGENT_VERSION"
for target in linux-x64 linux-arm64 darwin-arm64; do
name="goodbuddy-agent-$AGENT_VERSION-$target.gbagent"
curl --fail --silent --show-error --location \
"$root/$name" --output "$RUNNER_TEMP/$name"
cmp "dist/agent-release/$name" "$RUNNER_TEMP/$name"
done
for name in agent-catalog.json agent-catalog.sig; do
curl --fail --silent --show-error --location \
"$root/$name" --output "$RUNNER_TEMP/immutable-$name"
cmp "dist/agent-release/$name" \
"$RUNNER_TEMP/immutable-$name"
done
- name: Publish non-Latest GitHub Agent release
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="agent-v$AGENT_VERSION"
title="GoodBuddy Agent $AGENT_VERSION"
notes="resources/agent-release-notes/$AGENT_VERSION.md"
test -s "$notes"
if gh release view "$tag" >/dev/null 2>&1; then
is_draft="$(gh release view "$tag" --json isDraft --jq .isDraft)"
if [[ "$is_draft" == true ]]; then
gh release edit "$tag" \
--title "$title" \
--notes-file "$notes" \
--latest=false
gh release upload "$tag" dist/agent-release/* --clobber
gh release edit "$tag" --draft=false --latest=false
else
verify="$RUNNER_TEMP/github-agent-release"
mkdir "$verify"
gh release download "$tag" --dir "$verify"
for file in dist/agent-release/*; do
cmp "$file" "$verify/$(basename "$file")"
done
fi
else
gh release create "$tag" \
--draft \
--verify-tag \
--title "$title" \
--notes-file "$notes" \
--latest=false
gh release upload "$tag" dist/agent-release/* --clobber
gh release edit "$tag" --draft=false --latest=false
fi
test "$(gh api repos/mesalogo/goodbuddy/releases/latest --jq .tag_name)" != "$tag"
- name: Point OSS Agent catalog to published release
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_REGION: ${{ steps.oss.outputs.region }}
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
node - "$AGENT_VERSION" > dist/agent-release/latest.json <<'NODE'
const version = process.argv[2]
process.stdout.write(`${JSON.stringify({
formatVersion: 1,
version,
catalog: `v${version}/agent-catalog.json`,
signature: `v${version}/agent-catalog.sig`
}, null, 2)}\n`)
NODE
ossutil cp dist/agent-release/latest.json \
"oss://${OSS_BUCKET}/agent-releases/latest.json" \
--endpoint "$OSS_ENDPOINT" \
--region "$OSS_REGION" \
--force
- name: Verify public Agent catalogs
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
mirror="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases"
github="https://github.com/mesalogo/goodbuddy/releases/download/agent-v$AGENT_VERSION"
curl --fail --silent --show-error --location \
"$mirror/latest.json" --output "$RUNNER_TEMP/mirror-pointer.json"
curl --fail --silent --show-error --location \
"$mirror/v$AGENT_VERSION/agent-catalog.json" \
--output "$RUNNER_TEMP/mirror.json"
curl --fail --silent --show-error --location \
"$mirror/v$AGENT_VERSION/agent-catalog.sig" \
--output "$RUNNER_TEMP/mirror.sig"
curl --fail --silent --show-error --location \
"$github/agent-catalog.json" --output "$RUNNER_TEMP/github.json"
curl --fail --silent --show-error --location \
"$github/agent-catalog.sig" --output "$RUNNER_TEMP/github.sig"
cmp dist/agent-release/latest.json "$RUNNER_TEMP/mirror-pointer.json"
cmp dist/agent-release/agent-catalog.json "$RUNNER_TEMP/mirror.json"
cmp dist/agent-release/agent-catalog.sig "$RUNNER_TEMP/mirror.sig"
cmp dist/agent-release/agent-catalog.json "$RUNNER_TEMP/github.json"
cmp dist/agent-release/agent-catalog.sig "$RUNNER_TEMP/github.sig"
+355
View File
@@ -0,0 +1,355 @@
name: GoodBuddy Agents
on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/agents.yml'
- 'agent-runtime-lock.json'
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
- 'package-lock.json'
- 'package.json'
- 'resources/agent-release-keys.json'
- 'remote-runtime-lock.json'
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
push:
branches:
- main
paths:
- '.github/workflows/agents.yml'
- 'agent-runtime-lock.json'
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
- 'package-lock.json'
- 'package.json'
- 'resources/agent-release-keys.json'
- 'remote-runtime-lock.json'
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
permissions:
contents: read
concurrency:
group: agents-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
name: Validate Agent source
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Validate Agent source and workflow
run: |
npm test -- \
tests/agent-bundle.test.ts \
tests/agent-darwin-build.test.ts \
tests/agent-package.test.ts \
tests/remote-runtime-bundle.test.ts \
tests/agents-workflow.test.ts
npm run typecheck
build:
name: Build ${{ matrix.platform }} ${{ matrix.arch }} Agent
needs: validate
strategy:
fail-fast: false
matrix:
include:
- platform: linux
arch: x64
runner: ubuntu-24.04
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify native runner target
run: node -e "if (process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Agent CI requires the native target')"
- name: Resolve locked official Node runtime
id: runtime
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { lock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const target = lock.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
if (
!target ||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(target.archive) ||
!/^[a-f0-9]{64}$/u.test(target.sha256)
) {
throw new Error('Locked Agent Node runtime metadata is invalid')
}
const url = new URL(target.archive, lock.node.source)
if (
url.protocol !== 'https:' ||
url.hostname !== 'nodejs.org' ||
url.username ||
url.password ||
url.search ||
url.hash
) {
throw new Error('Agent Node runtime must use the official HTTPS origin')
}
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`archive=${target.archive}`,
`sha256=${target.sha256}`,
`url=${url.href}`,
''
].join('\n')
)
NODE
- name: Cache verified Node runtime input
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-node
key: goodbuddy-agent-ci-node-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
- name: Acquire and verify locked Node runtime
shell: bash
env:
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
NODE_SHA256: ${{ steps.runtime.outputs.sha256 }}
NODE_URL: ${{ steps.runtime.outputs.url }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-node"
archive="$directory/$NODE_ARCHIVE"
mkdir -p "$directory"
if [[ ! -f "$archive" ]]; then
rm -f "$archive.partial"
curl --proto '=https' --tlsv1.2 \
--fail --silent --show-error --location \
--retry 3 --retry-all-errors \
--connect-timeout 15 --max-time 300 \
--max-filesize 104857600 \
"$NODE_URL" --output "$archive.partial"
mv "$archive.partial" "$archive"
fi
node - "$archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
- name: Resolve locked OpenCode Runtime
id: opencode
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { createHash: hash } = require('node:crypto')
const { runtimeLock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const runtime = runtimeLock.runtimes?.opencode
const key = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH
: `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const target = runtime?.targets?.[key]
if (
!runtime ||
!target ||
!/^[a-z0-9][a-z0-9._-]*$/u.test(target.package) ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(runtime.version) ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(target.integrity)
) {
throw new Error('Locked OpenCode Runtime metadata is invalid')
}
const archive = `${target.package}-${runtime.version}.tgz`
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`package=${target.package}`,
`version=${runtime.version}`,
`integrity=${target.integrity}`,
`archive=${archive}`,
`cache-key=${hash('sha256').update(target.integrity).digest('hex')}`,
''
].join('\n')
)
NODE
- name: Cache verified OpenCode Runtime input
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-opencode-runtime
key: goodbuddy-agent-ci-opencode-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
- name: Acquire and verify locked OpenCode Runtime
shell: bash
env:
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
OPENCODE_INTEGRITY: ${{ steps.opencode.outputs.integrity }}
OPENCODE_PACKAGE: ${{ steps.opencode.outputs.package }}
OPENCODE_VERSION: ${{ steps.opencode.outputs.version }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-opencode-runtime"
archive="$directory/$OPENCODE_ARCHIVE"
mkdir -p "$directory"
if [[ ! -f "$archive" ]]; then
npm pack \
--ignore-scripts \
--registry https://registry.npmjs.org/ \
--pack-destination "$directory" \
"$OPENCODE_PACKAGE@$OPENCODE_VERSION"
fi
node - "$archive" "$OPENCODE_INTEGRITY" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync, statSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
const size = statSync(archive).size
if (size <= 0 || size > 536870912) {
throw new Error('OpenCode Runtime archive exceeds its safety limit')
}
const actual = `sha512-${createHash('sha512')
.update(readFileSync(archive))
.digest('base64')}`
if (actual !== expected) {
throw new Error('OpenCode Runtime archive integrity mismatch')
}
NODE
- name: Run native Agent regressions
if: matrix.platform == 'linux'
run: |
npm test -- \
src/agent-daemon/model-bridge.test.ts \
src/agent-daemon/lifecycle.test.ts
- name: Run native macOS process regressions
if: matrix.platform == 'darwin'
run: npm test -- src/agent-daemon/darwin-process-identity.test.ts
- name: Build and verify ephemeral signed compound Agent package twice
id: agent
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-ci"
rm -rf "$directory"
mkdir -p "$directory"
agent_version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
continue_archive="$(node build/acquire-continue-runtime.cjs "$RUNNER_TEMP/goodbuddy-continue-runtime" "$AGENT_ARCH" "$AGENT_PLATFORM")"
node build/agent-ci-bundle.cjs \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-node/$NODE_ARCHIVE" \
--opencode-archive "$RUNNER_TEMP/goodbuddy-opencode-runtime/$OPENCODE_ARCHIVE" \
--continue-archive "$continue_archive" \
--output-directory "$directory/bundle" \
--archive "$directory/goodbuddy-agent-$agent_version-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" |
tee "$directory/result.json"
node - <<'NODE'
const { appendFileSync, readFileSync } = require('node:fs')
const result = JSON.parse(
readFileSync(
`${process.env.RUNNER_TEMP}/goodbuddy-agent-ci/result.json`,
'utf8'
)
)
if (
result.architecture !== process.env.AGENT_ARCH ||
result.platform !== process.env.AGENT_PLATFORM ||
!/^[a-f0-9]{64}$/u.test(result.archiveSha256)
) {
throw new Error('Agent CI result is invalid')
}
appendFileSync(
process.env.GITHUB_OUTPUT,
`sha256=${result.archiveSha256}\n`
)
NODE
- name: Record verified Agent build
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_SHA256: ${{ steps.agent.outputs.sha256 }}
run: |
{
echo "### $AGENT_PLATFORM $AGENT_ARCH Agent build"
echo
echo "Native compound Agent, Node, and OpenCode Runtime build, test-signature verification, launch smoke, and deterministic archive comparison passed."
echo
echo "Archive SHA-256: \`$AGENT_SHA256\`"
echo
echo "The ephemeral CI key and bundle are not published."
echo "Native build checks do not replace real Host installation, Attach, and Ask/Execute acceptance."
} >> "$GITHUB_STEP_SUMMARY"
+114 -14
View File
@@ -19,14 +19,14 @@ jobs:
validate:
name: Validate source
runs-on: ubuntu-24.04
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Verify release tag version
@@ -40,9 +40,27 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Prepare sandboxed Electron tests
run: |
sudo apt-get update
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
force_no_cache=true node node_modules/electron/install.js
fi
test -f "$sandbox"
sudo chown root:root "$sandbox"
sudo chmod 4755 "$sandbox"
- name: Run validators
run: |
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
@@ -50,7 +68,9 @@ jobs:
run: npm run build:bundle
- name: Upload production bundle
if: github.event_name == 'workflow_dispatch' || github.ref_type == 'tag'
if: >-
github.ref_type == 'tag' ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v7
with:
name: goodbuddy-production-bundle
@@ -58,10 +78,67 @@ jobs:
if-no-files-found: error
retention-days: 1
authorize-release:
name: Authorize desktop packaging
if: >-
github.ref_type == 'tag' ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
authorized: ${{ steps.policy.outputs.authorized }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Verify immutable release ref policy
id: policy
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin \
"+refs/heads/main:refs/remotes/origin/main"
git merge-base --is-ancestor \
"$GITHUB_SHA" refs/remotes/origin/main
case "$GITHUB_EVENT_NAME" in
push)
test "$GITHUB_REF_TYPE" = "tag"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).created)")" = "true"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).forced)")" = "false"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).deleted)")" = "false"
expected="v$(node -p "require('./package.json').version")"
test "$GITHUB_REF" = "refs/tags/$expected"
test "$GITHUB_REF_NAME" = "$expected"
git fetch --no-tags origin \
"+refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}"
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = "tag"
test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
;;
workflow_dispatch)
test "$GITHUB_REF" = "refs/heads/main"
test "$GITHUB_REF_TYPE" = "branch"
test "$GITHUB_SHA" = "$(git rev-parse refs/remotes/origin/main)"
;;
*)
echo "Desktop packaging is limited to immutable version-tag pushes or a manual run of refs/heads/main." >&2
exit 1
;;
esac
echo "authorized=true" >> "$GITHUB_OUTPUT"
package:
name: ${{ matrix.platform }} ${{ matrix.arch }}
if: github.event_name == 'workflow_dispatch' || github.ref_type == 'tag'
needs: validate
if: >-
needs.authorize-release.result == 'success' &&
needs.authorize-release.outputs.authorized == 'true'
needs:
- validate
- authorize-release
strategy:
fail-fast: false
matrix:
@@ -71,7 +148,7 @@ jobs:
runner: windows-2025
- platform: windows
arch: arm64
runner: windows-2025
runner: windows-11-vs2026-arm
- platform: macos
arch: x64
runner: macos-15-intel
@@ -92,7 +169,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Cache packaging toolsets
@@ -115,6 +192,23 @@ jobs:
name: goodbuddy-production-bundle
path: out
- name: Verify native runner architecture
run: node -e "if(process.arch !== '${{ matrix.arch }}'){throw new Error('Expected ${{ matrix.arch }} runner, received '+process.arch)}"
- name: Install and validate managed Python
run: npm exec vitest run tests/local-tool-environment-live.test.ts
env:
GOODBUDDY_RUN_LIVE_TOOL_ENVIRONMENT: '1'
- name: Install RPM packaging tools
if: matrix.platform == 'linux'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install --no-install-recommends --yes rpm
rpmbuild --version
- name: Resolve macOS signing mode
id: macos-signing
if: matrix.platform == 'macos'
@@ -172,7 +266,7 @@ jobs:
- name: Build, sign and notarize macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
@@ -185,7 +279,7 @@ jobs:
- name: Build unsigned macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'false'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build --unsigned
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
@@ -229,8 +323,13 @@ jobs:
release:
name: Publish GitHub and OSS release
if: github.event_name == 'push' && github.ref_type == 'tag'
needs: package
if: >-
github.event_name == 'push' &&
github.ref_type == 'tag' &&
needs.authorize-release.outputs.authorized == 'true'
needs:
- authorize-release
- package
runs-on: ubuntu-24.04
timeout-minutes: 35
environment:
@@ -247,7 +346,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
- name: Verify release tag
shell: bash
@@ -364,6 +463,7 @@ jobs:
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_UPLOAD_ENDPOINT: https://oss-accelerate.aliyuncs.com
OSS_REGION: ${{ steps.oss-release.outputs.region }}
run: |
set -euo pipefail
@@ -378,7 +478,7 @@ jobs:
name="$(basename "$file")"
ossutil cp "$file" \
"oss://${OSS_BUCKET}/releases/${GITHUB_REF_NAME}/${name}" \
--endpoint "$OSS_ENDPOINT" \
--endpoint "$OSS_UPLOAD_ENDPOINT" \
--region "$OSS_REGION" \
--update
done
+7
View File
@@ -1,11 +1,18 @@
node_modules/
out/
dist/
# Local build output left by the extracted ShareServer project.
shareserver/build-server/
coverage/
/temp/
.vite/
.runtime-resources/
.remote-runtime-resources/
.agent-resources/
*.log
*.tsbuildinfo
__pycache__/
*.py[cod]
.env
.env.*
!.env.example
+258
View File
@@ -0,0 +1,258 @@
--- a/src/main/assistant/assistant-database.test.ts
+++ b/src/main/assistant/assistant-database.test.ts
@@ -174,7 +174,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 35;`)
legacy.close()
try {
@@ -325,7 +328,10 @@
DROP VIEW supervision_review_current;
DROP TABLE supervision_review_navigation; DROP TABLE supervision_review_batches;
DROP TABLE supervision_review_sources; DROP TABLE supervision_review_runs;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 41;`)
legacy.close()
try {
@@ -1088,6 +1094,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 3;
`)
@@ -1251,6 +1260,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 26;
`)
@@ -1367,6 +1379,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 30;
`)
@@ -1445,6 +1460,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 30;
`)
@@ -1542,6 +1560,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1589,6 +1610,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1637,6 +1661,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1681,6 +1708,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1714,6 +1744,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1744,6 +1777,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 5;
`)
@@ -1871,6 +1907,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 21;
COMMIT;
@@ -1967,6 +2006,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 9;
`)
@@ -2025,6 +2067,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 16`)
legacy.close()
@@ -2977,6 +3022,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 23`)
legacy.close()
@@ -3141,6 +3189,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 18;
`)
@@ -3654,6 +3705,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 31;
`)
--- a/src/main/assistant/activity-history-io.test.ts
+++ b/src/main/assistant/activity-history-io.test.ts
@@ -27,6 +27,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version=43`)
const legacySave = raw.prepare('UPDATE activity_history SET records_json = ?, legacy_history_may_be_incomplete = 1')
--- a/src/main/assistant/assistant-storage-upgrade.test.ts
+++ b/src/main/assistant/assistant-storage-upgrade.test.ts
@@ -101,7 +101,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 37`)
} finally { legacy.close() }
expect(hasPendingAssistantStorageUpgrade(path)).toBe(true)
@@ -419,7 +422,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = ${sourceVersion}; BEGIN`)
const insert = legacy.prepare(
`INSERT INTO task_events(
--- a/src/main/assistant/heartbeat-database.test.ts
+++ b/src/main/assistant/heartbeat-database.test.ts
@@ -82,6 +82,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 2`)
raw.close()
@@ -141,6 +144,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 20`)
raw.close()
--- a/src/main/magic-notes/magic-note-storage.test.ts
+++ b/src/main/magic-notes/magic-note-storage.test.ts
@@ -223,6 +223,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 37`)
rmSync(join(directory, 'notes'), { recursive: true })
+5
View File
@@ -0,0 +1,5 @@
--- a/src/renderer/src/App.test.tsx
+++ b/src/renderer/src/App.test.tsx
@@ -13972 +13972 @@
- supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 30, executionSeconds: 300 }
+ supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 30, executionSeconds: 300, responseKiB: 1024 }
+254 -213
View File
@@ -4,9 +4,72 @@
These instructions apply to the entire repository.
GoodBuddy is a secure, cross-platform Electron desktop assistant. It uses
Electron, React, TypeScript, Vite, Vitest, and SQLite. User-facing copy is
primarily Simplified Chinese.
GoodBuddy is a cross-platform Electron desktop assistant for internal
organizational networks. It uses Electron, React, TypeScript, Vite, Vitest, and
SQLite. User-facing copy is primarily Simplified Chinese.
## Product Priorities
GoodBuddy is primarily an internal-network product. Unless the user states
otherwise, assume a trusted organizational environment, not a hostile public,
multi-tenant, or zero-trust deployment.
Use this order when requirements or implementation choices compete:
1. Complete, working product functionality.
2. Measured performance and responsiveness.
3. Clear, low-friction user experience.
4. Simple, conventional, maintainable implementation.
5. Security controls only for a concrete risk in the actual deployment model.
Functionality and performance are the primary acceptance criteria. Do not
disable, delay, restrict, or complicate a working product path for speculative
security hardening. Preserve the existing Electron, credential, user-data, and
release-signing boundaries, but do not expand them without a concrete current
risk or an explicit user requirement.
## Design Simplicity
Follow KISS and YAGNI. Use the smallest conventional design that completes the
real user workflow. NO over-engineering or over-design.
- A demonstrated failure authorizes only the smallest proportional fix, not a
general recovery, compatibility, or security framework.
- Prefer deleting bounded staging and retrying, idempotent operations, and
reconciliation from one authoritative source of truth. Do not persist copies
of state that can be derived or safely retried.
- Do not add durable snapshots, journals, versioned internal receipts, rollback
engines, compatibility readers, multi-phase state machines, protocol phases,
cryptographic identities, trust tiers, consent ceremonies, or quotas when
cleanup, retry, or reconciliation is safe.
- Before introducing any of that machinery, stop before coding. Explain the
reproducible current failure, why the simple alternative is insufficient,
and obtain the user's explicit approval for the added complexity. A reviewer
suggestion, test-only scenario, hypothetical crash, or possible future need
is not sufficient evidence.
- Performance outranks simplicity only for a measured bottleneck. Security
complexity requires a concrete threat at an actual trust boundary.
- Ignore hypothetical threats that require a hostile public network,
untrusted tenants, or zero-trust deployment unless that environment is an
explicit product requirement. Do not turn such findings into implementation
work.
- A complete workflow means the normal production path plus demonstrated
failure cases. It does not require exhaustive recovery from every
hypothetical interruption point.
- Test Hosts are test environments. Do not turn their workflows into production
compliance exercises.
When the user asks to implement a feature, deliver the complete usable user
workflow unless the user explicitly requests a prototype, scaffold, staged
landing, or partial implementation. Internal contracts, managers, protocols,
disabled UI, unavailable catalog entries, placeholders, and tests around
injected fakes do not count as completion when the real product path still
cannot be used. If required metadata, dependencies, or integration details are
missing, investigate and resolve them as part of the implementation. Do not
turn missing implementation work into a product "security gate", permanent
unavailable state, or acceptance criterion. If a genuine external blocker
cannot be resolved, stop and ask the user rather than presenting the partial
work as the implemented feature.
## Architecture
@@ -19,7 +82,8 @@ primarily Simplified Chinese.
- `build`: packaging scripts and icons.
- `out` and `dist`: generated output. Change source files instead.
Keep Electron security boundaries intact:
Keep these existing Electron security boundaries intact. They do not authorize
additional hardening beyond the actual internal-network threat model:
- Preserve context isolation and sandboxing.
- Never enable renderer Node integration.
@@ -29,11 +93,29 @@ Keep Electron security boundaries intact:
## Runtime Behavior
- Ask mode must remain read-only at the runtime boundary.
- Execute mode may use tools only through the existing approval controls.
- Normal requests use the selected Runtime, enabled capabilities, and execution
account permissions without Ask/Execute or the product `toolApproval` policy.
Do not restore a product work mode or general tool approval gate.
Implementation and validation status belongs
in `docs/features/unified-execution/progress.md`.
- Respect explicit user instructions such as explanation-only requests. Keep
dedicated internal text tasks tool-free. Do not add T2/T3 trust tiers,
separate consent checklists, per-tool approvals, or a replacement work mode.
- Preserve cancellation, timeout, bounded-output, and shutdown behavior.
- Treat OpenCode and Continue as untrusted child runtimes. Preserve environment
allowlists, sandbox checks, and per-tool approval enforcement.
- When fixing Runtime behavior or limits, also inspect the remote Runtime path
through GoodBuddy Agent (`gbagent`). Verify whether local and remote execution
actually share the affected control logic; do not assume that a desktop fix
covers the Agent, bridge, launcher, or bundled Runtime. Fix any affected
remote-specific implementation and validate it under the GoodBuddy Agent
development validation rules below. If no separate remote fix is needed,
record which shared implementation covers it or why the remote path is
unaffected.
- Keep model/provider credentials in Main, but do not otherwise reduce execution
permissions with extra product policy gates. Keep child-process cleanup
straightforward and reliable.
- On test Hosts, GoodBuddy may install and start its own test resources and
create dedicated work directories. Never delete or overwrite unrelated Host
files.
- A successful image-model configuration check does not prove generation works.
Only an actual generation request verifies the provider path.
- Do not fetch provider-returned image URLs. Accept and validate bounded inline
@@ -41,7 +123,16 @@ Keep Electron security boundaries intact:
## Data and Compatibility
- Preserve existing user data and migrations.
- Before changing released database schemas or large-data migrations, read
[`docs/development/database-migrations.md`](./docs/development/database-migrations.md).
Keep feature-specific storage rules and validation evidence in the owning
feature documents.
- Preserve released user-owned data, migrations required by released data, and
supported external contracts.
- Compatibility does not apply to unshipped, branch-local, or safely disposable
internal operation records. Replace or discard those records instead of
adding migrations, legacy readers, versioned receipts, or recovery machinery.
- Do not weaken SQLite transaction, lifecycle, deduplication, or cleanup logic.
- Treat user workspaces and untracked files as user-owned.
- Keep Windows, macOS, Linux x64, and Linux arm64 behavior in mind.
@@ -49,10 +140,39 @@ Keep Electron security boundaries intact:
## 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.
- Reviewer findings do not expand task scope. Implement a finding only when it
identifies a reachable current failure, a released compatibility contract,
or reproducible user-visible harm. Record speculative concerns without
building machinery around them.
- Validate the real end-to-end path, not only schemas, mocks, injected test
doubles, or isolated infrastructure. A feature is not complete until its
normal UI action reaches the production implementation and produces the
intended result.
- Security work must remain proportional to a concrete risk and must not
replace missing product functionality. Use established platform and
repository mechanisms instead of inventing extra gates, ceremonies, or
disabled states.
- Add or update focused tests for behavioral changes and regressions.
- Read [`docs/quality/recurring-defects.md`](./docs/quality/recurring-defects.md)
before changing overlays, store lookups, retrieval entry points, cancellation
behavior, or render-time derived values. It records defect types that have
already recurred in this repository and the shared mechanism that covers each
one. When fixing an instance of a listed type, extend that shared mechanism
instead of patching the one reported case, and add an entry when a new defect
type recurs.
- After completing any functional change, inspect the affected product,
architecture, design, feature, setup, and operational documentation and
update every relevant document to match the implemented behavior. Treat the
@@ -64,6 +184,22 @@ Keep Electron security boundaries intact:
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
focus states.
## Product Design Documentation
- Organize feature documentation by product feature under
`docs/features/<feature-name>/`, not by document type.
- Treat each feature directory's `README.md` as its documentation entry point.
- Follow `docs/features/AGENTS.md` for required document responsibilities,
naming, traceability, progress evidence, and migration rules.
- Keep cross-feature architecture, quality protocols, development procedures,
and product-wide roadmaps outside feature directories only when they cannot
be owned by one feature.
- Maintain one authoritative definition for each requirement, state rule, UI
behavior, technical contract, and implementation fact. Link to it instead of
duplicating it across documents.
- Update the affected feature documents after functional changes. The
implemented and validated production behavior is the final source of truth.
## UI Consistency
- Treat `UI-DESIGN.md` as the canonical UI design system. Read and follow it
@@ -131,213 +267,70 @@ mode.
Release note: 修复左上角项目设置与消息通道项目设置不一致的问题;现在从任一入口修改后,另一处会同步显示相同配置。
```
## Release Packaging
## Releases
- `.github/workflows/packages.yml` is the canonical cross-platform packaging
workflow. It validates and builds `out` once, then packages on six native
runners: Windows, macOS, and Linux, each for x64 and arm64.
- Run the unified packager with
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG and ZIP for
macOS, and AppImage and DEB for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
- Keep electron-builder invocations on `--publish never`. Main-branch builds
run validation and build the production bundle without running the native
package matrix. Manual builds upload 30-day GitHub Actions artifacts.
Version-tag builds verify and aggregate packages before publishing GitHub
Release assets. The macOS jobs sign, notarize, and verify packages when all
five Apple credentials are configured. With no Apple credentials they must
use the explicit `--unsigned` path, warn that Gatekeeper may block the
packages, and still complete; a partial credential set must fail rather than
silently downgrade.
- Keep `ELECTRON_CACHE` and `ELECTRON_BUILDER_CACHE` under
`${{ runner.temp }}` in step-level workflow contexts. A cache beneath the
repository inherits the root `"type": "module"` and breaks electron-builder's
CommonJS macOS icon tool.
- Tag builds must use `v${package.version}`. The workflow also supports manual
dispatch and main-branch changes to release tooling.
Detailed release operations are governed by
[`docs/development/release-runbook.md`](./docs/development/release-runbook.md).
Read that runbook only when preparing, publishing, recovering, or verifying a
desktop or GoodBuddy Agent release. It is an operator and CI procedure, not an
authorization to add product persistence or recovery machinery.
### Tagged Release Process
Repository-wide release invariants:
Every version-tag release must follow this sequence. A branch-only push does
not require release notes.
1. Confirm that the user wants a release tag and identify the exact release
commit and the new `package.json` version.
2. Find the latest stable version tag reachable before the release commit and
inspect the complete commit and file diff from that tag to the release
commit. For the first tagged release, inspect the relevant repository
history instead.
3. Draft concise, user-facing release notes in both Simplified Chinese and
English based only on verified changes in that range. Use the titles
`GoodBuddy <version> 更新内容` and
`What's New in GoodBuddy <version>`, with corresponding `功能更新` /
`Features` and `问题修复` / `Bug Fixes` sections when applicable. The two
language versions must describe the same changes. Do not expose
internal-only details, credentials, private content, or unverified claims.
4. Show the exact bilingual release-note draft to the user and wait for
explicit approval. If the release commit or either language version changes
after approval, inspect the updated tag range and request approval again.
5. Write the approved notes to the single entry for the release version in
`resources/release-notes.json`. A failed unpublished candidate whose content
is carried forward must not retain a duplicate packaged entry.
6. Verify that `package.json`, the root `package-lock.json` version, and
`package-lock.json.packages[""].version` all equal the release version. Run
`npm run release:notes:verify`, the required source validators, the
production build, and any native candidate launch probe available on the
current host. The six native CI jobs remain the cross-platform authority.
7. Fetch both remotes immediately before tagging. Inspect any remote branch
movement instead of overwriting or silently merging it. Confirm the working
tree is clean, the candidate tag is unused locally and remotely, and the
exact approved commit has not changed.
8. Only after all previous steps pass, create an annotated
`v${package.version}` tag at the exact approved commit. Push `main` to
`origin` and `github`, verify both branch SHAs, then push the tag to both
remotes and verify each peeled tag SHA (`refs/tags/<tag>^{}`) equals the
release commit.
9. Keep both approved language versions as the single source for the GitHub
Release body and the packaged first-open release-notes modal. The modal
displays the release notes matching the current interface language and
contains no button linking to a full release page.
10. Observe the tag workflow through publication and complete the public
verification checklist below. A successful push is not a completed
release.
### OSS Publication Contract
The tagged release job publishes through the GitHub Environment selected by
`.github/workflows/packages.yml` (`aliyun-oss-release`) and reads the following
effective GitHub Actions variables. Before a release or same-tag rerun can
publish, verify that repository-, organization-, or environment-level
resolution exposes:
- `ALIYUN_OSS_BUCKET=goodbuddy`
- `ALIYUN_OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com`
- a non-empty `ALIYUN_OIDC_PROVIDER_ARN` matching
`acs:ram::*:oidc-provider/*`
- a non-empty `ALIYUN_ROLE_ARN` matching `acs:ram::*:role/*`
For environment-scoped values, use the exact Environment name from the
workflow; do not assume a similarly named UI environment such as `Production`
contains the active variables.
The Bucket and Endpoint are a deployment contract, not interchangeable
examples. They must stay aligned with the trusted URL checks in
`src/main/version-checker.ts`, `sites/app.js`, website validation, and related
tests. A host, Bucket, Region, or CDN migration must update and validate every
surface together before a new release.
- Use GitHub OIDC and the RAM Role to obtain short-lived STS credentials.
Never add long-lived AccessKeys to repository or environment secrets.
- Keep `ossutil` pinned. Its V4 signing requires the Region; derive it from the
canonical Endpoint, verify that the production value resolves to
`cn-beijing`, and pass `--region` to every `ossutil cp`, including the final
`latest.json` update.
- Grant the RAM Role only the actions and prefixes required by the workflow.
It must be able to write immutable version objects and the final latest
pointer without granting unrelated administration privileges.
- Upload release assets and `site-release.json` under the immutable
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
creating or publishing the GitHub Release. Update
`releases/latest.json` only after the GitHub Release is public and all prior
checks succeeded.
- The expected GitHub Release contains 20 assets: 12 installers (two formats
for each of six platform/architecture targets), six renamed target
manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
`site-release.json` is an OSS publication artifact, not a GitHub Release
asset.
### Failed Tag Recovery
Classify a failed tag by the external side effects that completed before
choosing a recovery:
- If no source or release metadata must change, correct only the external
configuration and use **Re-run failed jobs** for the same immutable tag. Do
not change, move, delete, or recreate the tag.
- If immutable OSS objects were partially uploaded but their source bytes are
unchanged, a same-tag rerun may idempotently re-upload or verify them. Never
point `latest.json` at a partially verified prefix.
- If the GitHub Release is already public but the final latest-pointer step
failed, it is a published version. Keep its packaged notes and rerun the
failed release job for the same tag; do not classify it as an unpublished
candidate.
- If code or release metadata must change, keep the failed tag immutable,
increment the patch version, obtain approval for the revised exact release
commit and notes, and create a new tag. Do not reuse the failed version.
When recovering from a version tag whose workflow never published a public
GitHub Release:
- If a code or metadata change requires a higher version and a new tag, carry
the failed candidate's approved user-facing notes forward into the recovery
version, then remove the superseded failed version's entry from
`resources/release-notes.json`.
- The packaged first-open modal must show that carried-forward content only
once under the recovery version. Never retain both the failed version and
its cumulative recovery copy, because users upgrading across them would see
duplicate content.
- Never remove the packaged history for a version that successfully published
a GitHub Release. Verify the failed release state before treating an entry as
superseded.
### Post-Publication Verification
Do not report a release complete until all of the following are verified:
1. The tag workflow and all six native package jobs succeeded. Verify the
recorded macOS signing mode: signed builds must pass `codesign`, `spctl`,
and `stapler`; unsigned builds must record the Gatekeeper caveat in the
Actions log and job summary. In the final release job, explicitly verify
the OSS configuration, OIDC authentication, release-index generation,
immutable upload, public asset check, GitHub Release publication, and
latest-pointer steps.
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
uses the expected tag and title. Its body must exactly match the Markdown
generated from the approved packaged bilingual notes.
3. The GitHub asset set has exactly the expected 20 names and every asset is
uploaded. Compare installer sizes and SHA-256 digests with the aggregate
manifest and `SHA256SUMS`.
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
version, exact six targets and 12 installer entries, the trusted Beijing
URLs, and the GitHub fallback URL. It must match the immutable
`releases/<tag>/site-release.json`.
5. All 12 public installer URLs accept `HEAD` without redirects and report the
declared size. For small JSON/checksum metadata, prefer a `GET` byte and
digest comparison; OSS may gzip JSON responses and omit an uncompressed
`Content-Length` on `HEAD`.
6. The live website successfully fetches the index and produces the 12 correct
platform/architecture/format links. Exercise the application's actual
mirror checker against the public index for all six targets.
7. Both remote `main` refs and both peeled tag refs still equal the approved
release commit, and the local working tree is clean.
Never create or push a release tag, and never push a previously created
release tag, before the release-note draft has received explicit approval.
- Before a push that updates the `github` remote, ask whether the user wants a
release tag unless they already specified that choice. A branch-only push
does not require a version bump or tag. When the user requests a release,
verify that `package.json` and `package-lock.json` contain the same release
version, create `v${package.version}` at the exact commit being pushed, and
push that tag so the native package matrix and GitHub Release run.
- Never move or reuse an existing release tag. If `v${package.version}` already
exists locally or on a remote at another commit, increment the package
version and create a new matching tag before the release push.
- Verified release baseline on 2026-08-18: commit
`60119a4317118fa3f077db0382664f15266a6682`, annotated tag `v0.10.4`, and
GitHub Actions run `32038633609` attempt 2 succeeded through all six native
packages, GitHub Release publication, Beijing OSS publication, and the final
`latest.json` switch.
- Write each release-note feature item as two or three clear sentences that
identify the user situation, the available action, and the resulting
behavior or important boundary. Do not impose an arbitrary per-item
character limit; use the detail needed to make the user scenario clear.
- Before approving the exact release commit and bilingual release notes,
inspect the English `FEATURES.md` and Simplified Chinese
`FEATURES.zh-CN.md` against the verified changes since the previous stable
release. Keep both versions aligned, and update their provided capabilities,
roadmap status, current versions, limitations, and remaining release
acceptance work as needed. Include any required update in the release
candidate; if no change is needed, explicitly confirm that both feature lists
were reviewed and remain accurate.
- Never create or push a release tag before the user approves the exact release
commit and bilingual release notes.
- Never move, delete, recreate, or reuse a release tag.
- Keep desktop and Agent release paths, tags, artifacts, and Latest status
separate.
- GitHub Actions is the release build and cross-platform packaging authority.
During release preparation, run local tests, type checks, and lint, but do
not run `npm run build`, `npm run build:bundle`, local packaging, or
packaged-app probes. Push the approved candidate commit first and require its
main-branch CI validation and production build to pass before tagging it,
unless the user explicitly requests an additional local build.
- LoongArch is a separate experimental preview. Build or publish it only when
the user explicitly requests it for that release.
- Never commit or publish credentials. Production publication uses the existing
protected environments and short-lived credentials described in the runbook.
## Validation
Run all validators after source changes:
### 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
npm test
@@ -345,14 +338,62 @@ npm run typecheck
npm run lint
```
Run `npm run build` for production build changes. Use `npm run portable` only
when a current Windows portable package is requested. Gated runtime tests may
make paid or external calls, so run them only with explicit authorization.
Run `npm run build` for production build changes during development, but not
solely as a release-preparation step; release candidates use the main-branch CI
production build before tagging. Use `npm run portable` only when a current
Windows portable package is requested. The user has granted standing
authorization in this repository for bounded real text-model calls needed to
validate changed product paths; do not ask again for each such call. Keep each
validation call minimal, disable tools and attachments where possible, and
report the exact call count. This standing authorization does not cover bulk,
high-cost, destructive, publishing, messaging, purchasing, or other
consequential external actions.
### GoodBuddy Agent development validation
Every source change that can alter the deployed GoodBuddy Agent or the
desktop-to-Agent production path must be validated on the shared Linux x64 test
Host while the change is being developed. This includes Agent daemon and
protocol changes, packaging and installation, attach/update behavior, Runtime
launch and process ownership, workspaces, model bridges, and lifecycle or
recovery behavior. Unit tests, mocks, fixtures, CI, and waiting for release
validation are not substitutes for this development-time check.
- Reach the shared Host at `192.168.0.23` on the LAN when available, otherwise
use `10.7.0.23` over VPN. These are two routes to the same physical machine,
not two independent test targets or architecture results.
- Exercise the current source or package under development, not only the
previously published Agent. Run the affected production path as soon as it is
runnable and repeat the relevant scenario after the final change.
- Select the real-Host scenario from the changed surface: attach/bootstrap or
update for installation changes; unified tool execution and a bounded real
model request for Runtime or model-bridge changes; and the applicable disconnect, restart,
cancellation, or reconnection sequence for lifecycle and recovery changes.
- Use the existing pinned Host identity and GoodBuddy credential storage. Never
add credentials to source, documentation, commands, logs, or test output, and
never hard-code these test addresses into product behavior or defaults.
- Keep all artifacts in GoodBuddy-owned test directories. Before stopping or
changing a remote process, verify that it belongs to the test run; never
modify unrelated Host files or processes.
- During remote testing, prefer dedicated test directories under `/root/tmp`
for temporary files. Avoid creating files under `/tmp` where possible, since
it may be memory-backed and consume additional RAM. Set `TMPDIR` to the
dedicated test directory when needed so test tools use it as well.
- If neither route is reachable, report the real-Host validation as blocked and
do not present the Agent work as complete or defer its first real validation
to release time.
The detailed scenario rules are documented in
[`docs/features/remote-host/technical-design.md`](./docs/features/remote-host/technical-design.md).
Before committing or pushing, inspect `git status`, `git diff`, and
`git diff --cached`. Do not commit secrets, local databases, logs, generated
credentials, or private user artifacts.
Before a push that updates the `github` remote, ask whether the user wants a
release tag unless they already specified that choice. A branch-only push does
not require a version bump or tag.
This repository has two synchronized remotes, `origin` and `github`. Unless the
user explicitly names a remote, every requested push must update the current
branch on both remotes. When the user requests a release tag, push the new tag
+354 -30
View File
@@ -31,6 +31,10 @@ npm start
## 质量验证
跨功能测试方法见[质量验证索引](./docs/quality/README.md)。桌面界面的测试分层、
原生键盘、截图与计算样式取证、隔离启动和脱敏报告遵循
[Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md)。
提交或打包前运行:
```bash
@@ -39,6 +43,31 @@ npm run typecheck
npm run lint
```
Vitest runs two ordered projects from `vitest.config.ts`. The `unit` project runs
first with up to four workers, bounded by Node's available CPU parallelism. Once
it finishes, the `integration` project runs with one worker. The serial group
contains all test files under `tests/` (including `*.electron.test.ts`),
`src/main/agent/**/*runtime*.test.ts`, and
`src/agent-daemon/private-endpoint.test.ts`. Other tests remain in the unit group.
Both projects inherit the React plugin, jsdom environment, renderer setup, and
existing exclusions; test-file isolation remains enabled.
This preserves serialization for the process/socket tests that previously timed
out on Linux with two workers (commit `93721d6`). `sequence.groupOrder` keeps the
two phases from overlapping. Existing file filters still work, for example
`npm test -- src/main/agent/opencode-runtime.test.ts`. To select a whole phase,
use `npm test -- --project unit` or `npm test -- --project integration`. To inspect
file selection without running tests, use `npx vitest list --filesOnly`.
For activity-history persistence, run
`npx vitest run src/main/assistant/activity-history-io.test.ts`. This checks both
WAL growth and repeated full-history reads: consecutive saves compare against
the last committed in-memory records, invalidated on close, data clearing or
another SQLite connection's commit. It also checks rollback and retry. The cache
does not change the database schema or the full-snapshot IPC contract. WAL size
and SQL scan counts do not measure physical disk throughput or conversation
message persistence; validate those separately when investigating live I/O.
监听模式:
```bash
@@ -56,6 +85,14 @@ OpenCode/Continue 用例默认读取 `dist/harness-package-probe/win-unpacked`
调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环
broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。
不使用外部模型的 Runtime 原生交互回归位于
`src/main/agent/opencode-runtime-permissions.test.ts`、
`src/main/agent/continue-runtime-permissions.test.ts` 和
`src/main/agent/deepseek-harness-acp-e2e.test.ts`。它们用本地确定性模型驱动真实 Runtime
或固定 Host,覆盖工作区外访问、原生问答及能力范围。OpenCode 用例默认使用开发缓存
二进制;可通过 `GOODBUDDY_TEST_OPENCODE_BINARY` 指定已经安装的候选二进制,避免把
仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。
## 生产构建
生成 Electron Main、Preload 和 Renderer 生产文件:
@@ -66,6 +103,13 @@ npm run build
中间构建输出位于 `out`。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。
Obsidian 接入的构建钩子将锁定的 npm `@bitbonsai/mcpvault@0.16.0` 及递归运行依赖、
许可证和原生辅助资源复制到 `out/main/obsidian-mcpvault`,桌面打包时解包到
`app.asar.unpacked`。运行时复用现有工具执行环境的 Node,不执行在线 npm 安装。
生产组件自动化、Electron 托管 Node、Windows 设置与聊天 UI,以及受限网络下的
Windows x64 解包应用已验证;物理断网、安装器及其他系统验收待完成。
验证记录见 [Obsidian 进度](./docs/features/obsidian/progress.md)。
## 图标生成
应用图标源文件位于 `icons`。修改源图或图标处理逻辑后运行:
@@ -127,15 +171,117 @@ npm run dist:linux:arm64
每个 Linux 架构生成:
- `deb`:适用于麒麟、统信 UOS 等 Debian 系桌面。
- `rpm`:适用于 Fedora、RHEL、Rocky Linux、openEuler 等 RPM 系桌面。
- AppImage:适用于免安装验证和便携运行。
打包产物位于 `dist`。
所有桌面打包入口通过同一个 `electron-builder` 启动脚本运行。正常日志会隐藏
`duplicate dependency references` 信息摘要,因为它只表示同一已解析依赖被多条
生产依赖路径共享,并不要求构建操作者处理;调试日志仍会保留该摘要。Renderer 中
已由 Vite 打包的 `@antv/g6` 归类为构建期依赖,不会再作为独立 Node 模块复制进桌面
安装包。
Desktop packaging includes only `out/main`, `out/preload`, and `out/renderer`;
validation builds elsewhere under `out` are not shipped. Renderer-only React,
Xterm, Mermaid, Fabric, Quill, i18next, and Markdown/math dependencies are build-time
dependencies: Vite ships their browser bundles, while the existing license
resources remain included. Install development dependencies before building;
do not prune them before bundling and copying license resources.
The desktop DS Web resource filter excludes JavaScript/TypeScript source maps,
`.d.ts` / `.d.mts` / `.d.cts` declarations, and `.pdb` debug symbols. It preserves
the installed dependency layout, plugin configuration, browser assets, licenses,
native binaries, and non-code `.map` assets. This filter does not change the
separately built remote Agent or Runtime payloads.
Windows x64 development validation on 2026-09-28 compared the existing 0.15.2
portable directory (excluding `data`) with a fresh unpacked test build:
`app.asar` decreased from 609.4 to 350.5 MiB, and DS Web decreased from 28,701
files / 463.4 MiB to 14,984 files / 357.0 MiB. Total measured size decreased from
1,758.3 to 1,438.4 MiB despite the test build retaining 45.6 MiB more Electron
locales than the portable build. These are unpacked sizes, not installer download
sizes. Builder-copy regression tests, production build/type checks, packaged
Harness/OpenCode/npm smoke checks, DS Web authentication/page scripts/workspace
creation, and packaged App/settings/notes-page loading passed. Full editor/canvas
interaction and other platform packages were not revalidated in this check.
以上 `dist*` 与 `portable` 命令用于普通本地桌面打包。它们会携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,但所有桌面构建路径(包括本地 `portable`)都不会嵌入
可安装的远端 Agent 或远端 Runtime payload。没有使用托管 SSH 时,无需下载任何额外
组件,缺少远端包也不能阻塞普通桌面打包或发布。
“设置 > 平台功能 > 远程项目”列出用户数据目录中已经下载或导入的
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
用户也可导入或导出 `.gbagent` 离线包。
Main 在写入缓存前校验签名目录、目录签名、大小、SHA-256、桌面最低版本、Agent 协议、
内部 Agent/Runtime 签名和完整 payload。缓存位于 Electron `userData` 下的
`remote-components/agent-packages`,不暴露给 Renderer。
公钥 registry 的信任来自严格 schema、唯一 key ID、规范 Base64、有效 Ed25519 key、
production/test 环境和撤销列表,不来自 JSON 空白或换行。读取桌面资源、复合包和 Host
已有 registry 时接受等价 UTF-8 JSON 格式;向 Host 写入前统一重新序列化为确定性 LF
JSON。签名目录、包描述符、manifest、签名和 payload 的原始字节校验保持不变,包下载
继续流式计算 SHA-256。
“SSH 主机 > 远程运行环境”只有一个按版本事实显示“安装远程环境”“更新远程环境”或
“重新安装”的主按钮。次级 SegmentedControl 选择默认且不持久化的“自动”、“Host 下载”
或“GoodBuddy 传输”:
- “Host 下载”使用签名目录中的固定 GitHub/北京镜像 URL、大小、SHA-256、
有界重定向链和固定 SSH prepare channel。GoodBuddy 桌面控制面通过该通道发送有界
one-shot installer;Host 校验目录绑定的归档大小和 SHA-256 后,由包内固定 Node 解码
并运行 installer。
- 显式“GoodBuddy 传输”在在线目录不可用,或本地已验证包版本不低于在线候选时优先使用
本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。需要下载时,
同一次用户操作会校验 SHA-256 与签名、缓存并取得 lease,再以有界流式 SFTP 上传一个
compound archive 和归档内已验证的 bootstrap Node;约 294 MiB 的整包不会一次读入
Main `Buffer`,Host 仍再次校验。
- “自动”只在 operation/prepare 前执行 Host capability probe;直连明确可用才选择
Host 下载,否则选择 GoodBuddy 传输。显式选择保持有效,prepare、commit 或 adoption
失败后不跨 acquisition 自动 fallback。
两种 acquisition 都把同一签名 compound `.gbagent` 交付到固定 operation staging,随后
共用 control-plane `prepare → commit → Agent activate/health → Runtime activate →
finalize → cleanup`。Main 只持久化清理 operation staging 所需的 operation ID,不保存
Agent/Runtime metadata 副本。操作中断或 adoption 失败后,下一次更新尽力清理旧暂存并
重新 prepare,不恢复或续写旧操作。确认 adoption 后显式 cleanup。Host 在 prepare
解包时完成一次完整 payload 校验;commit 只复核签名 metadata
并原子发布,Agent/Runtime 注册后的 health、capabilities 和 prompt 启动不再遍历或哈希
完整 payload。cleanup 失败不回滚已健康环境,也不阻塞下一次 fresh prepare。
进入“主机与远程执行”设置页只读取本地 Host 列表,不逐台连接。用户点击某台 Host 的
“刷新版本”后才执行远端版本和直连准备能力探测。项目切换只更新本地选择,不建立 SSH
连接;首次实际使用远程 Workspace 或 Runtime 时才建立连接。当前进程会复用该 Host 已确认
的 Agent/Runtime identity 和 Agent 连接,Host 编辑或环境更新会定向清空它们。
直连安装本身只依赖 SSH,不依赖已有 Agent。显式选择 Host 下载时由实际安装流程重新探测
并报告工具、权限、空间或来源不可达等原因;本次操作不会自动改用 GoodBuddy 传输。
新建远程项目弹窗也只读本地 Host 验证记录;目录浏览或保存才连接所选 Host。Host 卡片
在版本相同时仍允许显式重装;损坏的 GoodBuddy-owned 同 digest 目录会先隔离、替换并在
失败时恢复。直连 bootstrap 使用固定短命令 `exec sh -s`,脚本和有界输入经 stdin 发送,
避免 SSH command 长度限制。
新增 Host 只保存并探测,不提供自动安装开关;只有用户明确点击主按钮后才传输完整包。现有
package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host 直连;归档无需携带
`agent/lib/package-installer.cjs`,签名目录不提供也不检查额外的 bootstrap 能力元数据。
首次 bootstrap 不依赖已安装 Agent daemon。目标契约见
[SSH Host 远程环境准备与直连下载设计](./docs/features/remote-host/environment-provisioning-technical-design.md)。
删除 Host 或远程项目属于纯本地管理操作。Host 确认框列出全部关联项目记录并级联清理
本地数据库,但不连接 Host、不检查远端目录,也不删除远端文件;Host 设置文件和项目事务
的普通失败会回滚已写入的一侧。
## Runtime 资源
发布包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
- DSH 插件安装使用精确锁定并从 `app.asar` 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
- DSH 图片输入使用精确锁定的 `@napi-rs/canvas` 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 `app.asar` 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
@@ -143,12 +289,162 @@ npm run dist:linux:arm64
跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。
GoodBuddy 管理的本机 OpenCode 启动后会轮询已认证的 loopback health 接口,最多等待
30 秒。该窗口用于兼容 Linux ARM 等较慢环境;不要改回依赖 stdout 文案或 10 秒固定上限。
聊天中的 Runtime 切换只执行路径和模型凭据就绪检查,不为状态显示启动临时 Server。
设置中的显式连接测试、原生能力清单读取和首次实际请求仍会启动 OpenCode,并继续使用
上述完整健康检查。
## GoodBuddy Agent 工件
面向用户发布的是按远端 Host 架构区分的复合 `.gbagent`,每个包同时包含签名
Agent、固定 Node 和当前桌面源码维护并适配的签名 OpenCode Runtime。Runtime 不建立
独立的用户版本流。远程直连所需的有界 one-shot installer 由桌面控制面打包并通过固定
SSH prepare channel 发送,不是 `.gbagent` 的必需条目。Host 按签名目录校验完整归档
大小和 SHA-256 后,包内固定 Node 解码并运行 installer。两种 acquisition 都在固定
operation staging 中执行同一 prepare/commit;本地 pending record 只保存清理暂存所需的
operation ID。失败或中断后的下一次更新尽力清理旧暂存并重新 prepare,不保存或恢复
Agent/Runtime metadata 副本。生产构建命令为:
```bash
node build/agent-package.cjs build \
--arch <x64|arm64> \
--minimum-desktop-version <desktop-version> \
--node-archive <locked-node-runtime.tar.gz> \
--runtime-archive <locked-opencode-package.tgz> \
--output goodbuddy-agent-<agent-version>-linux-<arch>.gbagent
```
底层 `agent:build/import/verify` 与 `remote-runtime:build/import/verify` 命令继续用于
开发、签名和复合包组装验证;其 `.agent-resources` 与
`.remote-runtime-resources` 输出都是不提交的生成内容,也不会被任何桌面打包命令发现
或嵌入。
### Agent 原生 CI 策略
Agent 源码、共享协议、lock、bundle 工具和测试与桌面应用保持在同一仓库、同一 commit;
不维护长期分叉的 Agent 源码分支。`.github/workflows/agents.yml` 在相关 Pull Request、
`main` push 和手工触发时,分别使用 `ubuntu-24.04` 与 `ubuntu-24.04-arm` 原生构建
Linux x64/arm64 复合包,并使用 `macos-15` 原生构建 Darwin arm64
复合包。所有 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
测试签名验证、Agent CLI 加载 smoke 和确定性 `.gbagent` 对比。所有目标还验证
Koffi 原生模块加载与 OpenCode `--version`;Linux job 继续运行 Agent 原生回归。
Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份
`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 提供,不再维护 CI 专用副本。
组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为
`goodbuddy-agent-<version>-darwin-arm64.gbagent`,不构建 Intel Mac 目标。
macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。
进程身份使用 macOS 原生接口,Unix Socket 使用 `getpeereid`;Host 下载兼容系统自带
`shasum -a 256`,不要求安装 GNU coreutils。真实 Host 验证记录见
[远程主机技术设计](./docs/features/remote-host/technical-design.md)。
三个目标共用 `agent-catalog.json` 和现有签名格式,条目按平台与架构区分。旧版 Desktop
只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版
Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败,
本地已验证包不被删除。不得为此改写历史包或复用发布标签。
该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传
可安装 Agent 工件。`.github/workflows/agent-release.yml` 是唯一 production Agent
发布路径:只接受指向受保护 `main` 历史的 annotated
`agent-v${agent-runtime-lock.agentVersion}` 标签,在 `agent-signing` Environment 中
原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:
- `GOODBUDDY_SIGNING_KEY_ID` /
`GOODBUDDY_SIGNING_PRIVATE_KEY`
该 GoodBuddy 身份统一签署内部 Agent、固定 Runtime manifest、外层 `.gbagent`
描述符和累计目录,各层通过既有签名域区分用途,不再要求内部 Runtime 单独配置生产
身份。私钥只注入签名步骤;预检只读取 key ID 并确认公钥已在
`resources/agent-release-keys.json` 注册为 production 且未撤销。
累计目录使用 `agent-catalog.json` 与 `agent-catalog.sig`。每个条目绑定 Agent 版本、
最低桌面版本、Agent 协议、远端 OpenCode 版本/digest、架构、文件名、大小、SHA-256
以及固定 GitHub/OSS URL。同一版本和架构的字节不可改变;目录最多保留 200 个条目。
OSS 先写 `agent-releases/v<version>/` 不可变对象,GitHub Release 公开且验证后才更新
单一 `agent-releases/latest.json` 指针。该指针只引用同一不可变版本目录中的
`agent-catalog.json` 与 `agent-catalog.sig`,避免客户端观察到跨版本组合。Agent Release 必须使用
`--latest=false`,不得改变桌面 Release 的 Latest 标记。
### 远程 Runtime 工件
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64
baseline 与 arm64 官方包 integrity、`bin/opencode` 入口和固定 `acp` 参数。它不同于
`agent-runtime-lock.json`,后者锁定 Agent 自带 Node 以及用于 Linux `SO_PEERCRED`
的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标
Linux 架构的 glibc/musl 原生 binding;构建与导入都会校验其 ELF 架构和 MIT 许可证。
lock 同时固定 `@continuedev/cli@1.5.47` 的 npm integrity。CN 工件使用
`lib/continue/dist/cn.js`,由 Agent 中的 HTTP-to-ACP helper 启动,不能传入原生 `acp`
参数。`remote-runtime:build` 与 `build/agent-package.cjs build` 接受
`--runtime-id continue`;默认 Agent 组包通过 `--continue-archive` 同时携带 OC/CN。
CN 的 JS payload 通过文件摘要与签名校验,执行它的 Node 仍来自 Agent 固定包。
默认 workflow 已接入联合组包;开发包与完整桌面到 Linux 的实测见
[CN 部署记录](./docs/features/remote-host/runtime-checklist-validation.md#cn-部署与生命周期)。
远程 Runtime 工件由以下命令独立管理:
```bash
# 在对应 Linux 原生 Runner 上从 lock 指定的 npm 归档构建并签名
npm run remote-runtime:build -- --arch <x64|arm64> \
--runtime-archive <locked-opencode-package.tgz>
# 为复合 Agent 包组装导入并完整验证签名归档
npm run remote-runtime:import -- --arch x64 --archive <runtime-x64.tar>
npm run remote-runtime:import -- --arch arm64 --archive <runtime-arm64.tar>
```
默认生成目录为 `.remote-runtime-resources/linux-<arch>/opencode/<digest>`,
不提交到仓库。生产 workflow 从 `remote-runtime-lock.json` 解析固定包名、版本和
integrity,以 `--ignore-scripts` 从官方 npm registry 获取归档;bundle 工具在签名前
再次验证归档 SHA-512、包名、版本和 ELF 架构。
源码已经实现 Runtime bundle build/import/verify、Daemon 侧
manifest/Ed25519/payload/ELF/lock 校验、digest registry、runtime/acp 6、直接进程 ownership 和
`runtime/model-bridge` v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖
systemd、D-Bus、Linger 或 bubblewrap。请求直接启动签名 Runtime,使用所选 SSH 账号
权限与已接入能力;不携带产品工作模式。旧 capability 5 不兼容,发布前须重建配套工件。
OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型
网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。helper 的 loopback
HTTP 入口使用每个进程随机生成的路径 capability,Anthropic/OpenAI SDK 需要的固定非秘密
本地兼容标记由 helper 核对后丢弃。GoodBuddy 自己维护会话标题,所以生成的 OpenCode
配置必须禁用 title Agent;工具循环只使用 build Agent 的模型轮次。
ACP 权限中介按活动请求与工具能力回复合法允许选项,保留取消及未知请求拒绝。Unix 模型桥
broker 必须按 socket 当前已缓冲字节增量读取长度帧,不能等待 `read(remaining)` 一次返回
完整大响应。Agent 传输或权限实现变化后,除了普通测试,还要在原生 Linux 上运行包含
至少 256 KiB 响应的模型桥回归。
`agent-runtime-lock.json` 维护独立于桌面应用的 Agent 版本,
`remote-runtime-lock.json` 维护 OpenCode Runtime 版本。Agent 内容变化时必须更新
`agentVersion`;Runtime 更新时使用 Runtime 自身版本。签名 manifest 或 bundle digest
唯一标识对应版本的精确工件。变更后必须重新构建、签名并导入 Linux x64/arm64 两套工件;
旧版本或旧 digest 会按设计校验失败。不要手工把本机 `.runtime-resources` 当作远程 bundle。
桌面应用更新后不会在项目切换时下载或安装 Agent。用户先在 SSH Host 的远程运行环境
卡片显式选择直连下载或 GoodBuddy 传输。托管 SSH 项目只保存 Host ID、远端路径、
Runtime 选择和默认模式;创建或保存时验证 Host 当前环境,普通项目切换只读取这些本地
配置。首次实际使用 Workspace 或 Runtime 时解析 Host current identity;同一进程内后续
项目复用已确认的 Agent/Runtime identity 和 Agent 连接。该流程不取得安装包、不重读完整
payload,也不发布组件。
两条手动准备路径都支持阶段进度和取消;成功后定向失效该 Host 的连接与 Runtime 缓存,
引用项目下一次打开自然使用新的 current 环境。验收应覆盖两种准备方式与项目当前环境
验证入口,并确认失败或取消不会删除 Host 配置、凭据、项目设置、Workspace 文件、全局
可信元数据或仍可用的旧安装。
## 跨平台 CI 与 GitHub Release
`.github/workflows/packages.yml` 是统一发布工作流。它先验证并生成一次
`out` 生产 bundle,再在六个原生 Runner 上分别打包 Windows、macOS 和
Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifact
供打包任务复用,不会上传到 GitHub Release。
供打包任务复用,不会上传到 GitHub Release。该工作流不构建、下载、签名或嵌入
远端 Agent/OpenCode 复合包,也不依赖 `agent-signing` Environment;远端包缺失或
Agent 发布失败不会阻塞任何桌面目标。
本地构建单个平台目标:
@@ -156,14 +452,21 @@ Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifac
npm run release:package -- --platform <windows|macos|linux> --arch <x64|arm64>
```
`release:package` 仅构建桌面产品。基础配置统一携带
`agent-release-keys.json`、`agent-runtime-lock.json` 与 `remote-runtime-lock.json`
供 Main 校验独立下载的复合包,但 release 配置不增加 `.agent-resources` 或
`.remote-runtime-resources`。发布校验继续检查桌面自身的 OpenCode、Continue 与
DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
在 macOS 上明确生成未签名、未公证的开发验证包:
```bash
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
```
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直接
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
`release-manifest.json`。
@@ -172,6 +475,28 @@ ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直
打包矩阵,避免随后推送版本标签时对同一提交重复完整打包。手动触发会运行
验证和六平台打包,并保留 30 天 Actions artifacts,但不会创建 Release。
### Desktop 发布顺序(强制)
Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段,不能在同一批操作中先后
推送 `main` 和标签:
1. 完成源码、版本、双语 Release Notes 与相关文档修改,运行 Release Notes 校验、
`npm test`、`npm run typecheck` 和 `npm run lint`。常规发布准备不在本机运行
production build、打包或安装包探针。
2. 提交候选,确认用户批准的准确 commit SHA 与双语 Release Notes,fetch 两个远端后,
将该候选分别推送到 `origin/main` 和 `github/main`。
3. 等待这个准确 SHA 的 `main` 分支 `Cross-platform packages` workflow 完成。只有
`Run validators` 和 `Build production bundle` 都成功,候选才具备打标签条件。
4. 如果 `main` workflow 失败,不得创建标签。修复必须形成新的 commit,重新完成本地
校验、双远端 `main` 推送,并等待新 SHA 的 `main` workflow 成功;旧 SHA 的成功结果
不能替代新 SHA 的验证。
5. `main` 成功后再次 fetch 两个远端,确认工作区干净、两个远端 `main` 都等于获批 SHA、
目标标签在本地和远端均未使用。此时才可在该 SHA 创建 annotated tag,并把标签分别
推送到两个远端,随后等待六平台打包和正式发布完成。
不可移动、删除、重建或复用失败的发布标签。完整审批、恢复和发布后检查以
[发布 Runbook](./docs/development/release-runbook.md) 为准。
推送 `v${package.version}` 标签时,工作流运行验证和六平台打包。只有在
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
@@ -185,6 +510,26 @@ GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上
附件。若源码与发布元数据未变化,应修正外部配置后重跑同一不可变标签;
只有必须修改代码或元数据时才递增版本并创建新标签。
Agent 发布使用另一套标签和 workflow:
```bash
agent_tag="agent-v$(node -p "require('./agent-runtime-lock.json').agentVersion")"
git tag -a "$agent_tag" -m "GoodBuddy Agent $(node -p "require('./agent-runtime-lock.json').agentVersion")"
```
实际创建或推送前同样必须获得用户确认。`.github/workflows/agent-release.yml`
原生构建两个 `.gbagent`、签名累计目录、上传
`agent-releases/v<agent-version>/`,创建 `--latest=false` GitHub Release,再切换
OSS 签名目录指针。生产发布需要 `agent-signing` 与 `aliyun-oss-release` 两个受保护
Environment;普通分支/PR 的 `agents.yml` 不使用其中任何 Secret。
Agent 标签也必须遵循与 Desktop 相同的 pre-tag 顺序。更新 Agent 生产源码或
`agentVersion` 后,先把候选提交推送到两个 `main`,等待该准确 SHA 的
`GoodBuddy Agents` main workflow 全部成功,再次 fetch 并确认两个远端 `main`、干净
工作区和未使用标签,最后才可创建和推送 annotated `agent-v<version>`。不得把 Agent
标签与候选 `main` 放在同一批推送中;任何修正 commit 都必须重新等待新 SHA 的 Agent
main workflow,旧 SHA 的成功结果不能复用。
中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次
亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前
请留意 / Before You Start”四段组织,应用首次启动弹窗与 GitHub Release
@@ -204,7 +549,7 @@ git push github "$tag"
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
任务失败,不能静默降级为未签名包。
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
@@ -242,11 +587,11 @@ Windows 代码签名仍未配置。对外分发前还应配置 Windows 签名凭
2. 中文输入法、窗口缩放和高分屏显示。
3. 系统密钥环和模型连接。
4. 本地知识库导入、检索和知识图谱。
5. Ask、Execute 的权限边界与旧版 Plan 数据兼容。
5. 无模式执行、schema 60 与 Runtime 设置 22 的历史字段清理及用户数据保留。
6. OpenCode 与 Continue 的权限边界、取消和超时。
7. DeepSeek Harness Ask 拒绝写入和第三方插件工具,可调用 Main 管理的 Web Search/Fetch;Execute 可调用已启用插件工具。文本模型在网络调用前拒绝图片,声明图片能力的模型可以实际接收 JPEG/PNG。
7. DeepSeek Harness 控制协议 2 可调用已注册内置及已启用插件工具,Main 代理沿用能力分配和请求归属。文本模型在网络调用前拒绝图片,声明图片能力的模型可以实际接收 JPEG/PNG。
8. OpenCode Agent/Command、原生上下文 Compact,以及 Continue Rules/Prompt 预设、结构化提问和 GoodBuddy 手动摘要压缩。
9. Runtime 原生清单把 Tools 与 Commands/LSP/Formatters 分开,显示来源及 Ask/Execute 可用性,不混入 GoodBuddy 分配的 Skills/MCP;外部 OpenCode 只报告连接状态,Continue 明确标记原生 Tools 静态发现不支持;内置 MCP 的启停与 Runtime 分配会持久化并限制后续请求,DeepSeek Harness 保持不可分配;MCP 测试只读取有界 Prompt/Resource 元数据,不读取 Resource 内容。
9. Runtime 原生清单把 Tools 与 Commands/LSP/Formatters 分开,显示来源及描述,不提供统一逐工具开关,不混入 GoodBuddy 分配的 Skills/MCP;外部 OpenCode 只报告连接状态,Continue 明确标记原生 Tools 静态发现不支持;内置 MCP 的启停与 Runtime 分配会持久化并限制后续请求,DeepSeek Harness 保持不可分配;MCP 测试只读取有界 Prompt/Resource 元数据,不读取 Resource 内容。
10. DSH 市场可安装、停用、重新启用和移除插件;启动失败插件不会阻止 Host,并显示为自动停用。
11. 智能心跳的创建、暂停、恢复和历史记录。
12. 应用退出后无残留 Runtime 子进程。
@@ -282,7 +627,7 @@ GOODBUDDY_DSH_MARKETPLACE_E2E=1 npm test -- src/main/agent/dsh-extension-marketp
该测试使用临时用户目录,经捆绑 npm 路径安装已审查的最小测试插件,并验证 Host
加载和真实工具调用;测试结束后删除临时目录。
要让真实模型同时验证插件的 Ask 拒绝与 Execute 调用,可显式提供兼容的
要让真实模型验证已启用插件的调用,可显式提供兼容的
OpenAI Chat Completions 配置:
```bash
@@ -295,24 +640,3 @@ npm test -- src/main/agent/deepseek-harness-acp-e2e.test.ts -t "rejects a real n
如需覆盖发布包内置 npm 路径,再设置 `GOODBUDDY_DSH_NPM_CLI` 与
`GOODBUDDY_DSH_NODE_EXECUTABLE` 指向已解包应用中的 npm CLI 和应用主程序。
## Renderer bundle 性能门禁
`electron-vite` 会在 `out/renderer/.vite` 生成 Vite manifest 和仅含构建模块
归属的 module manifest。`npm run build:bundle` 在 bundle 完成后自动运行
`build/check-renderer-bundle.cjs`,去重统计首屏同步闭包及 Knowledge、知识图谱、
Activity、Magic Notes、Settings 动态入口同步加载的 JS 与 CSS 合计 raw / gzip
大小并执行预算校验。
门禁同时验证以下结构约束:
- Knowledge、Activity 与 G6 不得进入首屏同步闭包。
- `KnowledgeGraphChart` 与 G6 不得进入 Knowledge shell 的同步闭包。
- G6 必须由知识图谱动态入口同步拥有。
路径遍历使用 manifest 中的相对文件名并通过 Node `path.resolve` 读取,因此兼容
Windows 与 POSIX 构建输出。构建专用 module manifest 只记录项目相对路径、
`node_modules/` 相对路径或稳定的虚拟模块名,不记录 Runner 的盘符、主目录或
绝对路径;检查成功后会删除该诊断文件,检查失败时保留以便排查。标准 Vite
manifest 会保留在输出中。预算以干净生产构建为基线并保留有限余量;若业务确需
提高预算,必须先检查 manifest 闭包和产物差异,不能只为通过 CI 调大数值。
+1206 -80
View File
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
# GoodBuddy 功能矩阵与路线图
[English](./FEATURES.md) | **简体中文**
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
## 状态说明
- [x] 已完成
- [ ] 待实现
状态表示功能实现情况,不等同于版本已发布或全部测试已执行。验证记录单独列出,不作为第三种功能状态。
## 功能总表
### 桌面基础、工作空间与上下文
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;仅在明确要求时构建。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
- [x] **跨项目会话活动**:统一入口汇总各项目正在运行和需要处理的对话,保留仍有活动的较早会话。锚定的“项目 → 会话”菜单支持键盘导航和精确打开对话,不再使用整块活动弹窗;空闲时隐藏摘要,长会话子菜单独立滚动,项目选择器继续显示活动数量。
后台会话成功完成后,在本次客户端运行期间保留完成提示,直到对应聊天可见且加载完成;提示不跨重启保留。
- [x] **会话置顶与完整历史**:常用会话可置顶,重启后保留。列表和搜索不再限于最近 100 个会话,连续发送不再截断超过 500 条的已保存历史;模型上下文限制仍有效,已丢失的历史无法恢复。
置顶与非置顶分组按最近一条消息的创建时间排序,流式文本和工具进度更新不再反复改变位置。
- [x] **可选桌面任务通知**:“平台功能 / 通用设置”提供默认开启、保存后立即生效的系统任务提醒开关,应用内提示独立保留;通用设置按快捷键、会话与通知、工作目录分组。
- [x] **按需上下文帮助**:页面和设置中的补充说明使用标题或字段旁的共享帮助入口,支持鼠标与键盘;关键操作后果和错误提示保持可见。
- [x] **紧凑会话控件**:输入选项集中在紧凑设置面板,Runtime、发送和队列操作保持可用;会话搜索提供内嵌清除按钮,清除后恢复当前范围列表并把焦点返回输入框。
- [x] **会话历史按需加载**:列表只加载轻量摘要,不再一次读入全部会话的过程元数据;已打开和活动会话保留完整详情,搜索、复制、导出、续聊和远程待答问题保持可用。图片合并不会重新发送未变化消息,已确认保存的空闲详情随现有视图缓存释放。详见[历史读取与保留规则](./docs/features/assistant-workbar/execution-history-storage.md#会话列表读取与前端保留)。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。聊天输入框支持 `Ctrl+V` 粘贴一个或多个本地支持文件并自动添加为附件,沿用附件按钮的解析进度与限制;普通文本和截图仍可直接粘贴。详见[聊天附件规则](./docs/features/document-processing/prd.md#321-聊天附件入口)。
- [x] **图片型 PPTX 文字识别**:配置本地 OCR 并选择启用 OCR 的解析方式后,可识别内嵌 PNG、JPEG 和 WebP 图片,保留原生文字与幻灯片定位。OCR 在没有活动或排队任务时空闲 60 秒自动释放模型内存,下次按需重载;快速文本/索引模式仍跳过 OCR。详见[解析行为与真实文件验证](./docs/features/document-processing/chat-attachments-technical-design.md)。
- [x] **HTTP OCR 与附件持久预览**:在“设置 / 文档解析”配置 HTTP PaddleOCR-VL,供聊天文档和知识库导入共用。保存原件与插图,提供正文/图片/详情预览、显式图片文字提取,以及选图加入指定会话草稿。普通图片默认走图片输入,发送与入队检查实际模型和 Runtime。Windows、直连视觉输入和共享 Linux Host 托管 OpenCode 已有真实验证;部署高级 OCR 模型及其他 Runtime 组合需分别测试。本机推理监控只管理本地服务,不新增应用中心入口,详见[范围与证据](./docs/features/document-processing/README.md)。
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Mermaid 大图放大后可滚动到各个边缘,并可导出完整 PNG,不受当前缩放和拖动位置影响。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目和所有项目,范围随布局保存;无项目任务在所有项目中标注“未绑定项目”。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记编辑器的 AI 评论栏支持鼠标与键盘调宽;AI 评论栏记住显示状态及宽度,窄窗口下移至编辑器底部。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、能力分配、预算和超时,并支持模板复用。
- [x] **应用中心与导航**:底部“应用”和“设置”为显示文字的等高按钮,中间使用弱竖分隔线。“应用”带向上箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”使用 `LayoutGrid` 图标,打开标题仍为“应用中心”的可搜索列表及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。五个应用行均支持上下移动和拖动排序,不受启用或常驻状态限制;列表、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。设备共享在测试阶段默认关闭,提供启用开关,不提供常驻开关。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [x] **本地设备共享元数据**(源码已实现):测试阶段默认关闭,先在应用中心启用,再从应用菜单或应用中心打开;已明确保存的启用选择保留,关闭会退出页面,但不清除目录或撤销已发布记录。可保存服务地址和设备名称、注册本机、浏览服务返回的目录、发布能力或知识元数据并撤销本机 ID 的条目。默认服务为 `http://127.0.0.1:8787`。服务未认证,ID 只用于识别;服务器来源不上传或同步内容,知识搜索/读取/下载标志是独立声明,不提供正文访问或远程执行。详见[范围与验证](./docs/features/device-sharing/README.md)。
- [ ] **私有化应用市场**(规划中):在应用管理基础上提供组织自己的目录与应用分发。当前只管理内置应用,尚无市场浏览或安装能力,详见 [FR-15](./docs/features/application-tool-navigation/prd.md#fr-15-私有化市场与-yaml-交换后续)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入更完整的执行监督、统一 Runtime 监控、受管进程、可固定目标的工作区/浏览器实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
- [ ] **ShareServer Office 协同编辑**(设计中):通过可选 ShareServer 集成 ONLYOFFICE Docs,在助手工作栏中以文件名打开多个文档 Tab,支持 DOCX、XLSX、PPTX 的人工编辑、保存、撤销、源文件冲突保护和后续 AI 选区修改。Office 正文会由所选 ShareServer 和编辑引擎处理;Desktop 不内置或启动 Document Server,未配置服务时不宣称离线编辑。详见[设计文档](./docs/features/office-document-editing/README.md)。
### Agent Runtime 与模型连接
- [x] **子任务历史去重与空间回收**:本机与托管 SSH 子任务事件只存变化,不再反复保存完整进度。首次升级自动转换旧记录并回收空间,显示进度且支持退出重试;保留聊天、执行详情、结果和远程事件去重。已随 0.13.2 发布的 schema 35 补修还会整理升级到 0.13.1 后重复写入的工具块,不删除事件;终态及时释放写入缓存,远程重复重放不重建已释放的缓存。普通结构升级不重新转换历史或回收正常空闲页,启动页区分结构更新、旧内容转换与实际空间回收。旧客户端不能打开升级后的数据库,具体规则见[执行记录存储与升级回收](./docs/features/assistant-workbar/execution-history-storage.md)。
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
- [x] **当前会话图片工具**:为图片模型开启会话调用后,支持工具调用的聊天模型可生成图片、修改上传图片或历史成果,无需另开会话或手动切换图片模型。直连文本模型、本地 OpenCode、Continue、DeepSeek Harness 和托管远程 OpenCode 复用统一服务,Runtime 分配由模型设置派生;图片请求可能产生费用,编辑需服务商支持。详见[实现与验证记录](./docs/features/conversation-media-generation/progress.md)。
- [x] **跨项目 Runtime 进程复用**:兼容配置的本地 OpenCode 和 DeepSeek Harness 共享重型进程,托管 OpenCode 会话在同一 Host 上复用进程。项目适配器及会话的工作区、模型、工具、问答和取消仍独立路由;远程待答问题和任务状态可在重连或桌面重启后恢复,不重发已接受请求。新的远程行为需要 Desktop `0.13.5` 与 Agent `0.13.0` 配套更新,详见[设计与实测](./docs/features/assistant-workbar/runtime-process-reuse-technical-design.md)。
- [x] **直连模型编程 Agent**:本机直连文本模型可运行平台 Shell,并可按父请求模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
- [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.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。
- [x] **远程原生问答与取消修复**:Agent `0.11.24` 修复远程原生问答转交和取消待答后同一会话续发,需要先升级 Desktop 至 `0.13.0`。
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。已注册内置工具及已启用插件工具以当前用户权限运行,Main 代理工具沿用能力分配和请求归属校验。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。Windows Host 启动和 ACP 会话共用规范工作区路径,同目录不同写法可复用正常 Runtime,不再因路径不一致而创建会话失败。
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方初始化代码使用当前用户权限。
- [x] **统一执行源码链路**:已移除 Ask/Execute、“安全”分类和 `toolApproval` 策略;请求按所选 Runtime、已启用能力及本机或 SSH 账号权限执行。“清除本地数据”位于“通用 > 平台功能”,保留二次确认。schema 60 和 Runtime 设置 22 清理旧字段;远程要求 runtime/acp 6,本地 Harness 控制协议为 2。原生工具清单不提供统一的逐工具开关。真实 Electron 与 Linux Host 定向证据见[进度](./docs/features/unified-execution/progress.md),全量测试及配套发布包验证仍待完成。
- **本地 Runtime 原生客户端(预览)**:输入区可按当前本地项目与模型,在工作栏终端打开 Continue/OpenCode,或在系统浏览器打开官方 DS Web。客户端使用独立会话,不导入 GoodBuddy 对话历史;关闭 DS 网页不停止服务,可在 GoodBuddy 中停止。Continue/OpenCode 使用 Main 按当前请求分配的工具,无产品工作模式。远程快捷入口未接通,托管标准 Node 交付和完整跨平台安装包验收仍有缺口;Windows 真实模型证据不代表全部会话能力等价,详见[实现与剩余工作](./docs/features/assistant-workbar/progress.md#2026-09-27-local-native-client-validation)。
- **Runtime 遥测关闭配置**:本地 DS Web、远程 Runtime 和模型桥启动共用遥测关闭环境,不限制模型、MCP 或用户主动联网。Continue 在自动更新关闭时跳过检查;远程改动随配套 Agent 新启动的 Runtime 生效,当前源码 Linux x64 Agent/model 验证见 [0.15.0 发布准备](./docs/development/release-preparation-0.15.0.md)。
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,本机与托管 SSH OpenCode 同时转交属于当前请求的子会话提问。托管 SSH OpenCode 问答需要配套 Agent 和新启动的托管 Runtime,不扩展到任意 ACP 服务。回答或跳过成功后在原位置保留问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。并行问题按顺序等待回答,重复事件保留草稿,提交失败可重试;取消远程待答后可在同一会话继续发送。原生工具权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
- [x] **原生执行清单**:OpenCode 和 Continue 在对话顶部更新只读执行清单,进度随会话保存。详情在可滚动的锚定浮层中展开,不挤动聊天内容;点击外部关闭,Escape 关闭后焦点返回入口。显式清空和远程重放保留请求归属,取消不把未完成条目标成完成;远程交付需要配套 Agent,详见[清单契约](./docs/features/assistant-workbar/runtime-checklist-technical-design.md)。
- **取消后的历史**:模型可见历史保留已中断的部分回复或占位标记,避免旧问题合入下一轮;本地 OpenCode 复用会话时附带一次中断说明。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求执行空间和能力范围,可使用已启用的本机直连模型工具;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
- [x] **OpenCode 事件连接回收**:聊天与原生上下文压缩在结束事件迭代前关闭各自订阅,覆盖完成、失败、取消和消费方提前结束,不取消其他并行会话。
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
- [x] **直连模型命令工作目录**:命令可通过绝对路径、相对路径或符号链接使用当前账号可访问的工作区外目录;工作区仍为默认目录和相对路径基准。
- [x] **Runtime 与模型分层选择**:按会话、项目、全局逐字段解析 Runtime 和模型。会话双栏选择器显示来源,可覆盖或恢复项目默认;项目与通道设置共用规则,Main 在每次请求及排队派发时重新解析。已删除或不兼容的连接会回退,缺少凭据仍明确报错。schema 49 迁移已保存选择,用途模型仍为规划,详见[选择设计](./docs/features/model-connections/runtime-selection-design.md)。
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
- [x] **本地直连手动压缩**:文本模型会话可在自动压缩关闭或未到阈值时手动生成摘要,保留最近轮次和完整历史,保存的摘要用于后续对话;图像连接和远程直连入口不适用。摘要模型调用可能产生费用。
- [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。
- [x] **持久凭据保护**:API Key 由 Main 使用系统安全存储加密且不暴露给 Renderer。托管 SSH accepted Prompt 只把当前 profile 与密钥放入 Agent 内存,不写入 SSH 参数、远端环境或磁盘;其他路径仍在 Main 内使用。密钥随对应模型连接保存,修改服务地址或临时切换为无需认证不会要求重新输入;只有用户显式清除凭据或删除连接时才移除。
- [x] **有界故障诊断**:Desktop 在用户数据目录中轮转保存启动、Runtime 和远程连接的固定阶段失败,最多 4 个 256 KiB 文件;GoodBuddy Agent 在各 installation 的私有 state 目录中轮转保存 daemon、连接、恢复和 Runtime 生命周期,最多 3 个 64 KiB 文件,并可通过固定 `diagnostics --installation-id` 命令读取。两端都只记录白名单阶段、稳定错误码/类型和固定短消息,不保存 Prompt、凭据、文件内容、路径、环境、SSH 参数或 Provider 原始响应;诊断写入失败不改变正常运行。
- [x] **OpenCode Runtime 定制**:GoodBuddy 管理的内置 OpenCode 可发现原生 Agents、Tools、Commands、LSP、Formatters、MCP、Skills、Prompts 与 Resources;Tools 单独显示读取、文件修改、命令、网络、Agent 编排等类型、来源及描述,并隐藏 OpenCode 内部 `invalid` 与 GoodBuddy 临时 MCP 工具。支持保存默认 Agent、每次请求覆盖 Agent、通过原生 SDK 执行 Command、显示上下文用量并调用有总时限的原生 Compact;并发外部 Server 对话的提问使用请求级公开 ID 映射,回答不会串到其他会话。外部 OpenCode Server 只报告连接状态,不宣称原生清单可读。任意插件安装、Session Share、自动 Worktree 和 OpenCode 原生会话持久化仍不开放。
- [x] **Continue Runtime 定制**:提供静态配置中的原生 Rules、Prompt 模板与 MCP 清单,以及可编辑的 GoodBuddy Rules/Prompt 配置预设;聊天可按请求选择预设和填入可继续编辑的 Prompt。当前 Continue Host 没有可信的静态原生 Tool 发现接口,且使用隔离的 `CONTINUE_GLOBAL_DIR`,因此界面明确标记 Tools 不支持静态发现,也不把 Host 实际不会加载的工作区或用户 Skills 冒充原生能力;GoodBuddy 分配的 Skills 仍按请求暂存执行。Continue 临时 Host 不复用原生会话压缩,手动压缩由 GoodBuddy 摘要模型完成并验证持久化摘要覆盖范围;Agent 交互提问转换为统一问答卡片。Resources、Hooks、后台 Job 和 Continue 原生会话管理继续暂缓。
- [x] **Runtime 原生清单语义**:原生能力以 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 个页签展示;清单状态独立于 Runtime 连通性,区分完整、部分、不可用、仅连接和不支持。DeepSeek Harness 通过 Host Registry 枚举有界的内置/插件 Tools 与 Skills,显示来源和描述,并排除 GoodBuddy 按请求分配的 Skills、Web/MCP 代理。
- [ ] **Runtime 监督栏目**(规划中):在应用级助手工作栏的固定 Runtime 栏目统一承载 OpenCode、Continue 和 DeepSeek Harness 的 Task 级委派、后台执行、Workflow/Hook、长任务与原生会话监督;用户只选择 Conversation 或 Task,Job/Run 保持内部,不形成树或独立操作对象。
- [ ] **Subagent 高级监督**(规划中):在工作栏固定 Runtime 栏目按 Task 聚合,提供可配置的嵌套、并行、预算和生命周期控制。基础专家能力继承与单层直连模型编程委派已经可用。
### Skills、MCP 与知识库
- [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。
- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证;托管 Python 归档按目标文件系统验证,Linux 保留大小写不同的合法路径,所有目标仍拒绝完全重复路径和不安全条目。
- **本机工具执行环境验证记录**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv。不可变的 `v0.12.0` 尝试在发布前通过 Windows 与 macOS,但暴露了 Linux TAR 大小写处理问题;`v0.12.1` 在同步 Agent 发布时于原生打包前取消。已发布的 `v0.12.2` 随后通过全部六个原生打包任务及其托管 Python 安装探针。现有记录未覆盖真实 Skill/MCP、自定义解释器和运行中进程协调。
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;这些 Server 在 DeepSeek Harness 中仍不支持。Story Graph 另通过 Main 代理支持 Harness。内置 MCP 仅通过当前请求的短期本机权限提供,继续校验启用、分配和资源归属。
- **Obsidian 接入(源码已实现,验收中)**:默认关闭,默认发现全部本机注册仓库,也可指定文件夹并在保存或启用前测试。随包提供 MCPVault 0.16.0,19 个共享工具保留全部上游能力;生产组件自动化、Electron 托管 Node、Windows 设置与聊天 UI,以及受限网络下的 Windows x64 解包应用已验证。物理断网、安装器及其他系统验收待完成。DeepSeek Harness 与远程执行保持不可用,详见[功能文档与验证记录](./docs/features/obsidian/README.md);此项不表示已发布。
- [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent 和 DeepSeek Harness;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍经过发现、执行记录与资源范围校验。直连模型与 Harness 的健康调用复用当前请求已发现的工具清单,不在每次调用前重复发现。
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
内容来源与文档索引共用可搜索列表,单文档来源不重复占行,目录保留分组文档,失败来源保留恢复操作;任务历史和次级操作从统一入口访问。
- [x] **外部知识库接入**:在知识库页面管理 Dify、FastGPT 和 RAGFlow 实例,通过目录或手工 ID 验证并绑定远端知识库,保留 Provider 专属检索配置和引用;对话支持按需检索及每次回答前检索。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
- **外部知识库验证记录**:三家 Provider 已通过真实服务、本机 HTTP MCP 和生产 IPC 短答案检查,Dify 另通过完整 App 输入区到回答及引用的链路。现有记录未覆盖 FastGPT/RAGFlow 桌面专属参数、复杂多库问答和远程回答前检索,详见[验证记录](./docs/features/knowledge-base/progress.md)。
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
模型抽取不再额外限制为 8192 tokens;OpenAI 使用服务商上限,Anthropic 使用模型连接的输出设置。
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论。笔记从自适应卡片总览打开;独立待办页提供搜索、状态筛选、分组列表和直接勾选,宽屏列表稳定在左侧,任务说明、来源记录及 AI 评论在右侧同一区域显示,窄屏按需显示详情并提供返回列表。紧凑页签放在页头新建按钮左侧,筛选放在搜索右侧。打开原笔记再返回可恢复任务上下文;切换任务不会混入上一项的来源内容,读取失败使用应用通知并提供就地重试,来源缺失有明确提示。左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。Agent / MCP 写入后,已打开的工作台自动更新,并保留当前选择与未保存草稿。
- **会话采集与快速笔记**(已实现,真实 Electron 验收通过):单条助手回答或完整会话正文可先预览、编辑,再追加到已有笔记或连同首条记录创建新笔记。右侧笔记 Tab 支持标题与正文搜索、快速追加及打开完整工作区,记录来源可返回会话和消息;切换会话保留草稿。真实 App/preload/IPC/SQLite 验收覆盖完整 502 条历史及 214px 面板原生键盘操作,模型调用为 0。[功能验证进度](./docs/features/magic-notes/progress.md)保留聚焦通过、fixture 修复与当时的测试和类型检查失败;[0.15.0 候选验证](./docs/development/release-preparation-0.15.0.md)已通过全量测试、类型检查和 lint。
- 待办宽屏列表与详情之间可拖动分隔条并记住宽度;来源笔记分组可独立折叠,不清除当前选中任务的详情。
- [x] **连续记录工作区**:笔记详情连续展示全部记录,左侧索引默认 168px,可拖动右边界或用键盘在 140–320px 内调宽,并记住偏好;索引与右侧 AI 评论独立收起。每条画布记录显示真实首页缩略图、页数和时间,文字记录显示摘要;点击索引只滚动定位,保留当前编辑器和草稿。打开笔记就在标题下方显示空白输入区,保留文字/画布选择和保存操作;新增保存后清空,供下一条记录使用,旧记录通过编辑入口修改。空白输入区和保存后未再修改的记录返回时不提示;保存失败和真实修改仍受保护。窄屏索引使用固定宽度折叠抽屉,不显示调宽分隔条,AI 评论移至底部。
- [x] **分页画布笔记**:已集成 PeopleLib Fabric + Quill 编辑器,支持跨页正文、笔迹、高亮、对象选择及变换、浮动文字、图片和纸张模板。PDF 可作为分页底版导入并提取原生文字;正文和批注可导出为栅格 PDF,导出文件不保留可搜索文字层。用户手动保存后,记录正文与二进制资源存入本地笔记文件,SQLite 保留元数据、索引、修订、待办和评论。备份须协调保存 SQLite 与笔记目录,产品无一键备份入口。撤销/重做限当前批注页或 Quill 模式,不提供全局文档撤销、无限画布或 PNG 下载按钮。草稿、已保存记录和来源为画布的待办按默认模型图像输入能力使用页面图像加文字,或明确标注仅文字降级;“发送画布页数”可选 1~8,默认 1,按当前顺序发送前 N 页,文字和图片范围一致,保存与导出仍支持 50 页。纯视觉记录/草稿需要支持图像输入的模型。即时评论模式提供手动画布分析,保存后自动分析失败不阻断或回滚保存。提取文字不变时保留文字评论;布局变化使视觉评论失效,并在保存后自动评论模式下重新分析。MCP 禁止以纯文字覆写画布记录。实现已完成,最终验证状态见[功能进展](./docs/features/magic-notes/progress.md)。
编辑与只读画布均支持 25%–300% 视图缩放、100% 还原及随视口调整的“适应宽度”,不改变保存内容或导出尺寸。
历史正文迁移已改用文件存储,当前 schema 48 新增会话来源元数据;旧客户端不能再打开,回退需使用升级前备份。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、能力范围和审计。
- [ ] **知识/网页摘录与 AI 编辑**(规划中):在已实现的会话正文采集之外,扩展知识和网页摘录,并提供需确认的总结、改写和整理操作。
### 工作管理、长期协作与工作流
- [x] **任务、活动与工作区文件**:集中管理任务状态和审计活动。成果页已移除,工作区按实际文件显示修改时间和可用的创建时间,支持排序及向所选本机或远程目录导入文件;远程上传需要更新后的 Agent。会话图片和附件保留在消息中,已有 artifact 数据不删除,也不自动复制到项目。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;用量按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺。独立“系统任务”页签按来源汇总后台用量,支持手动刷新和记录更新后刷新;监督回顾、图片重新生成、图谱提取和远程嵌入补充用量记录,不补算缺失的历史或供应商数据。活动按会话分组并默认收起,避免长历史占满页面。
OpenCode 的输入量已扣除缓存,计算命中率时会加回缓存读取和写入量;查询已有记录时也按此口径重算。
- [x] **紧凑会话工具记录**:逐项展开工具记录即可查看和复制结果、错误及输入参数;会话与子任务进度使用简洁行,减少重复 Runtime 摘要。OpenCode、Continue、DeepSeek Harness 和直连模型可显示已有文件路径、命令或搜索摘要,并在工具完成和会话重载后保留。
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,沿用 Runtime、已启用工具和资源范围;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
- [x] **记忆与智能心跳**:计划触发共享增量回顾,不再单独生成心跳报告;可只更新记忆图谱,或从未决事项、分歧、修订和候选约定生成有依据的建议。接受未决事项创建暂停任务,确认约定写入长期背景;建议失败可单独重试,不撤销已完成回顾。
- [x] **定时消息沿用会话**:定时消息使用会话当前历史、Runtime 和已保存的知识检索设置;任务详情显示运行状态,当前 occurrence 未结束时禁止重复“立即运行”。
任务中心通过状态数量和筛选定位运行中及待处理任务,结构化问题在对应会话中回答。
- [x] **智能心跳设置与范围**:“监督者 > 智能心跳”统一管理 Global 或指定 Project 的每日、每周计划及建议,Task Center 和设置中心不复制表单。待处理建议不重复,忽略后有新依据才再次提出;项目与全局共用按来源版本维护的时间线进度,会话级实时主动介入仍为规划。详见[实现规则](./docs/features/conversation-supervision/logic-design.md)。
- [x] **故事、经验与时间螺旋**:事件可组织为项目功能、子线索及可选跨项目故事,支持重命名、合并、调整归属、移除和撤销。经验保留形成及应用依据与人工调整标记,心跳可提示暂无进展故事或可能适用的经验。平铺和懒加载 WebGL 时间螺旋共用选择与来源,保留平铺回退及图形资源释放;整理与建议可能增加模型费用,自动结果须核对来源。详见[实施证据](./docs/features/conversation-supervision/progress.md)。
- [x] **监督者回顾、故事线图谱与活动**:按范围和时间回顾工作,查看保存结果的图谱及来源,确认或修订实体,并带入相关回顾上下文继续讨论。侧栏可跟随或固定 Conversation、Task,并打开匹配结果。活动合并心跳触发与下游监督的阶段状态,保留失败和旧结果入口。自动与默认手动回顾只处理新增、修改及未处理来源,无变化时跳过模型;显式“重新整理”重读区间,但不推进或重置共享进度,自动回顾不续跑用户暂停的运行。回顾只读且禁止工具,模型调用可能产生费用,详见[实施与验证记录](./docs/features/conversation-supervision/progress.md)。
手动开始、自动触发和继续共用一个活动回顾;活动记录可取消并保留已保存批次,清理结束前不接纳新回顾,已取消回顾不可继续。故事线事件、实体和关系按独立页签展示数量,并与画布选择同步。
可从监督反馈或图谱来源直接继续讨论,编辑问题并明确打开目标会话;界面不再提供知识实体写回控件或重复上下文说明,智能心跳用可处理的监督建议代替报告指标和趋势。
- 手动回顾分页处理所选历史,并按批保存事实与来源;不再按固定时长自动暂停,仍支持主动暂停和从保存进度继续。设置提供并发、单次请求超时、批次大小和响应容量;活动记录突出相连阶段、已保存批次与剩余来源,次要统计可展开查看。工作回顾采用全宽纵向布局,运行状态单独显示,空态不再挤在控件旁。
- [ ] **监督后续能力**(规划中):逐事件完整回放、Experiment 目标、人工修改时间序列审计、事件触发及更完整的执行观察仍未完成。当前自动回顾受输入预算和滚动窗口限制,删除报告不会重置处理进度或重建图谱。详见 [会话监督 PRD](./docs/features/conversation-supervision/prd.md)。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
- **Story Graph 只读工具(Desktop 0.15.9 / Agent 0.15.3)**:Agent 可搜索已保存工作、故事和经验,读取上下文并分页核对来源。监督者开关及 Runtime 分配同时控制发现和调用,旧会话也受限制;本地和远程路径共用桌面读取入口。共享来源版本事实按允许范围复用,关键词匹配和工具说明引导模型按需查询,不自动注入全图;经验是参考而非指令,查询本身不调用模型,`as_of` 仍未实现。详见[设计与验证](./docs/features/conversation-supervision/story-graph-mcp-design.md)。
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、业务确认、取消和恢复,执行节点继续经过 Main Runtime 边界。
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具活动、用量和成果,支持失败定位、重试和脱敏导出。
### 浏览器、通信、语音与应用维护
- [x] **Runtime 共享内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过内置 MCP 总开关和 Runtime 分配决定工具接入,开启后不逐次询问。关闭 Agent 浏览器能力不影响用户继续在浏览器工作栏中手动前往、返回、刷新、停止加载、交互或关闭浏览器。直连模型、GoodBuddy 管理的 OpenCode 和 Continue 通过请求级权限共享按 Conversation 归属的会话和串行操作路径,用户导航会改变 Agent 下一步看到的页面。
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
- [x] **浏览器关闭与刷新**:AI 使用中的页签也可关闭,不取消整个请求;后续显式导航会创建独立替代页。空白页不能刷新,切换或关闭面板后清除旧操作错误。
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
- [x] **远程消息通道项目**:微信 ClawBot、企业微信、钉钉和 Telegram 分别拥有系统管理的项目、独立远程会话、工作目录、处理后端及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。Telegram 的真实 Bot 与模型链路仍[待验收](./docs/features/telegram-channel/progress.md)。
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
- [x] **受管本地模型下载源**:“模型连接”页头在模型类型页签同行右侧、保存操作下方显示当前来源与次按钮。弹窗为后续受管本地模型下载全局选择 ModelScope(默认)或 Hugging Face,确认才保存,取消保持已保存来源不变,错误可重试;向量、语音和 OCR 的更换下载源入口直接打开该弹窗。所选来源缺少完整已验证文件时明确不可用,不静默换源或混合文件。
- [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、ZIP 或本地目录导入、切换和删除。
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
- [x] **窗口恢复入口**:渲染失败时显示本地化的重新加载页面,渲染进程崩溃时提供原生恢复确认;重新加载可能丢失未保存输入。详见[窗口恢复说明](./docs/development/window-recovery.md)。
- [x] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
### 开源、构建与发布
- Desktop `0.15.3` 修复本地直连手动压缩和请求有效期间的工具授权,区分取消与失败并去除重复提示,优化侧栏搜索、注册成功后的快捷键提示及笔记操作布局,减少启动和存储中的重复处理,并精简重复依赖及开发文件。不宣称整体渲染性能已显著提升;Office 实时编辑仍为设计。证据见[0.15.3 发布准备](./docs/development/release-preparation-0.15.3.md)。
- 当前 Desktop 候选为 `0.15.13`,Agent 候选为 `0.15.6`、最低要求 Desktop `0.15.13`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- 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)。
- Desktop `0.15.8` 与 Agent `0.15.2` 支持 Execute 模式保存会话图片到本机或 Host 绝对路径,覆盖需明确指定,不支持转换为 WebP。MCP 设置列出图像工具,不代表所有模式均可用。笔记增加新记录取消、编辑焦点返回及可用内容全宽;缺少浏览器和磨砂设置时默认开启,已保存选择不变。schema 仍为 52。真实 Linux x64 Host 工具层证据与待完成的精确候选 CI、打包验证分别记录在[发布准备](./docs/development/release-preparation-0.15.8.md)。
- Desktop `0.15.7` 复用已完成的流式 Markdown 块与会话派生值,不改默认流式节奏。运行记录进入时读取、可见时每 15 秒刷新,也可手动刷新;本地自定义 MCP 发现故障按服务隔离。受管模型下载源移至模型连接中的确认弹窗,设备共享改为显式启用的测试功能。schema 仍为 52,仍需精确候选 CI 与打包验证,组件测量不等同完整 App 结果。详见[发布准备](./docs/development/release-preparation-0.15.7.md)。
- Desktop `0.15.6` 增加默认关闭、可保存的顶栏与清单磨砂外观,不透出系统桌面;减少聊天重复渲染,草稿与队列图片支持直接预览,设置分组及开关右对齐,笔记标题和编辑间距更紧凑。本地 OpenCode 在清理后对明确暂时性 MCP 传输错误最多重试一次,作废结果不确定的名称,不重放 Prompt 或工具调用;反馈包含脱敏连接诊断。schema 52 增加执行统计索引,升级前备份数据库和笔记。详见[发布准备](./docs/development/release-preparation-0.15.6.md)。
- Desktop `0.15.5` 更新智能心跳与增量回顾,保留中断轮次历史,执行清单改为浮层,笔记保存成功后退出编辑;自动评论不重新打开编辑器,失败保留草稿。新增手动笔记标签、AND 筛选及重命名/合并/删除,双栏笔记与全宽概览,固定工作区文件操作栏,缓存 OpenCode 终端依赖并缩短 MCP 名称。schema 51 增加标签存储,同时迁移旧待确认记忆为待处理约定;升级前备份数据库与相邻笔记。证据见[发布准备](./docs/development/release-preparation-0.15.5.md)。
- Desktop `0.15.4` 增加 Runtime/模型分层选择及独立系统任务用量,减少 Runtime 指令和历史重复。schema 49 迁移项目与会话选择,回退须恢复完整升级前数据库和相邻笔记备份。Agent `0.15.1` 要求 Desktop `0.15.4`,修复远程 Continue 历史重复封装,Runtime 锁版本不变。证据见[0.15.4 发布准备](./docs/development/release-preparation-0.15.4.md)。
- Desktop `0.15.1` 已完成六平台原生打包及 GitHub/OSS 发布。Desktop `0.15.2` 新增 Story Graph 只读工具和完整可编辑的监督讨论消息,简化笔记采集并补充退出诊断;远程图谱读取需配套 Agent `0.15.0`。主机与云环境管理应用仍为待实现规划,当前候选证据见[0.15.2 发布准备](./docs/development/release-preparation-0.15.2.md)。
- Desktop `0.15.1` 承接未发布的 `0.15.0` Obsidian 接入、会话采集到魔法笔记和预览中的本地原生客户端,并修复知识库、监督者与界面操作。schema 48 新增会话来源元数据,回退旧客户端需使用升级前完整备份。Agent `0.15.0` 要求 Desktop `0.15.2`,保留 Node `24.19.0`,补齐远程遥测关闭配置及 Continue 跳过已关闭的更新检查,不新增远程客户端快捷入口。
- Desktop `0.13.15` 承接未发布的 `0.13.14`,改善监督者分页回顾与续跑、ripgrep 原生搜索、待办布局和远程结果未知后的显式续聊。数据库升级至 schema 47,回退旧客户端需使用升级前完整备份。
- Desktop `0.13.16` 分离结构升级与旧历史回收,移除回顾固定时长自动暂停,并改善监督者布局。相较 `0.13.15` 不新增 schema 变更,也不要求更新 Agent。
- Agent `0.13.3` 回收空闲 Runtime,并使被替换的 Agent 等已有工作结束后退出;复合包要求 Desktop `0.13.12`,请先升级 Desktop,再更新 Host 环境。Node 保持 `24.19.0`。
- Agent `0.13.4` 在完成前等待模型交付确认,空闲 Runtime 回收失败不改变已完成结果,并在环境更新后清理已确认停用的旧程序及所属孤儿进程;保留历史、活动及状态不明的安装。独立包要求 Desktop `0.13.14` 或更新版本;因 `0.13.14` 未发布,请先升级至 Desktop `0.13.15`,再更新 Host。
- 发布准备要求全量测试、发布说明校验、类型检查及 lint;候选主分支 CI、生产构建、原生打包及公开发布核验为独立验收步骤。本轮不做本地生产构建、打包或龙芯预览,当前验证状态见[发布准备记录](./docs/development/release-preparation-0.15.0.md)。
- Desktop `0.13.12` 和 Agent `0.13.3` 已完成原生发布流程,但不替代开发期真实 Host 场景覆盖。
- 验证记录与功能状态分开。存储已有本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;进程复用有 Windows 完整 App 与 Linux x64 Host 证据,PPTX OCR 与空闲内存释放有 Windows 真实文件证据。图片工具已有真实服务商生成/编辑及 Host 传输证据,这些记录未覆盖完整界面自然语言调用和本地/远程切换。候选仍需主分支 CI 及原生发布打包,详见[Runtime 验证记录](./docs/features/assistant-workbar/progress.md)与[图片工具验证记录](./docs/features/conversation-media-generation/progress.md)。
- 发布准备前,`9836a4c` 已通过本地 4,858 项测试(67 项跳过)、完整类型检查和 lint;真实服务商画布分析仍未验证。该提交的三个原生 Agent CI 构建通过;桌面 CI `35485936807` 为 4,875 项通过、8 项 Electron 测试失败,均因 Linux 沙箱辅助程序未配置,在页面断言前退出。候选配置该程序并在 Xvfb 下运行测试,不关闭沙箱;候选本地验证、主分支 CI 与原生标签打包分别记录,不在发布准备中运行本地生产构建或打包。
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
### 开放接口、团队协作与远程执行
- [x] **远程 Continue 交付**:已接入 CN 1.5.47 受管包、Agent HTTP-to-ACP 适配、模型路由、项目选择和原生清单。默认包同时携带 OC/CN;Windows 桌面到 Linux x64 已通过安装、真实模型清单更新、长清单、取消及重启恢复验证。Execute 会话 MCP 已通过真实文本模型 Host 往返,图片服务使用替代接收端,不代表真实生图验证。已发布 Agent `0.13.4` 需要 Desktop `0.13.14` 或更新版本;正式发布及原生平台验收与 Linux 开发证据分开,详见[验证记录](./docs/features/remote-host/runtime-checklist-validation.md)。
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
- **SSH 主机与远程执行空间历史验证记录**:当时已接线主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP v5、Agent-owned Prompt/gateway/transcript、Ask 只读、Execute 完整账号权限、取消、detached Agent 精确重连和 release-only 双架构资源校验。Linux x64 当时源码已用真实模型与工具通过 detach、进程结束、relay 丢失、并发、取消、Provider 异常、Agent 重启和 Desktop SQLite 恢复矩阵;测试签名 Agent `0.11.17` 又通过两个并发 Ask 读取、一次 Execute 写入并读回,以及 6 次带自定义 Header 和 Body 的真实受管网关模型请求,未发现 Provider 请求重放,隔离 Agent state 中也没有 API Key 或自定义请求值。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包;现有记录未覆盖当前 Linux x64/arm64 正式签名工件的完整公开核对及 GitHub/北京镜像、双架构和离线 GoodBuddy 传输矩阵,发布结果另行记录。
- [ ] **主机管理与云环境管理**(规划中):主机管理迁入应用中心,提供列表与详情工作页面;云环境管理作为同级应用,复用主机连接和执行能力。首版适配计划覆盖 AWS、Azure、阿里云、腾讯云、VMware vCenter / vSphere、ZStack,分批完成已有实例发现、接入及真实执行验收;实例创建、启停、销毁和网络编排后续交付。平台 API 尚未实现,现有云主机可按 SSH 条件手工接入。详见[产品范围](./docs/features/remote-host/prd.md)。
## 规划原则
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、能力范围、权限、取消、超时和审计边界。
-80
View File
@@ -1,80 +0,0 @@
# GoodBuddy
English | [简体中文](README.md)
A secure, cross-platform, local-first desktop AI assistant and Agent workspace.
![GoodBuddy workspace](docs/screenshots/workspace-overview.png)
## Highlights
- **Controlled execution**: `Ask` stays read-only; `Execute` runs only enabled tools within defined boundaries and records their activity.
- **Local-first data**: Conversations, tasks, artifacts, memory, knowledge bases, and graphs are stored in local SQLite. API keys are encrypted by the operating system.
- **Multiple runtimes**: Connect directly to models or use OpenCode, Continue, and the preview DeepSeek Harness, with cancellation, timeouts, output limits, and process cleanup.
- **Open integrations**: Supports OpenAI Responses, OpenAI-compatible Chat Completions, Anthropic Messages, OpenAI Images, Embeddings, cross-runtime Skills and custom MCP, plus a default-off DeepSeek Harness npm plugin marketplace that users enable explicitly.
- **Knowledge workspace**: Import files, folders, and web pages, then search them with full-text, phrase, vector, and graph retrieval.
- **Work management**: Organize projects, conversations, tasks, activity, artifacts, memory, Magic Notes, and Smart Heartbeat.
- **Remote channels**: Connect WeChat ClawBot, WeCom, and DingTalk with separate remote sessions for each sender.
- **Desktop context**: Add selected files, screenshots, application windows, clipboard content, and voice.
- **Offline speech**: Use local SenseVoice, Paraformer, and Whisper models.
- **Rich responses**: Render Markdown, LaTeX, and controlled Mermaid diagrams.
![GoodBuddy knowledge workspace](docs/screenshots/knowledge-workspace.png)
![GoodBuddy knowledge graph](docs/screenshots/knowledge-graph.png)
![GoodBuddy Magic Notes](docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy Smart Heartbeat](docs/screenshots/smart-heartbeat.png)
See [FEATURES.md](FEATURES.md) for the detailed feature matrix and roadmap, and
the [documentation index](docs/README.md) for product, architecture, design,
and quality documents.
## Install
Download a build from [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
| Platform | Architectures | Formats |
| --- | --- | --- |
| Windows | `x64`, `arm64` | NSIS, portable ZIP |
| macOS | `x64`, `arm64` | DMG, ZIP |
| Linux | `x64`, `arm64` | AppImage, DEB |
Code signing and macOS notarization are not configured yet, so your operating system may display a security warning.
## Run from source
Requires Node.js 24 and npm:
```bash
git clone https://github.com/mesalogo/goodbuddy.git
cd goodbuddy
npm ci
npm run dev
```
See [BUILD.md](BUILD.md) for build and packaging instructions.
## Privacy and security
- Model requests are sent only to services selected by the user.
- Local data stays in the operating system's application data directory by default.
- The Renderer has no access to raw Electron APIs or model credentials.
- The DeepSeek Harness plugin marketplace is off by default. After it is enabled and a third-party plugin is installed, its install scripts, initialization, and Execute tools run with the current user's permissions. Turning off the marketplace only hides its catalog and management interface; it does not disable or uninstall existing plugins. Installation requires explicit confirmation, and Ask limits only model tool calls.
- Remote delegation is disabled until the user configures an endpoint and token.
- Private-network compatibility permits in-app HTTP and non-standard HTTPS certificates. WeChat credential and media endpoints remain strictly validated.
## Contributing
Issues and pull requests are welcome. Read [AGENTS.md](AGENTS.md) first, then run:
```bash
npm test
npm run typecheck
npm run lint
```
## License
Original GoodBuddy code is released under the [0BSD License](LICENSE). You may use, modify, distribute, and commercialize it freely. Third-party components and resources retain their respective licenses.
+127 -44
View File
@@ -1,50 +1,114 @@
<div align="center">
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
# GoodBuddy
[English](README.en.md) | 简体中文
**Desktop access to model chat, coding agents, knowledge, and scheduled work**
安全、跨平台、本地优先的桌面 AI 助手与 Agent 工作空间。
**English** | [简体中文](./README.zh-CN.md)
![GoodBuddy 工作空间](docs/screenshots/workspace-overview.png)
[Website](https://mesalogo.github.io/goodbuddy/en.html) ·
[Download](https://github.com/mesalogo/goodbuddy/releases) ·
[Feature Matrix](./FEATURES.md) ·
[Build Guide](./BUILD.md)
## 核心能力
[![Release](https://img.shields.io/github/v/release/mesalogo/goodbuddy?label=release)](https://github.com/mesalogo/goodbuddy/releases)
[![License: 0BSD](https://img.shields.io/badge/license-0BSD-blue.svg)](./LICENSE)
[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20arm64-0078D4)](https://github.com/mesalogo/goodbuddy/releases)
[![macOS](https://img.shields.io/badge/macOS-x64%20%7C%20arm64-000000)](https://github.com/mesalogo/goodbuddy/releases)
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
- **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。
- **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。
- **多 Runtime**:支持直连模型、OpenCode、Continue 和预览版 DeepSeek Harness,统一处理取消、超时、输出限制和进程退出;原生能力清单将 Tools 与 Commands、LSP、Formatters 分开,并显示来源及 Ask/Execute 可用性。内置 OpenCode 提供 Agent、Tool、Command 与原生 Compact,Continue 提供 Rules、Prompt 预设、结构化提问与 GoodBuddy 手动摘要压缩,并明确标记当前版本无法静态发现原生 Tools。
- **开放连接**:支持 OpenAI Responses、OpenAI 兼容 Chat Completions、Anthropic Messages、OpenAI Images、Embeddings、跨 Runtime Skills 与自定义 MCP,以及默认关闭、由用户显式开启的 DeepSeek Harness npm 插件市场。MCP 测试可读取有界 Prompt/Resource 元数据,但不会读取 Resource 内容。
- **知识工作区**:支持文件、目录和网页导入,以及全文、词组、向量和图谱混合检索。
- **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳。
- **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。
- **桌面上下文**:可选择文件、截图、应用窗口、剪贴板和语音作为上下文。
- **离线语音**:支持 SenseVoice、Paraformer 和 Whisper 本地模型。
- **富文本回答**:支持 Markdown、LaTeX 公式和受控 Mermaid 图表。
![GoodBuddy workspace](./docs/screenshots/GoodBuddy_en.png)
![GoodBuddy 知识工作区](docs/screenshots/knowledge-workspace.png)
GoodBuddy is a desktop application for model chat, coding agents, knowledge
retrieval, notes, and scheduled tasks. No GoodBuddy account is required.
Workspace data is stored locally by default. Model endpoints can run on the
same device, on a private network, or at a provider selected by the user.
![GoodBuddy 知识图谱](docs/screenshots/knowledge-graph.png)
## Highlights
![GoodBuddy 魔法笔记](docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
- **Local-first workspace**: No GoodBuddy account is required. Workspace data
stays local by default, and users choose where model services run.
- **Unified Agent Runtimes**: Use direct models, OpenCode, Continue, and the
preview DeepSeek Harness from one desktop interface.
- **Unified execution**: Requests use the selected Runtime, enabled capabilities,
and current account permissions without choosing a work mode. Tool activity, cancellation,
timeouts, and token usage are recorded.
- **Broad platform coverage**: Official Linux releases cover `x64` and `arm64`,
including compatible distributions used in China's domestic computing
ecosystem. A separate
experimental `loong64` preview is available for LoongArch systems.
- **Knowledge and long-running work**: Knowledge bases, graphs, Magic Notes,
tasks, and Smart Heartbeat share one desktop workspace.
![GoodBuddy 智能心跳](docs/screenshots/smart-heartbeat.png)
Platform coverage describes operating-system, instruction-set, and package
compatibility, not certification for every hardware vendor or distribution
release. LoongArch is outside the official release matrix; see the
[LoongArch preview notes](./docs/development/loongarch-preview-build.md) for its
current limitations.
完整功能和路线图见 [FEATURES.md](FEATURES.md),产品、架构、设计与质量文档见
[文档导航](docs/README.md)。
## Capabilities
## 安装
| Area | Available in GoodBuddy |
| --- | --- |
| Agent Runtimes | Direct model connections, OpenCode, Continue, and the preview DeepSeek Harness |
| 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, DingTalk, and Telegram ([live Bot + model acceptance pending](./docs/features/telegram-channel/progress.md)) |
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
## Product tour
| 系统 | 架构 | 格式 |
### Knowledge bases and graphs
### Magic Notes and Smart Heartbeat
Magic Notes displays a continuous record stream, with a narrow left index and
independent right AI comments. Both panes can collapse; thumbnails scroll to a
record while preserving drafts. It supports rich-text entries and paged canvases with flowing text,
handwriting, highlights, images and PDF backgrounds. Import PDF text and export
annotated pages as a raster PDF. Canvas saves are manual; AI comments use the
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.
## Agent Runtimes
| Runtime | Functions | Configuration |
| --- | --- | --- |
| Windows | `x64`、`arm64` | NSIS、便携 ZIP |
| macOS | `x64`、`arm64` | DMG、ZIP |
| Linux | `x64`、`arm64` | AppImage、DEB |
| Direct models | Chat, retrieval, image generation, and built-in tools | GoodBuddy model connections |
| OpenCode | Coding agents, native commands, tools, and skills | GoodBuddy-managed or native OpenCode configuration |
| Continue | Rules, prompt presets, and coding tasks | GoodBuddy-managed or native Continue configuration |
| DeepSeek Harness (preview) | A fixed host with optional plugin capabilities | GoodBuddy-managed OpenAI-compatible connection |
当前尚未配置代码签名和 macOS notarization,系统可能显示安全提示。
Each run reports cancellation, timeouts, token usage, tool calls, and history.
Runtime-specific capabilities and configuration remain separate.
## 从源码运行
## Download
需要 Node.js 24 和 npm:
Download a build from
[GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
| Platform | Architectures | Formats |
| --- | --- | --- |
| Windows | `x64`, `arm64` | NSIS, portable ZIP |
| macOS | `x64`, `arm64` | DMG, ZIP |
| Linux | `x64`, `arm64` | AppImage, DEB, RPM |
The experimental LoongArch `loong64` build is distributed through a separate
preview channel on the [product website](https://mesalogo.github.io/goodbuddy/#download).
Desktop installers do not contain remote Agent payloads. Local projects do not
need an Agent package; managed SSH users download or import one explicitly in
Settings.
Code signing and macOS notarization are not configured yet, so the operating
system may display a security warning.
## Run from source
Requires Node.js 24 and npm:
```bash
git clone https://github.com/mesalogo/goodbuddy.git
@@ -53,20 +117,35 @@ npm ci
npm run dev
```
构建与打包说明见 [BUILD.md](BUILD.md)。
See [BUILD.md](./BUILD.md) for build and packaging instructions.
## 隐私与安全
## Documentation
- 模型请求只发送到用户选择的服务。
- 本地数据默认保存在系统应用数据目录。
- Renderer 不接触原始 Electron API 或模型凭据。
- DeepSeek Harness 插件市场默认关闭;开启并安装第三方插件后,其安装脚本、初始化和 Execute 工具以当前用户权限运行。关闭市场只隐藏目录和管理界面,不会停用或卸载已有插件;安装前会明确确认。Ask 只允许 Host 原生 `read`/`skill` 与 Main 管理的 Web Search/Fetch,不允许调用第三方插件工具,也不能限制插件初始化代码。
- 远程委派仅在用户配置端点和令牌后启用。
- 内网兼容模式允许应用内 HTTP 和非标准 HTTPS 证书;微信凭据和媒体端点仍执行严格校验。
- [Feature matrix and roadmap](./FEATURES.md)
- [Product feature documentation](./docs/features/)
- [UI design system](./UI-DESIGN.md)
- [Build and packaging](./BUILD.md)
- [Release runbook](./docs/development/release-runbook.md)
## 参与贡献
## Privacy and execution boundaries
欢迎提交 Issue 和 Pull Request。请先阅读 [AGENTS.md](AGENTS.md),提交前运行:
- Model requests are sent only to services selected by the user.
- Local data stays in the operating system's application data directory by
default. API keys are encrypted by secure system storage.
- The Renderer has no access to raw Electron APIs or model credentials.
- The DeepSeek Harness plugin marketplace is off by default. Third-party
install scripts, initialization code, and tools run with the current
user's permissions.
- Remote delegation is enabled only after the user configures an endpoint and
token.
- Private-network compatibility permits in-app HTTP and non-standard HTTPS
certificates. WeChat credential and media endpoints remain strictly
validated.
## Contributing
Issues and pull requests are welcome. Read [AGENTS.md](./AGENTS.md) first, then
run:
```bash
npm test
@@ -74,14 +153,18 @@ npm run typecheck
npm run lint
```
## 社区交流
## Community communication
目前采用微信群的方式供大家高效交流,大家可以微信扫码进入社区群:
Scan the QR code to join the GoodBuddy WeChat group:
<img width="1279" height="1306" alt="056bfac87840a95547f9805a8122fc2d" src="https://github.com/user-attachments/assets/b3342635-60a1-484c-959c-f90ba3c39d69" />
<img width="1250" height="1743" alt="8d6cd5dc3f347a0f3f1cf3844071b4d3" src="https://github.com/user-attachments/assets/1c92c1b8-d1f7-4270-be2c-b9543beadc11" />
## 开源许可
GoodBuddy 的原创代码采用 [0BSD License](LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
## License
Original GoodBuddy code is released under the [0BSD License](./LICENSE). You may
use, modify, distribute, and commercialize it freely. Third-party components
and resources retain their respective licenses.
+136
View File
@@ -0,0 +1,136 @@
<div align="center">
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
# GoodBuddy
**模型对话、编程 Agent、本地知识库和定时任务的桌面入口**
[English](./README.md) | **简体中文**
[产品官网](https://mesalogo.github.io/goodbuddy/) ·
[下载](https://github.com/mesalogo/goodbuddy/releases) ·
[功能矩阵](./FEATURES.zh-CN.md) ·
[构建说明](./BUILD.md)
[![Release](https://img.shields.io/github/v/release/mesalogo/goodbuddy?label=release)](https://github.com/mesalogo/goodbuddy/releases)
[![License: 0BSD](https://img.shields.io/badge/license-0BSD-blue.svg)](./LICENSE)
[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20arm64-0078D4)](https://github.com/mesalogo/goodbuddy/releases)
[![macOS](https://img.shields.io/badge/macOS-x64%20%7C%20arm64-000000)](https://github.com/mesalogo/goodbuddy/releases)
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
![GoodBuddy 工作空间](./docs/screenshots/GoodBuddy_zh.png)
GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时任务的桌面应用。使用 GoodBuddy 无需注册账号,工作数据默认保存在本机。模型服务可以运行在本机、组织内网或用户选择的云端服务。
## 产品亮点
- **本地优先**:无需注册 GoodBuddy 账号,工作数据默认保存在本机,模型服务由用户选择。
- **统一 Agent Runtime**:在一个桌面界面中使用直连模型、OpenCode、Continue 和预览版 DeepSeek Harness。
- **统一执行**:请求按所选 Runtime、已启用能力和当前账号权限运行,无需选择工作模式;工具调用、取消、超时和 Token 用量均有记录。
- **支持信创软硬件**:正式 Linux 版本覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境;龙芯 LoongArch 提供独立 `loong64` 实验预览版。
- **知识与长期工作一体化**:知识库、知识图谱、魔法笔记、任务和智能心跳共享同一个桌面工作空间。
信创支持按操作系统、指令集和安装包兼容性描述,不代表已完成所有厂商、整机和系统版本的兼容认证。龙芯版本尚未进入正式发布矩阵,具体限制见[龙芯预览版说明](./docs/development/loongarch-preview-build.md)。
## 主要能力
| 范围 | GoodBuddy 提供的能力 |
| --- | --- |
| Agent Runtime | 直连模型、OpenCode、Continue 和预览版 DeepSeek Harness |
| 数据与执行 | 本地 SQLite 与笔记文件存储、当前账号权限下的工具执行、工具调用记录 |
| 知识库 | 导入文件、目录和网页,通过全文、中文词组、向量和知识图谱检索 |
| 桌面版本 | Windows、macOS、Linux 的 `x64` 与 `arm64` 正式版本,以及龙芯 LoongArch 实验预览版 |
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信、钉钉和 Telegram([真实 Bot 与模型链路待验收](./docs/features/telegram-channel/progress.md)) |
## 产品界面
### 知识库与知识图谱
### 魔法笔记与智能心跳
魔法笔记连续展示全部记录,左侧窄栏索引与右侧 AI 评论可独立收起,点击缩略图滚动定位并保留草稿。
支持富文本记录和分页画布,可混排跨页正文、手写笔迹、高亮、图片与 PDF 底版,
导入 PDF 时提取原生文字,并将批注页面导出为栅格 PDF。画布由用户手动保存;AI 评论按
默认模型的图像输入能力分析,不支持图像时明确提示仅文字降级。具体行为与最终验证状态见
[魔法笔记文档](./docs/features/magic-notes/README.md)。
## Agent Runtime
| Runtime | 适用场景 | 配置来源 |
| --- | --- | --- |
| 直连模型 | 对话、知识检索、图片生成和内置工具 | GoodBuddy 模型连接 |
| OpenCode | 编程 Agent、原生 Commands、Tools 和 Skills | GoodBuddy 管理或 OpenCode 自有配置 |
| Continue | Rules、Prompt 预设和编程任务 | GoodBuddy 管理或 Continue 自有配置 |
| DeepSeek Harness(预览) | 固定 Host 与可选插件能力 | GoodBuddy 管理的 OpenAI 兼容连接 |
每次运行都会记录取消、超时、Token 用量、工具调用和执行历史。各 Runtime 的原生能力与配置仍分别管理。
## 下载
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
| 系统 | 架构 | 格式 |
| --- | --- | --- |
| Windows | `x64`、`arm64` | NSIS、便携 ZIP |
| macOS | `x64`、`arm64` | DMG、ZIP |
| Linux | `x64`、`arm64` | AppImage、DEB、RPM |
龙芯 LoongArch 使用独立的 `loong64` 实验预览通道,可在[产品官网](https://mesalogo.github.io/goodbuddy/#download)选择下载。
桌面安装包不包含远端 Agent payload。本地项目无需 Agent 包;托管 SSH 用户可在设置中单独下载或导入。
当前未配置代码签名和 macOS notarization,操作系统可能显示安全提示。
## 从源码运行
需要 Node.js 24 和 npm:
```bash
git clone https://github.com/mesalogo/goodbuddy.git
cd goodbuddy
npm ci
npm run dev
```
构建与打包说明见 [BUILD.md](./BUILD.md)。
## 文档
- [功能矩阵与路线图](./FEATURES.zh-CN.md)
- [产品功能文档](./docs/features/)
- [UI 设计系统](./UI-DESIGN.md)
- [构建与打包](./BUILD.md)
- [发布流程](./docs/development/release-runbook.md)
## 隐私与执行边界
- 模型请求只发送到用户选择的服务。
- 本地数据默认保存在系统应用数据目录,API Key 由系统安全存储加密。
- Renderer 不接触原始 Electron API 或模型凭据。
- DeepSeek Harness 插件市场默认关闭。第三方插件安装脚本、初始化代码和工具以当前用户权限运行。
- 远程委派只在用户配置端点和令牌后启用。
- 内网兼容模式允许应用内 HTTP 和非标准 HTTPS 证书;微信凭据和媒体端点仍执行严格校验。
## 参与贡献
欢迎提交 Issue 和 Pull Request。请先阅读 [AGENTS.md](./AGENTS.md),提交前运行:
```bash
npm test
npm run typecheck
npm run lint
```
## 社区交流
扫描二维码加入 GoodBuddy 微信群:
<img width="1250" height="1743" alt="8d6cd5dc3f347a0f3f1cf3844071b4d3" src="https://github.com/user-attachments/assets/e2cd4874-0b6c-40d4-8867-0a9e843a7cb5" />
## 开源许可
GoodBuddy 原创代码采用 [0BSD License](./LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
+337 -74
View File
@@ -35,6 +35,7 @@
- 加载、空内容、无结果、失败、只读和权限不足是不同状态,必须分别呈现。
- 可撤销操作优先提供撤销,不可撤销操作必须在执行前说明影响范围。
- 不依赖颜色单独表达状态或风险。
- 当前模型或 Runtime 及切换进度集中显示在会话输入区,顶栏不重复显示可用性标识。切换失败、自动状态检查失败或返回不可用时,通过应用通知说明原因;发送按钮保留关联的无障碍状态说明。
### 2.5 默认支持浅色与深色主题
@@ -42,6 +43,10 @@
两种主题必须保持相同的信息层级,但不要求机械地反转明暗。浅色主题以白色主内容画布、冰蓝灰侧栏和轻微着色的顶栏建立空间关系;深色主题使用深海军蓝与蓝灰表面逐层提亮,避免纯黑。蓝色承担主要选择和交互,青绿色主要承担成功与可用状态,二者不得混用语义。
外观设置提供“透明磨砂特效”开关,默认开启,文字在左、共享 Switch 在右。新用户及缺少该字段的旧配置使用开启状态;已保存的开关选择保持不变。选择保存在应用设置中,保存成功后立即生效,重启后恢复。开启时,右侧内容框架顶栏的背景不透明度在浅色主题为 65%、深色主题为 70%,保留 18px 背景模糊与淡分隔线。聊天滚动视口向上延伸 58px,消息上滚时实际经过顶栏背后;不使用人工渐变充当背景。初始留白放在滚动内容中,包含顶栏高度和当前任务/待办条的实测高度,首条消息仍从这些控件下方开始。任务/待办条固定在顶栏下方并保持实色;展开的 Runtime 待办卡片使用两主题各自的 75% 不透明背景、18px 背景模糊和清晰的文字颜色。
已保存的开关状态同步到文档根节点,卸载时清理,使 body/top layer 中的待办卡片也能应用样式;其他菜单保持原有表面。输入区、右侧工作栏和非聊天页面保持原有位置,聊天加载态及无活动消息面板时不扩展视口。窗口拖拽和控制按钮保持原有行为;该效果不透出操作系统桌面。关闭后恢复顶栏下方的聊天布局,顶栏与待办卡片均恢复实色。`tests/frosted-glass.electron.test.ts` 使用真实 `ChatHistoryPane` 检查消息与顶栏的可见矩形交叠、初始留白、关闭恢复、浅深主题、跳底、历史加载锚点和会话切换。
## 3. 设计令牌
令牌以 CSS 自定义属性实现。`:root` 提供浅色值,`[data-theme="dark"]` 覆盖深色值。组件样式不得新增只服务于单个页面的颜色、阴影、圆角或间距常量。
@@ -67,7 +72,7 @@
| `--success`、`--success-subtle` | 成功状态 |
| `--warning` | 警告状态 |
| `--danger`、`--danger-strong` | 危险文本和状态 |
| `--danger-solid` | 最终确认按钮等需要反白文字的危险背景 |
| `--danger-solid`、`--danger-solid-hover` | 最终确认按钮等需要反白文字的危险背景及悬停背景 |
| `--danger-border`、`--danger-subtle` | 危险入口和失败状态背景 |
浅色与深色具体值只在 `styles.css` 的主题根节点维护。状态组件必须同时显示文字或图标,不能仅靠颜色区分。
@@ -115,6 +120,7 @@
- 业务组件通过 `--font-family-ui` 与字体尺寸令牌继承字体,不创建页面专属字体栈。
- 表单按钮、输入框、选择框和文本域必须继承界面字体,避免回退为原生控件字体。
- 文本表单复用 `.field` 下的共享输入规则;不适合增加字段包装层的独立输入或文本域使用 `.field-control`。文本输入规则排除 Checkbox 和 Radio,保留全局 `:focus-visible` 轮廓,不清除 Switch 焦点环。
- 连续阅读内容使用 `14px`,持久辅助信息不得小于 `11px`。
- 本地兜底字体资源必须随生产包交付,并同时包含 Inter 与 Noto Sans SC 的 OFL 许可证。
- 页面内不得通过同时放大字号、加粗和使用强调色制造多个同级主标题。
@@ -145,8 +151,9 @@
| 变体 | 最大内容宽度 | 适用场景 | 页面映射 |
| --- | --- | --- | --- |
| `reading` | `820px` | 连续阅读、单列编辑、对话撰写 | 聊天正文与输入区 |
| `standard` | `960px` | 常规列表、设置与表单 | 最近对话、设置 |
| `dashboard` | `1040px` | 指标、卡片网格、宽表格与审计数据 | 智能心跳、活动记录 |
| `standard` | `960px` | 常规列表与表单 | 最近对话 |
| `dashboard` | `1040px` | 指标、卡片网格、宽表格与审计数据 | 活动记录 |
| `supervisor` | `1440px` | 选择列表、图谱画布、来源详情三栏工作台 | 监督者(含智能心跳设置) |
| `master-detail` | 可用空间内流式铺开 | 左侧选择、右侧编辑或预览 | 知识库 |
共同规则:
@@ -156,6 +163,7 @@
- 页面顶部和底部内边距默认为 `32px`。
- `master-detail` 的左栏建议为 `280px` 至 `360px`,右栏占剩余空间。分隔线和滚动容器属于壳层,不由内容卡片模拟。
- 聊天输入区可以粘附在 `reading` 壳层底部,但消息内容和输入区必须共享同一阅读宽度。
- 输入区顶部向消息区延伸 `--space-4`(16px)的透明到 `--surface-raised` 渐变,柔化消息裁切边缘;渐变不占布局空间、不拦截鼠标事件,并随主题切换。
- 宽表格可在 `dashboard` 内容区内部横向滚动,不得撑宽整个应用窗口。
## 5. 信息层级
@@ -176,7 +184,8 @@
`PageHeader` 统一接收以下内容:
- `title`:必填,简短名词或任务名称。
- `description`:可选,一行说明当前页面能做什么,不重复标题。
- `description`:可选,需要常显的简短上下文,不重复标题或帮助内容。
- `help`:可选,补充用途和操作说明,通过标题同行的 `InlineHelp` 按需展开,规则见 6.14。
- `scope`:涉及范围的数据页面必填,使用 `ScopeBadge`。
- `primaryAction`:可选,页面唯一主操作。
- `secondaryActions`:可选,最多两个直接显示,其余进入更多菜单。
@@ -193,7 +202,7 @@
### 6.1 PageTabs
用于同一一级页面内的同级内容面板,例如智能心跳的“成长概览”和“心跳计划”。
用于同一一级页面内的同级内容面板,例如监督者的“工作回顾”和“故事线图谱”。
- 使用 `tablist`、`tab` 和 `tabpanel` 语义,当前项使用 `aria-selected="true"`。
- 一级页面之间的导航由应用主导航承担,不复用 `PageTabs`。
@@ -201,6 +210,7 @@
- 项目过多时优先重组信息架构,不把一级页签做成多行。
- `PageTabs` 可使用默认视觉或共享的 `segmented` 视觉变体。紧凑主从工作台中的 2 至 4 个同级面板可使用与模型设置一致的分段外观,但不得因此改用按钮组语义。
- 视觉变体不改变组件含义:分段外观的 `PageTabs` 仍使用页签语义、单一激活面板、游标焦点和方向键切换,不复制页面专属样式。
- 在独立滚动的 Flex 页面中,`PageTabs` 及其直接包装层必须保持 `flex: 0 0 auto`,不得被剩余内容压缩到零高度。新增页签容器时必须提供对应的 CSS 回归测试。
### 6.2 SegmentedControl
@@ -209,6 +219,8 @@
- 选项数量为 2 至 4 个。
- 使用带 `aria-pressed` 的按钮组语义,支持方向键切换。
- 不能用于多选筛选、页面导航或执行即时命令。
- 与 `PageTabs` 一样,共享控件根节点必须 `flex: 0 0 auto`,不能被同级长表单或滚动区域
压缩高度。横向空间不足时保留控件自身滚动,不把标签裁成一条窄缝。
- 控件宽度由内容决定,除移动窄屏外不默认等分整行。
### 6.3 筛选工具栏
@@ -263,6 +275,11 @@
- `danger-solid`:确认对话框中的最终危险操作。
- `danger-zone`:设置页中集中展示的高影响操作区域。
`danger-solid` 必须是全局共享类,不依赖页面祖先;通过 Portal 挂到 `body` 后仍须生效。
尺寸、圆角、字体、间距和禁用状态与 `primary-button`、`secondary-button` 共用规则,
键盘焦点沿用全局 `button:focus-visible`。背景及可用态悬停使用危险实心色令牌,文字使用
`--text-on-accent`;禁用时不触发悬停变色。
普通页面不得用实心红色按钮与主操作并列。危险入口必须使用具体动词和对象,例如“删除知识条目”,避免只写“确定”。
### 6.7 数据表格与活动列表
@@ -280,74 +297,191 @@
- 浅色侧栏使用冰蓝灰表面,与白色主内容画布形成明确边界;深色侧栏使用比主画布略亮的蓝灰表面。
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
- 当前导航项和当前会话使用一致的淡蓝选中背景、强调色文字和中等字重,不额外描边或添加侧边标记;导航图标随选中状态使用强调色,悬停时保留选中状态。浅深主题遵循相同规则,强制颜色模式保留系统选中边框。
- 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与会话区域之间、会话区域与底部入口之间的分隔线上下均各留 `8px`(`--space-2`)。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
- 最近会话列表在精细指针设备上使用细原生滚动条,滑块使用 `--border-control`、轨道透明;强制颜色与粗指针设备保留系统默认。
- 会话区域使用 `--surface-raised` 与主导航区分,浅色为白底,深色为相邻的深蓝灰底。不显示“最近会话”标题,保留区域的无障碍名称,列表独立滚动。搜索与新对话共用主导航上方的一行,不在会话区域另设搜索头部;展开、关闭和键盘规则见[会话搜索](./docs/features/application-tool-navigation/ui-design.md#会话搜索)。搜索框使用透明背景、`--border-control` 边框与完整焦点环。
- 窗口高度不超过 `600px` 时,侧栏允许整体纵向滚动,会话区域至少保留 `200px` 高度,避免搜索框、列表与底部入口相互覆盖。
- 最近会话的顺序不随流式输出增量反复变化;置顶与新消息排序遵守
[会话列表排序规则](./docs/features/application-tool-navigation/ui-design.md#会话置顶与操作菜单)。
- 项目选择器顶行保留项目切换与新建按钮;项目设置统一从下拉列表对应行的管理按钮进入,关闭后焦点返回项目选择器。
- 项目选择器与下方默认的新建对话按钮等宽,两行右侧图标按钮均宽 `36px`,与左侧控件间距均为 `8px`;搜索展开时项目选择器宽度保持不变。
- 项目下拉列表通过 `FloatingPortal` 浮在侧栏与会话区上方,默认宽度 `380px`,不受侧栏裁剪;距窗口边缘至少 `16px`,窄窗口自动收窄,长列表内部滚动。浮层锚定项目选择按钮,窗口、滚动和尺寸变化后重新定位。
- 项目选择器使用固定双行单按钮,第一行显示项目名,第二行显示跨项目活动汇总或“暂无活动”;点击任一行打开统一菜单并默认预览当前项目,全部项目范围在菜单内切换。尺寸、焦点、项目计数与菜单遵守
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
活动数量不改变项目菜单的稳定分组和顺序。
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
并记住用户选择;分隔条使用 `separator` 语义,支持方向键、Home 和 End。小于
`900px` 时继续使用固定宽度抽屉,不显示拖拽分隔条。
- 关联 Task 的 Conversation 在行最左侧显示独立展开按钮,父会话行不重复任务标签或数量;
展开后的每个 Task 子项使用共享状态点,展开层级只到 Task,不展示 Job、Subjob 或 Run。
- 会话标题保持单行且不挤压时间或操作按钮。标题实际溢出时,悬停会话行才在标题的固定裁切
区域内平滑横向滑动以展示完整名称;移开后快速复位。未溢出标题不产生位移动效,
`prefers-reduced-motion` 下禁用滑动并保留完整标题提示。
展开后的每个 Task 子项左侧显示名称,计划和共享状态标记收在右侧,与父会话的
更新时间位置一致;展开层级只到 Task,不展示 Job、Subjob 或 Run。
- 会话标题保持单行且不挤压更新时间。更新时间默认靠近行右侧;低频的更多操作按钮不常驻
占位,只在悬停会话行时替换右侧时间显示。键盘焦点进入会话行或操作菜单展开时,按钮仍
保持可见。标题实际溢出时,悬停会话行才在标题的固定裁切区域内平滑横向滑动以展示完整
名称;移开后快速复位。未溢出标题不产生位移动效,`prefers-reduced-motion` 下禁用滑动
并保留完整标题提示。
- 分支会话在侧栏标题前和聊天顶栏标题旁显示同一个横向分叉徽标;徽标通过可访问名称和
工具提示说明来源会话,不能只靠图形表达。分支不改成树状会话列表,也不挤压更新时间。
- 新建 Task 后可以首次自动展开所属 Conversation;用户手动折叠后保持其选择,后台状态变化
只更新状态提示,不强制展开或抢占焦点。
- Task 子项复用任务中心的共享状态点:运行中使用脉冲强调色,已完成使用静态成功色,
失败或中断使用危险色,等待审批使用警告色,暂停或取消使用禁用色;旁边同时显示
本地化状态文字,不能只靠颜色或动效表达状态。
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
- Task 子项右侧复用任务中心的共享状态标记:运行中使用脉冲强调色,已完成使用带勾成功点,
失败或中断使用危险色,等待审批使用警告色,暂停或取消使用禁用色;非完成态旁边同时
显示本地化状态文字,已完成文案保留给辅助技术而不在紧凑元数据中重复显示,不能只靠
颜色或动效表达状态。
- 左上角品牌区允许用户在“外观”设置中定制品牌名称、中英文副标题和单个 Logo。定制只
影响该品牌区,不替换安装包名称、更新协议、Agent 标识或应用正文中的 GoodBuddy 产品
身份。副标题按当前界面语言显示,留空时隐藏且不保留空行。
- 品牌设置必须提供同布局预览、显式保存、默认 Logo 和整组恢复默认操作。自定义 Logo 仅
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 侧栏底部提供“应用 / Apps”和“设置 / Settings”两个横向等高 `nav-item` 按钮,均显示图标和文字,中间使用弱竖分隔线。“应用”带向下箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”使用 `LayoutGrid` 图标,打开既有整窗管理 Modal,标题仍为“应用中心 / Application Center”,共同应用设置沿用设置 Modal。
五个内置应用的管理列表、菜单和侧栏共用保存顺序,菜单按启用过滤,侧栏按启用且常驻过滤。知识库始终启用并常驻,可调整位置;管理 Modal 提供“打开”和排序,不提供启用、常驻开关或通用空设置,标签为“始终显示”(英文 `Always shown`)。监督者沿用智能心跳身份,可修改启用和常驻,执行边界见[监督者逻辑](./docs/features/conversation-supervision/logic-design.md#应用启停与执行)。魔法笔记、本机推理监控同样可修改启用和常驻。设备共享在测试阶段默认关闭,标签为“可选”及启用状态,提供启用设置和排序,不提供常驻开关;启用后可从应用菜单及管理 Modal 在主内容区打开元数据页面,关闭时退出该页,规则见[设备共享 UI](./docs/features/device-sharing/ui-design.md#当前首期页面)。当前对话、运行记录和设置保持固定系统位置,不参与应用排序。
设置持久化后的事件同步各入口及工具配置写入;重开中心刷新期间锁定修改,新快照不被旧读取覆盖。文件、终端、浏览器和资源监控只通过右侧工作栏访问,不在左侧复制入口。
管理 Modal 始终使用紧凑单列横向列表,纵向对应保存顺序;左侧为图标、名称及简短用途,右侧为打开、可用的设置及上下移动操作,行间使用弱分隔线,不加卡片外框。窄窗口操作换行且不溢出,保留共享键盘焦点环。可选应用关闭时禁用“打开”。启用、常驻位于紧凑设置详情;五行均提供上下移动及拖动排序,不受启用或常驻影响,箭头按完整顺序判断边界。搜索时禁用拖动,箭头仍按完整顺序移动,不丢失未命中项。尺寸、返回与滚动重置规则见[应用中心 UI 设计](./docs/features/application-tool-navigation/ui-design.md)。已有稳定设置入口时,不在顶栏重复提供同一入口。
- 本机推理监控 Modal 复用整窗 Portal、背景隔离和焦点循环;停止、重启及释放使用其上方的紧凑影响确认。Escape 只关闭最上层,操作提交期间禁止关闭及重复提交;焦点恢复与尺寸见[本机推理监控 UI 规则](./docs/features/application-tool-navigation/ui-design.md#91-本机推理管理)。
### 6.9 助手工作栏
助手工作栏是应用级右侧工具容器,不归属于聊天页面,也不根据当前页面、项目或 Runtime
自动增删入口。产品契约见
[通用助手工作栏与执行空间 PRD](./docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
[通用助手工作栏与执行空间 PRD](./docs/features/assistant-workbar/prd.md),
动态 Tab、“+”和终端细节见
[助手工作栏多终端页签 PRD](./docs/features/assistant-workbar/terminal-tabs-prd.md)。
- 默认固定提供任务中心、上下文、工作区、浏览器和成果五个标准栏目,不根据当前页面或
Runtime 能力自动增删入口。
- Task Center 是 Task 的单例应用级索引,不使用“跟随 / 固定目标”多实例模式。每个 Task
只关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,
也不得把 Job/Run 提升为可导航 UI 对象。
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
- 用户可以主动排序或隐藏栏目,并可恢复默认布局;应用不能用用户偏好机制实现自动能力裁剪。
- 五个栏目使用稳定标签并保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
- 升级后默认打开任务中心、工作区和一个浏览器实例。任务中心与工作区是不可关闭的
单实例基础应用;浏览器是可关闭的多实例应用。成果页及其导入入口已移除。不可关闭不
等于锁定顺序。
- 工作区以实际目录文件为准,提供向所选目录导入文件、本机复制及远程上传。文件列表按名称、
修改时间或可用的创建时间排序,目录优先,缺失时间显示短横线且排序置后。窄栏在文件视口内
横向滚动。创建时间只取文件系统 birthtime,不使用 Linux ctime。会话附件和图片保留在消息中。
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
活动时采样,首期不持久化历史或提供后台负载告警。
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
- 应用注册表分别声明单实例或多实例、默认打开、必须存在、可关闭和排序策略。在工作栏“+”目录中,
单实例重复选择时聚焦已有 Tab;浏览器和终端每次选择创建独立实例。选择已有 Tab 只激活
该实例,保持其目标和历史;主侧栏不再提供按当前上下文匹配实例的重复工具入口。
- Task Center 是默认跟随当前项目的单实例索引,不使用多实例模式。用户可在同一 Tab 内切换
“当前项目”和“所有项目”;无项目任务在“所有项目”中标注“未绑定项目”。范围变化不得创建第二个 Task Center。每个 Task 只
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
不得把 Job/Run 提升为可导航 UI 对象。
- 各栏目按声明的范围读取内容;笔记使用应用级数据,选择和草稿不跟随项目或会话变化。当前内容不可用时仍保留栏目入口并说明原因。
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。笔记遵循应用中心的业务启用设置,禁用前处理草稿,禁用后隐藏目录入口并移除笔记 Tab。
- 用户可以主动排序栏目,并可关闭可关闭实例或恢复默认布局;任务中心和工作区不能隐藏或
关闭。应用不能用用户偏好机制实现自动能力裁剪。
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。不可关闭实例不渲染
关闭按钮,也不响应关闭快捷命令。
“+”位于滚动容器外并占用固定宽度;溢出时左右滚动箭头在滚动视口两侧各占独立宽度,不能覆盖页签、关闭按钮或添加入口。箭头隐藏或禁用时保留占位,避免悬停或滚动引起布局跳动。
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
- 工作栏 Tab、Tab 关闭按钮和“+”入口沿用共享控件圆角;终端面板外框及终端顶部工具栏按钮
使用直角,形成紧凑的停靠式终端界面。
- 选择“终端”后直接绑定当前项目,由项目执行空间自动决定本机或托管 SSH;不再显示第二层
目标选择器。终端面板内部不重复提供关闭会话按钮,关闭统一由 Tab 入口完成。
- 下一次请求使用的附件和知识库由会话输入区展示与管理,不在工作栏重复提供“上下文”栏目;
记忆功能实现前不展示占位内容,未来的记忆与历史执行上下文归入关联 Task 的任务中心详情。
- 徽标可以提示未解决意见、等待审批、失败或连接状态,但不能成为唯一状态信号,也不能无条件抢占当前栏目。
- 宽窗口可停靠并调整宽度,中等窗口可停靠或覆盖,窄窗口使用全屏或接近全屏抽屉;所有尺寸下均须保留全部栏目入口。
- 覆盖和抽屉布局打开后焦点进入工作栏并限制在其中,Escape 或背景点击关闭,关闭后焦点
返回触发按钮;覆盖期间背景内容必须从指针与辅助技术导航中隔离。宽窗口停靠布局不得
获得对话框语义或隔离主工作区。
- 浏览器 Tab 的内容顶部固定使用一条紧凑工具栏,按“返回、刷新/停止加载、地址输入、
前往、交互、关闭浏览器”排列。“前往”使用纯右箭头图标按钮,保留悬停提示和可访问名称。“刷新”在当前页面加载时原位切换为“停止加载”;
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
尚未提交的文字改变 Agent 看到的页面。
- 浏览器工具栏操作的是该实例绑定的 Conversation 和 Browser Tab。同一 Conversation 的多个
Tab 共享隔离登录上下文,但页面、导航、操作队列和元素引用相互独立。模型请求开始时冻结
工具目标,用户切换工作栏 Tab 不改变进行中的 Agent 操作。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
- 所有窗口宽度下均停靠在主工作区右侧并参与水平布局回流;展开时压缩主工作区,不能覆盖、
遮罩或隔离其内容。分栏计算排除最左侧主导航,主工作区与工作栏采用相同的 `300px`
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
所有可容纳两侧最小值的尺寸均支持指针和键盘调整,并须保留全部栏目入口。
- 工作栏分隔条须保留完整的 `9px` 拖拽命中区域,位于侧栏内部,不能被容器裁剪;栏目内容
和原生浏览器视口须避开该区域。浏览器全屏时隐藏分隔条并取消这段预留空间。
- 工作栏开关固定在顶栏 `.topbar__actions` 内、紧邻浅色/深色主题按钮之前,同一个按钮负责展开与收起并持续暴露状态;图标中的
箭头方向必须随当前动作切换,收起状态提示展开,展开状态提示收起。按钮保持普通图标按钮
视觉,不通过持续高亮表达展开状态。工作栏内部不再重复关闭按钮;当前会话没有 Task 时,
不得为了放置该开关而保留一条空上下文栏。
- 停靠工作栏不得获得对话框语义、焦点陷阱或背景遮罩。关闭后焦点返回同一个工作栏开关;
主工作区与工作栏在展开期间都保持可达。
- 对话消息流、输入区和输入区元信息必须在主工作区内部保留对称的左右安全边距。工作栏
分割线只能位于主工作区边界之外,不能覆盖、裁切或挤掉输入框边框、发送按钮或右侧边距。
- 终端、宽日志和大型成果可以由用户切换到底部停靠或独立窗口,应用不得因内容变化自动改变用户已选布局。
### 6.10 应用顶栏与全局操作
应用顶栏用于窗口级状态、侧栏开关和低频全局操作,不承担页面标题或主要导航。顶栏必须保持紧凑,不能与页面内容争夺注意力。
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
顶栏必须保持紧凑,不能与页面内容争夺注意力。
- 顶栏高度默认为 `58px`,图标按钮使用 `34px × 34px` 点击区域。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
- 应用壳层在水平方向分为全高主侧栏和右侧内容框架。主侧栏从窗口顶部贯穿到底部,品牌区、
项目切换器、导航和账户区保持同一连续表面;不能用全宽标题栏把整条侧栏向下推移。
- 无边框窗口的紧凑标题栏只覆盖右侧内容框架,保留侧栏开关、当前会话标题、范围、Runtime
状态、主题开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
`34px × 34px` 点击区域。
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
- 整窗固定遮罩通过 `createPortal(..., document.body)` 挂载,避免页面层叠上下文影响;
保留 React Context、关闭与焦点管理,表单样式不得依赖已移出的页面祖先选择器。
- 共享锚定菜单 `AnchoredMenu`、同行帮助 `InlineHelp` 和应用通知共用 `FloatingPortal` 的非模态 `popover="manual"`,由浏览器 top layer
绘制,避免被 Modal 或滚动容器遮挡。菜单和帮助挂在触发按钮所属 Modal 内,通知跟随当前活动
Modal,无 Modal 时挂在 `body`;不能只提高 `z-index` 或解除背景的 `inert`。
- 原生浏览器 `WebContentsView` 不受网页 `z-index` 控制。应用级 Modal 打开时统一释放
浏览器 viewport;非模态对话框、菜单、同行帮助 tooltip、局部确认或应用通知与浏览器区域相交时也释放。
浮层关闭后只恢复当前活动实例的 viewport,不关闭、重建或刷新网页。接线与测试见
[浏览器多实例技术设计](./docs/features/assistant-workbar/browser-tabs-technical-design.md#4-renderer-与-viewport)。
- 遮罩必须 `inset: 0` 铺满整窗,并由 `styles.css` 中统一的遮罩选择器声明
`-webkit-app-region: no-drag`,否则 `.brand` 与 `.topbar` 的原生拖动区域会截获指针,
使遮罩上的控件无法悬停和点击。新增整窗遮罩时扩展该共享选择器,不要在各自规则里复制
`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。遮罩回归按 fixed/inset 声明发现,不能只按 `backdrop` 或 `modal` 类名后缀筛选;知识引用的 `-dialog` 同样适用。
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
对齐。该栏不重复放置新建入口。
- “新建任务”只位于工作栏“任务索引”标题后方;任务状态筛选占满工作栏可用宽度,四个选项
等分,不能保留内容宽度的短控件。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。顶栏 Runtime
状态由状态点和 Runtime/模型名称紧凑表达:就绪使用绿色,连接中使用黄色,不可用使用灰色,
不再重复显示状态文字;完整状态详情继续通过可访问说明和工具提示提供。
- 浅色与深色切换属于持续可用的窗口级操作,直接显示太阳或月亮图标,并通过可访问名称说明将切换到的主题。选择必须持久化,切换不得改变布局。
- 顶栏只直接显示当前任务所需的高频操作。已有侧栏账户设置入口时,不再重复显示 Runtime/设置入口;使用帮助优先放在相关操作附近,而不是为单个帮助项创建“更多”菜单。
- 助手工作栏开关固定在顶栏 `.topbar__actions` 内、紧邻浅色/深色主题按钮之前;展开后该开关保持原位并
负责收起,不在工作栏标题区域增加第二个关闭入口。
- 只有存在至少两个无法由稳定入口承载的低频全局操作时才增加全局菜单,不为了容纳一个冗余入口而显示省略号按钮。
- 窄窗口下优先压缩状态标签并保留图标按钮,不隐藏窗口控制、当前范围或进行中的风险状态。
- 使用全局菜单时,菜单项使用 `--font-body`、`14px` 图标和约 `32px` 单项高度;标签使用短名称。菜单保留 `menu`、`menuitem` 语义,支持上下方向键、Home、End 和 Escape,关闭后焦点返回触发按钮。
### 6.11 单选选择器
模型、专家角色、工作模式、Runtime Agent、Runtime 预设和 Runtime 快捷操作属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与多个风格不一致的原生下拉框。
模型、专家角色、Runtime Agent、Runtime 预设和 Runtime 快捷操作属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与多个风格不一致的原生下拉框。
- 只有选项能由单行短标签充分区分、不需要补充来源、位置、状态或说明时,才使用原生 `select`。
- 当用户必须在选择前比较来源、范围、位置、连接状态或其他辅助信息时,使用与输入区选择器同类的自定义富信息单选菜单,不用 CSS 修饰原生 `select` 冒充富信息菜单。
- 触发按钮复用统一的模型选择按钮样式,保持相同高度、圆角、边框、展开指示和焦点状态。
- 菜单使用 `menu` 与 `menuitemradio` 语义,当前项同时显示选中标记和 `aria-checked`。选项可以包含一行简短说明,但标签和说明不得被截断到无法区分。
- 支持上、下方向键、Home、End、Enter 或 Space、Escape;打开后焦点进入当前项,关闭后返回触发按钮。
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和模式菜单互斥展开。
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和 Runtime 菜单互斥展开。
- 不可用选项保持可读并说明原因,键盘导航不得停留在不可选择项上。
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称和项目类型,并按数据可用性补充本地目录、远程来源、平台、连接状态或其他能帮助辨认项目的信息。
- 项目菜单当前区分“本地项目 / 远程通道”。未来出现真正的远程项目时新增独立分组,不把消息通道命名为远程项目。分组、项目类型和连接状态均须显示文字,不只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
- 当某个选项需要用户在其展开区域内完成路径选择、授权或其他前置操作时,不得因为前置条件
尚未完成而禁用该入口。选择入口只展开流程,验证成功前不提交实际值;不得自动采用第一个
候选。取消保持原值,验证错误在对应操作附近显示。
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称,并按数据可用性补充目录、远程来源或其他能帮助辨认项目的信息。托管 SSH 项目在“远程项目”内按已保存 SSH Host 做一层轻量分组,Host 标题旁显示真实 Agent 连接状态;项目副标题只显示远端路径,不重复“托管 SSH”类型。
- 项目菜单区分“本地项目 / 远程项目 / 远程通道”,不把消息通道命名为远程项目。“远程项目”关闭时完整移除远程项目分组,而不是显示不可操作的残留选项;本地项目和远程通道继续可用。Agent 连接状态使用“未连接 / 连接中 / 就绪 / 异常”等文字标签,读取状态不得发起连接,且不能只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
### 6.12 应用通知与就地反馈
应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。
- 右上角通知使用紧凑的中性表面、弱边框和轻阴影,最大宽度为 `360px`,窄窗口保留两侧各 `16px`。状态色仅用于图标,不铺满卡片;正文直接呈现,通用状态标题仅供屏幕阅读器读取。图标、正文与关闭按钮统一垂直居中,不使用各自的上下偏移。
- 异步操作成功、无需立即处理的信息,以及不属于某个字段的异步失败,使用应用级 `success`、`info` 或 `error` 通知。
- 成功和信息通知默认在约 4.5 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。
- 成功和信息通知默认在约 2 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。
- 通知出现不抢焦点;Modal 打开时,通知关闭按钮纳入当前 Modal 的 Tab 循环。聚焦通知时
Escape 返回原控件或当前 Modal 的可用控件,不关闭 Modal;关闭通知后同样恢复焦点。
打开、关闭或切换 Modal 不重建通知,不重置自动消失计时。
- 同一语义和文案的重复通知应去重。通知正文必须有长度上限,不包含凭据、私人内容或未脱敏的提供商响应。
- 字段校验、破坏性确认、操作进度、阻塞整个页面的状态,以及需要就地重试或修正的错误保留在相关控件附近。
- 就地错误必须与对应字段或操作建立程序化关联;全局错误使用 `alert` 和 assertive 实时区域,成功与信息使用 `status` 和 polite 实时区域。
@@ -362,10 +496,35 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开
- Checkbox 保留原生 Checkbox 语义和方形勾选视觉,不得添加 `role="switch"`。多项分配、列表选择、确认声明和“保存时清除密钥”等一次性选择均属于 Checkbox。
- 不创建页面专属 Switch 样式。需要紧凑布局时仍复用同一轨道、滑块、焦点环、禁用状态和动效,只调整共享组件支持的布局变体。
- Switch 支持 Tab 聚焦和 Space 切换,键盘焦点至少显示 `2px` 高对比焦点环。可见标签应描述被控制的能力,不能只显示“开 / 关”。
- 共享开关行统一为标签在左、Switch 靠右,说明放在标签下方;不因 JSX 中输入控件与文字的先后顺序改变视觉位置。窄窗口允许文字换行,开关保持固定尺寸。Checkbox 和 Radio 保留各自的选择布局。
- 异步切换期间禁用重复操作并保留原状态。失败时恢复或保留最后确认状态,通过应用通知或就地可恢复错误说明原因。
- 涉及联网、上传、电脑控制或其他外部影响的 Switch,附近必须持续说明数据去向、权限范围或风险,不能只靠设置名称表达影响。
- 自动化测试应按 `switch` 角色查询二元开关,按 `checkbox` 角色查询多选或确认项,防止视觉迁移后语义回退。
### 6.14 InlineHelp 同行帮助
页面、区块和字段的补充用途、格式及操作说明统一收进标题或标签旁的 `InlineHelp` 问号按钮。
图谱图例使用同一组件的 `info` 图标变体,只显示 i 图标,保留可访问名称与简短 title;浮层和键盘行为相同。
`PageHeader` 与 `SettingsSectionHeader` 通过 `help` 接收这类内容;`description` 保留需要常显的上下文,两处不重复说明。描述缺省或为空时不渲染段落,不保留空行。
当前范围、状态、字段错误、操作进度、风险、费用和影响用户决策的限制仍就地可见。混合文案先拆分,再将补充说明移入帮助。
- 标题或标签与问号使用共享 `inline-help-label` 同行排列,不为帮助另占说明行;字段标签通过 `htmlFor` 关联控件,帮助按钮放在 `label` 外,避免点击帮助触发输入或开关。
- 悬停或键盘聚焦按钮时展开;指针可移入浮层阅读。点击(含 Enter、Space 激活)固定展开,再次点击关闭;未固定时,指针和焦点均离开后关闭。
- Escape、点击外部或焦点移到外部关闭帮助。Escape 优先关闭帮助,不同时关闭所属 Modal;展开和关闭不主动转移焦点,不建立焦点陷阱。
- 按钮提供与标题或标签对应的可访问名称、`aria-expanded`,展开时通过 `aria-controls`、`aria-describedby` 关联 `role="tooltip"` 内容。提供稳定帮助 ID 时,关闭状态保留同 ID 的隐藏说明,展开时由浮层替换,确保字段的 `aria-describedby` 始终有唯一目标。持久标签与必要提示不得依赖浮层。
- SSH 密码字段常显认证用途和验证成功后加密保存的说明;Main 处理及不写入命令行、日志、普通设置的细节收进同行帮助。应用常驻说明与关闭后果使用独立翻译键,关闭后果保持常显,不按标点拆分翻译文本。
- 浮层复用 `FloatingPortal`,遵循 6.10 的 Modal 归属规则。宽度最多 `360px`,四周保留 `16px` 视口边距;空间不足时翻转或调整位置,长内容在浮层内滚动,窗口及滚动变化后重新定位。
- 问号使用共享 `28px × 28px` 点击区域、焦点环与主题令牌。不得另做原生 `title` 长说明或页面专属帮助浮层。
### 6.15 表单 Modal
- 复用 `styles.css` 中的 `custom-task-dialog__surface`、`custom-task-dialog__header`、`custom-task-dialog__content` 和 `custom-task-dialog__actions`,与定制任务、SSH Host 等现有弹窗使用同一结构;不为单个业务另建 Modal 框架。
- 标题放在 `header` 左侧,右侧只放共享 `icon-button` 的 X 关闭按钮,使用本地化 `aria-label` 和工具提示。取消不放在标题下方或表单顶部。
- `footer` 使用共享操作区,按钮整体右对齐,次要取消在前、主要保存在后。放弃修改确认沿用同一操作区,继续编辑在前、危险操作在后。
- header、滚动 body、footer 是 surface 的三个直接子区域。沿用共享 Grid 的 `auto minmax(0, 1fr) auto`,只有 body 滚动,短窗口仍可见标题、关闭和提交操作;窄窗口沿用共享外边距、内边距和主题令牌。
- X、取消、Escape 和遮罩关闭共用业务关闭检查,不绕过未保存确认或提交锁定。确认中的 Escape 或 X 返回编辑;继续复用 `activateModalFocus`、`trapTabFocus`,保留背景隔离、初始焦点和关闭后的焦点恢复。保存失败保留草稿及可恢复错误。
- 从已有 Modal 打开的子 Modal 同样复用 `--z-dialog` 和 `activateModalFocus`,以较后挂载的 body Portal 绘制在父层上方,并隔离父层;不能只手动聚焦或使用旧的页面层级常量。子层关闭后恢复父层的隔离状态与触发按钮焦点,Escape 不继续关闭父层。
## 7. 交互状态
所有可交互组件必须实现:
@@ -381,6 +540,8 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开
异步提交成功后更新内容并通过统一应用通知提供明确反馈。失败时保留用户输入和筛选上下文。
共享控件的状态规则须在容器组合中生效:菜单通用按钮样式不能覆盖危险按钮,普通图标悬停不能覆盖危险图标或作用于禁用按钮,页签悬停保留当前选中态。深色主题通过令牌提供控件颜色,避免用页面祖先下的通用输入规则替换控件边框。验证时读取最终 computed style;共享令牌对比度与源码规则存在性检查不能代替容器内、body Portal 和伪状态回归。修复与验证记录见[共享控件审查](docs/quality/shared-controls-review-2026-09-27.md)。
## 8. 范围与数据语义
### 8.1 页面范围
@@ -505,24 +666,56 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.1 聊天
- 使用 `reading` 壳层,消息流与输入区共享宽度。
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
- 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。
- 模型、专家角色、工作模式、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。
- 输入区第一行工具栏只承载附件、语音、知识范围、专家角色、工作模式、Runtime 选择和发送等通用操作。OpenCode Agent、Continue 预设及 Runtime 快捷操作必须放入其下方独立的 Runtime 专属功能行,通过可见分组名称、顶部边界和差异化表面与通用操作分层;该行只承载对当前消息生效的高频选择,当前 Runtime 没有可选专属功能时不保留空行。
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
原文,不包含推理过程、工具日志或引用元数据。复制成功后,当前按钮显示使用 `--success`
的绿色对号,可访问名称和工具提示同步表示已复制,3 秒后恢复复制图标;重复复制成功重新
计时,不显示成功通知。失败仍通过应用错误通知反馈。流式回复完成前不显示复制入口。
- 默认开启“会话 HTML 渲染”时,已完成 Agent 回复中的完整 HTML 与 `html` / `htm`
代码块在原位置显示隔离静态预览;用户消息、推理过程和流式中的回复不得执行 HTML。
每个预览持续标注脚本和网络已禁用,并在标题右侧复用与 Mermaid 图表相同的图标按钮组:
源码按钮使用代码图标并暴露 `aria-expanded`,全屏按钮使用放大图标打开统一 Modal。
全屏预览遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape 关闭和焦点恢复规则,
并沿用同一套清理与 Sandbox/CSP 边界。关闭平台功能后必须显示 HTML 源码,不能隐藏内容
或保留旧预览。
Tab 和 Shift+Tab 必须能进入及离开 iframe;焦点在预览内部时 Escape 仍关闭当前全屏层并返回入口。空 sandbox 与禁脚本 CSP 保持不变,跨文档键盘接线见[HTML 渲染技术设计](docs/features/conversation-html-rendering/technical-design.md#ui-与无障碍)。
- Mermaid 大图支持缩放和拖动;图形超过画布时,四个边缘都必须能通过滚动到达,不能因居中而裁掉顶部或左侧。
弹窗工具栏提供“导出 PNG”,保存完整图形,不受当前缩放和滚动位置影响;背景使用当前主题的画布底色。
导出按原图四周留出 `16px`,以 2 倍分辨率绘制,任一边最多 `4096px`。导出期间禁用重复点击,失败在按钮旁提示重试。
- 本地会话的更多操作提供“在新会话中继续”。创建前必须先持久化当前稳定消息;有活动运行
或待发送内容时不可创建。新会话复制聊天内容与当前 Runtime/知识检索选择,重置压缩状态,
使用新的会话和消息 ID,且不复制 Task、队列、运行、记忆或成果归属。
- 模型和能力范围属于上下文控制,不与页面导航页签混用。
- 模型、专家角色、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。
- Runtime 选择菜单必须反映当前执行空间的真实能力。托管 SSH 项目当前只支持 OpenCode,因此输入区只渲染已配置的 OpenCode 选项和管理入口,不显示直连模型、Continue 或 DeepSeek Harness 的不可用入口;旧会话保存的其他 Runtime 选择在项目激活时恢复为当前 OpenCode 配置。
- 输入区使用一个完整容器,主工具栏保留附件、语音、Runtime、会话选项及停止/发送。知识范围、专家角色、OpenCode Agent、Continue 预设和 Runtime 快捷操作收纳在会话选项浮层中;Runtime 专属选择在浮层内按提供方分组,无可选功能时不保留空分组。输入框上方以简短摘要显示生效的专家、知识范围、检索方式和 Runtime 专属选择。
- 会话选项浮层与主行菜单互斥,内部复用上下文选择菜单;子菜单就地展开,浮层统一限制宽高并滚动。Escape 先关闭子菜单并恢复其触发按钮焦点,再关闭会话选项并恢复入口焦点;点击外部、焦点离开、切换会话或开始/结束回复时关闭浮层。
- 会话选项入口紧随录音键,使用无常驻边框的调节图标,保留可访问名称、悬停说明和展开态反馈;浮层在工具栏左侧展开,最大宽度为 `300px`,窄窗口不超过输入区。浮层内选择器使用整行轻量列表样式,避免重复的按钮边框;知识库行右侧显示展开箭头,展开时朝上。
- OpenCode、Continue 和 DeepSeek Harness 后续的 Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止及原生会话监督统一进入应用级助手工作栏固定的“Runtime”栏目,不加入 Composer。栏目入口始终存在;只选择 Conversation 或 Task,并按 Task 聚合执行状态,不显示 Job/Run 树或独立操作菜单。内部可选区域按用户所选目标和 Runtime 的真实能力显示,不为未支持能力渲染空卡片或成排禁用按钮。切换跟随目标时必须清理上一归属的监督状态,固定目标则保持不变。
- 设置中心只管理持久 Runtime 配置、默认值和能力清单;右侧 Runtime 栏目管理用户当前跟随或固定目标的生命周期。两处不得复制同一实时操作,栏目中的高风险操作仍须就地确认并保留取消、权限、用量和活动审计。
- Runtime Prompt 快捷操作只把模板填入输入草稿,用户可以继续编辑;OpenCode Command 由 Runtime 原生 API 执行,输入框只承载可选参数,不以普通斜杠文本冒充执行。
- Agent 回复进行中锁定模型、专家角色、工作模式和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。
- OpenCode 原生 Task 调用在本地项目和托管 SSH 项目中统一解析为子 Agent 状态卡,不显示为普通工具卡。状态卡持续显示 Agent 类型、任务说明、运行状态、完成输出或失败原因;身份徽标、Agent 名称和状态位于首层,任务说明作为主信息,Runtime 来源作为次级元数据,输出与错误在同一卡片内按需展开。父 Agent 的推理不冒充子 Agent 过程,必须与状态卡保持明确间距。状态不能只靠颜色表达。推理、子 Agent、工具和正文统一按事件首次出现顺序进入消息块;增量更新只更新原位置,Task 临时工具块重分类后必须原位替换,不能把子 Agent 卡统一提升到消息顶部。同一调用的增量事件按稳定调用 ID 更新,不能因 ACP 首帧尚无参数而永久保留重复工具卡或活动记录。一次请求内已观察到的子 Agent 均保留在消息与活动记录中;父请求取消、失败或意外中断时仍处于活动状态的卡片必须结算为对应终态,父请求成功结束但子 Agent 未报告终态时必须显示为失败而不是推断为完成。
- Agent 回复进行中锁定模型、专家角色和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。
- 普通工具在主会话和子 Agent 过程中使用同一轻量折叠记录:不显示外层“工具执行”标题、计数、背景和边框,保留无障碍区域名称。记录纵向左对齐,头部按内容收宽且不超过消息宽度;展开箭头在左,工具名作为主标题,有额外信息的单行摘要弱化显示。右侧以绿色对勾显示成功完成,以图标显示运行和待执行状态,提供悬停文字和无障碍名称;失败和可恢复状态保留文字。仅重复工具名和执行状态的已知 Runtime 模板摘要在头部与详情中均隐藏;刷新重试、审批等额外提示保留。默认折叠,状态和输出更新不改变用户的展开选择;折叠时露出一行错误,展开详情使用消息可用宽度、浅背景和左侧细线,按错误、结果、独立折叠的输入参数阅读。详情保留原始文本,完整 JSON 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。
- OpenCode 主会话和子 Agent 的工具记录均保留 Runtime 提供的操作标题作为摘要,显示文件路径、命令或操作描述;本地 SDK 标题去除首尾空白后最多保留 240 字符,没有标题时沿用工具名回退规则。主会话工具失败或中断时保留已收到的标题;远端 ACP 继续在记录名称中显示其提供的标题。
- 工具取消后保留“已取消”文字,避免与失败图标混淆。
- Continue、DeepSeek Harness 和直连模型的工具名后显示单行操作摘要,内容提取、长度与完成态保留规则见[工具操作摘要](./docs/features/assistant-workbar/runtime-interactions.md#工具操作摘要)。
- Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
- 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。
- 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
- 宽度大于 `700px` 时,通用工具栏内的添加内容、知识范围、专家、模式和 Runtime 选择保持同一行,Runtime 专属功能在自己的下一行横向排列。窄输入区中两行分别换行,专属选择器以至少 `220px` 的基准宽度换行而不是被挤压;不能把专属控件重新塞回通用工具栏。
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,图片转换为本次消息附件。文件选择由上传按钮承担,不再提供独立“读取剪贴板”按钮;默认工具栏也不提供“截取当前屏幕”和“选择应用窗口”入口,避免与系统粘贴、文件选择和后续工具执行重复。
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
- 不显示 Ask/Execute 选择器、模式徽标或替代权限档位;能力由所选 Runtime 和已有启用、分配设置决定。
- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,系统截图和从文件管理器复制的一个或多个支持文件加入本次消息附件。文件粘贴与上传按钮共用读取、解析进度和数量限制;读取期间阻止重复导入及发送,错误就地关联输入框和附件按钮。具体类型、大小及批次失败规则见[聊天附件要求](./docs/features/document-processing/prd.md#321-聊天附件入口)。默认工具栏不提供独立“读取剪贴板”“截取当前屏幕”和“选择应用窗口”入口。
- 空输入框使用三行原生占位文案:第一行为消息提示(生图模式保留图片描述提示),第二行为“Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴”,第三行为“Ctrl+N 新建对话 · 实际已注册快捷键 快捷唤起”。新建与粘贴快捷键按平台显示,macOS 使用 Command;全局快捷键不可用时省略该项。输入文字后占位提示自然隐藏,输入框下方不再重复全局快捷键,保留能力范围和其他元信息。
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
- 草稿与消息中的附件操作使用紧凑图标按钮,并提供悬停和键盘焦点提示;文件名、解析状态与操作行的布局遵循[附件操作规则](./docs/features/document-processing/ui-design.md#12-附件操作菜单)。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
排除在可滚动范围之外。
### 13.2 最近对话
@@ -532,8 +725,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
- 侧栏中有关联 Task 的 Conversation 显示行首展开按钮,父行不显示任务标签或数量;展开后
最多直接显示 3 个 Task,“查看全部 N 个任务”打开该 Conversation 的完整任务区。
- Task 子项以共享状态点开头,并显示名称、本地化的 Ask/Execute、计划摘要和聚合状态。点击
子项打开同一 Conversation 并定位 Task,不继续展开 Job/Run。
- Task 子项左侧显示名称,右侧显示计划摘要和共享状态标记,与父会话
的更新时间位置一致;已完成使用带勾成功点,不重复显示完成文字。点击子项打开同一
Conversation 并定位 Task,不继续展开 Job/Run。
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
### 13.3 知识库
@@ -546,29 +740,39 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.4 智能心跳
- 使用 `dashboard` 壳层。
- 顶部先呈现运行状态、实际范围和主操作,再呈现指标、建议、历史与配置。
- 监督者入口使用 `supervisor` 壳层;智能心跳设置位于同一壳层内。
- 顶层页签为“工作回顾 / 故事线图谱 / 智能心跳 / 活动记录 / 设置”,使用一套共享 PageTabs,窄窗口单行横向滚动。页签下不重复同名标题,也不保留空标题容器;历史结果不得标为最新。
- 工作回顾只展示统一监督结果、历史选择和紧凑的新回顾控件;结果保留自身范围、覆盖区间和生成时间,图谱读取同一结果 ID。智能心跳集中展示心跳计划、监督建议和只读的历史心跳报告;活动记录只展示统一执行活动、进度和详情。每页保留所属数据的一处刷新入口,范围与时间选择仅作用于新回顾请求。详细布局与阅读规则见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md#应用入口)。
- 监督者保留统一页面壳层;工作回顾与活动子面板铺满壳层可用内容宽度,与页签左右对齐。工作回顾与图谱共用稳定的回顾上下文栏,新请求字段按需展开。工作回顾正文及辅助内容按实际容器宽度排列,空态独占上下文栏下方整行并水平居中。活动筛选按内容紧邻排列,阶段使用编号圆点和连接线,次要统计收进详情;具体布局见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md)。
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
Checkbox,不能依靠进入页面时的当前项目推断。
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 保留“成长概览 / 待处理建议 / 心跳轨迹 / 心跳计划”四个同级页面。
- 活动记录先显示运行状态、时间、范围及实际阶段,恢复操作紧邻进度。原始错误默认折叠,展开后在有界面板内滚动并可选择复制;配置归入次要详情。任务层级只展示可查询的记录,缺失阶段明确标注,不能用剩余来源为零推断发布成功。具体阶段与分页规则见[监督者活动设计](docs/features/conversation-supervision/ui-design.md#活动)。
- 智能心跳的“心跳计划”卡片内直接排列状态说明、创建按钮和计划列表,不再嵌套第二层边框;仅保留一个创建按钮,说明文字按无计划、全部暂停、正常三种状态只显示一条。创建与编辑复用同一份表单及共享 Modal 焦点、背景隔离和遮罩样式;未保存关闭须确认,保存失败保留草稿,编辑暂停计划不恢复运行。“执行记录”进入按该计划 ID 筛选的活动页,活动页不重复显示心跳审计列表。
- 设置只展示模型超时与并发、分页、分批和单次响应容量,不含计划、报告、审计或嵌套页签,不显示整页全局徽标或独立刷新行。
- 监督者的设置、帮助、空态及错误文案不得向终端用户展示内部路线图、交付进度或设计与实现缺项;只说明当前行为、适用范围和可执行的下一步。不得因此夸大为提供商全局限流或完整语义识别;真实的运行失败、超时、暂停、来源失效及恢复操作必须保留。
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
### 13.5 Task Center
- 保留现有助手工作栏入口,首期在窄栏内适度完善,不先扩张成新的独立一级页面。
- 只展示 Task;普通 Conversation、Job、Run、工具步骤、Subagent 和心跳事项不独立占行。
- 每项显示名称、关联 Conversation、Global 或 Project 范围、Ask/Execute、状态、最近进展、
最近真实活动时间及需要关注信息。
- 范围固定为当前项目,不提供全局或所有项目入口;切换会话不筛掉本项目其他会话的任务。
- 顶部紧凑显示当前会话消息总数和 Agent 累计回复时间,按每轮请求区间累加,包含轮内工具和
审批等待,不计轮间空闲。历史缺失显示不完整提示,远程缺少可靠证据时不虚构用时。
- 每项显示名称、关联 Conversation、所属项目、状态、最近进展、
最近真实活动时间、累计执行时间及需要关注信息。任务用时汇总历次执行请求,不按父任务生命周期计算。
- 点击列表项打开关联 Conversation 并定位该 Task,不显示第二份内容载体。
- 需要关注、进行中、已暂停和已结束使用共享 `SegmentedControl`;窄栏不足时单行滚动。
- 项目任务标题右侧依次为“+”新建和筛选图标;图标按钮提供可访问名称,筛选按钮提供展开状态。
状态筛选默认收起且为全部;展开后用共享 `SegmentedControl` 提供全部、进行中、暂停、已结束,
窄栏不足时单行滚动。收起保留条件,非全部时图标高亮并显示条件。状态归类遵循[任务中心 PRD](docs/features/task-and-job/task-center-prd.md#5-交互)。
- 完整消息留在 Conversation;长错误、工具、审批和成果按 Task 关联到 Runtime、活动记录和
成果查看器,不撑高列表,也不显示 Job/Run 树。
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
Conversation,Task Center 入口默认新 Conversation;选择必须持续可见。
- 创建 Modal 默认 Execute,并允许主动切换 Ask。Execute 持续显示实际 Runtime、Project、
工作目录、工具和审批摘要;不支持工具时不得静默降级。
会话消息或工作区文件,不撑高列表,也不显示 Job/Run 树。
- “新建定制任务”使用共享 Modal,默认立即执行和当前 Conversation,可选择本项目其他可用
Conversation 或新建会话,选择必须持续可见。内容必填,名称可选并从内容生成;定时提供单次、每日和每周。
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime,不提供模式开关。
详情显示状态、日程与 Runtime,不显示模式或从旧计划字段推断权限。不支持工具时说明真实限制。
### 13.6 运行记录
@@ -576,43 +780,100 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
- 任务或会话的综合状态以最近一次顶层请求对应的最终 Agent 结果为准;最终结果尚未产生时使用该请求的当前状态。中间工具或子专家的失败、取消和中断保留在活动详情中,但不得覆盖最终成功状态。
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系。节点内使用 `U / G / S / T / A` 拉丁字母简称,节点下显示完整身份或名称;选择节点后显示所属会话、不可变范围快照和完整详情。
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器内横向滚动。
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度;按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺。宽表格在独立容器内横向滚动。
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
### 13.7 魔法笔记
- “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
- 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
会话联动已接入:右侧提供可关闭的单实例笔记 Tab,通过工作栏“+ → 笔记”打开;左下角和聊天页头不再提供重复的“快速笔记”入口。助手消息和会话标题菜单可打开文字采集预览;保存记录在面板和完整工作区显示独立来源条。草稿由 App 保留,关闭、替换及禁用前确认。具体交互由[魔法笔记会话联动界面设计](./docs/features/magic-notes/ui-design.md)维护。真实 Electron 验收已通过,全仓验证未全绿,见[验证进度](./docs/features/magic-notes/progress.md)。下列规则描述独立工作区。
- 未打开笔记详情时,笔记列表与待办使用页头普通按钮切换。按钮显示目标“切换到待办”或“切换到笔记”及对应图标,按内容宽度显示,不拉伸;笔记的新建入口位于左侧列表标题行,待办页的新建入口位于页头。窄窗口操作组可换行到标题下方,保留控件高度。
- 切换按钮不使用页签语义,切换后焦点回到同一个按钮;内容 section 持续提供当前列表的 `aria-label`。切换沿用草稿保护入口,待办状态仍使用独立的 `SegmentedControl`。
- 文字记录编辑器保留 Quill 原生列表快捷输入:输入 `-`、`*`、`1.`、`[ ]` 或 `[x]` 后按空格,分别转换为项目符号、编号或待办清单。快捷输入只生成现有 Delta 富文本,不引入 Markdown 源码模式、第二套文字记录持久化格式或第三方 Markdown 插件;新建文字记录的操作提示收进类型选择旁的 `InlineHelp`。
- 新记录可选择“文字 / 画布”;已保存画布提供分页阅读和“展开编辑画布”。画布使用 PeopleLib 的 Fabric 分页批注与 Quill 跨页正文,支持纸张模板、增删页、笔迹、高亮、整对象橡皮、选择及移动缩放旋转、浮动文字、图片和 PDF 底版。画布有固定页面边界;已保存记录、待办来源和编辑态默认容纳完整当前页及工具栏,高度随当前页实际显示尺寸变化,纵向由外层笔记流滚动。多页仍属于一条记录,通过页码翻页;窄窗口工具栏换行,纸张仅在自身视口内横向滚动,不撑宽外层页面。
- 画布通过“保存记录 / 保存修改”手动持久化,离开或切换记录类型前沿用未保存草稿确认。撤销/重做作用于当前批注页或 Quill 正文模式,增删页和 PDF 导入不属于全局撤销历史。删除非空批注页或 PDF 页需要在五秒内再次点击确认。
- 画布编辑与只读工具栏均提供缩小、当前百分比、放大和“适应宽度”。默认 100%,范围 25%–300%;加减每次调整 25 个百分点,点击百分比还原 100%。画布区域内 Ctrl/Command + 滚轮连续缩放,普通滚轮和 Shift 横滚保持原有方向与边界传递。适应宽度按实际视口计算,窗口或分栏调整后自动更新;翻页保留固定比例或适应宽度策略。缩放仅影响显示,整页高度与横向滚动范围随之变化,不新增内层纵向滚动,也不改变正文分页、保存内容、导出尺寸或未保存状态。控件沿用画布的主题、焦点环与提示,窄屏整组换行,适应宽度通过 `aria-pressed` 和选中样式表达状态。
- “导入 PDF”保留页面并提取原生文字供搜索与 AI 使用,扫描页不自动 OCR。“导出 PDF”将底版、正文和批注逐页合成为栅格 PDF,不保留可搜索文字层;当前 UI 无 PNG 下载按钮,页面 PNG 捕获仅作为程序接口提供。
- 画布新建与编辑态不显示常驻分析提示。操作放在编辑区右上角:新建与类型选择同排,顺序为“分析画布草稿、保存记录”;编辑放在记录头部,顺序为“分析画布草稿、取消、保存修改”。保存为最右侧主按钮,窄屏允许换行;底部不重复操作,新建不增加取消按钮。`immediate` 仍通过按钮手动分析,不随笔画自动请求;`after-save-auto` 在手动保存后分析。设置读取或页面捕获失败不阻断保存,保存成功后通过应用通知说明自动分析失败,可稍后重新分析;模型分析失败也不回滚已保存内容。
- 魔法笔记设置提供“发送画布页数”单选框,复用设置表单 `.field` 控件样式,范围 1~8,默认 1。标题同行的问号使用 `InlineHelp`,下拉框通过稳定的帮助 ID 关联展开的说明,交互遵循 6.14。说明包含当前页序前 N 页、不足时发送全部页、数量指单个画布页数而非记录数,以及完整保存与导出不受影响。修改后沿用设置保存反馈,下次分析生效。
- 草稿、已保存画布记录及来源为画布的待办,按默认模型的 `supportsImageInput` 使用所选前 N 页图像与文字,或只发送同一范围文字并标注“仅文字分析(未使用画布图像)”。所选页只有视觉内容的记录/草稿遇到文字模型时提示切换支持图像输入的默认模型,不声称已理解笔迹或布局。待办分析同时保留任务标题、说明和所选来源画布页的上下文。
- 画布文字评论在提取的 `plainText` 不变时保留;图像评论随画布内容或布局变化失效,在保存后自动评论模式下重新分析。草稿内容变化清除旧草稿分析,评论区显示实际使用的输入模式。存储和判定细节见[画布技术设计](./docs/features/magic-notes/technical-design.md#canvas-analysis-and-comments)。
- 总览是全宽的“全部笔记”页,以自适应卡片网格(最小 `260px`)显示全部笔记,不显示右侧空白详情,也不自动打开第一篇;卡片显示标题、短日期、两行预览和记录数。打开笔记后切为左侧紧凑列表与右侧详情,页头右上角显示带文字的“返回总览”,点击回到全部笔记页并恢复搜索、滚动位置与卡片焦点;“切换到待办”只在总览显示。列表按紧凑两行行项显示:第一行为标题与短日期(今天显示时间,其余显示月/日,完整时间在工具提示中),第二行为单行预览;记录数只提供给读屏。行项无边框、透明底色,悬停为次级底色,选中为淡蓝底色加左侧 `3px` 强调标记;“更多”按钮悬停或聚焦时覆盖在日期位置。保留搜索、置顶菜单和当前选中状态。打开笔记只更新右侧详情;列表独立滚动,切换保留搜索和列表位置,并在工作区挂载期间记住各篇笔记的阅读位置。明确定位某条记录时优先跳到该记录。
- 笔记列表默认展开、宽 `280px`,支持拖动分隔线在 `240–420px` 内调宽,方向键每次调整 `16px`,Home/End 跳到边界;桌面端详情页提供列表显隐开关,记住宽度和展开偏好。工作区宽度不超过 `900px` 时切换显示列表与详情,返回恢复列表位置和入口焦点,不覆盖桌面偏好。新建成功后打开新笔记;切换、新建和删除当前笔记都保护未保存的正文与标题。
- 待办页是独立任务视图:搜索任务标题、说明或来源笔记,搜索框右侧同一工具栏放置“全部 / 未完成 / 已完成”筛选,显示匹配数与总数;任务按来源笔记分组。列表独立滚动,页头、搜索和筛选保持可见,窄屏工具栏空间不足时换行。直接勾选完成或恢复未完成,不自动选择任务;状态变化后按当前筛选更新列表。
- 待办内容容器宽于 `700px` 时,任务列表稳定在左侧,右侧显示当前任务说明、来源记录和 AI 评论;未选择时右侧提示选择任务。点击任务只更新右侧,不改变列表宽度、行位置或滚动位置,再次点击取消选择。AI 不增加第三栏。容器不超过 `700px` 时,按需详情替换同一内容区域,并提供“返回待办列表”;列表保持挂载,返回恢复滚动和任务行焦点,不将列表与详情上下堆叠。任务按钮提供 `aria-expanded` 和 `aria-controls`。
- 待办左右栏分割线支持拖拽,并记住列表宽度;键盘左右方向键每次调整 `16px`,Home/End 调整至边界。列表默认 `320px`、最小 `240px`,详情至少保留 `300px`;窄屏隐藏分割线,临时缩窄窗口不覆盖已保存宽度。每个来源笔记标题可独立收起或展开待办列表,保留标题与数量,通过 `aria-expanded` 和 `aria-controls` 表明状态;收起不清除右侧当前详情。
- 待办 AI 评论保留方向选择、流式输出、重新分析和已有结果;来源加载失败通过应用通知提示,并提供就地重试。从待办打开原笔记后,“返回待办”恢复任务选择、搜索、筛选和列表滚动;宽屏焦点返回任务行,窄屏返回待办详情中的返回按钮。
- 笔记详情主区按新到旧连续展示全部记录。打开笔记即在标题下、历史记录上显示空白输入区,保留文字/画布类型选择和保存操作,标题栏与索引均不提供“新增记录”按钮。新增保存成功后重建空白输入区,保留所选类型;自动分析结束也不转入旧记录编辑。旧记录通过显式编辑入口修改,保存使用同一 ID 和返回的修订号。点击索引或当前笔记内的 AI 来源只按 entry ID 滚动定位,不卸载编辑器、不清草稿、不询问离开。空白输入区离开不提示,返回总览、打开其他笔记或改为编辑另一条记录时仍保护未保存修改。
- 打开笔记后页面一级标题仍为“魔法笔记”;笔记标题只在记录流顶部出现一次,使用 `22px` 的可编辑大标题,悬停显示下划线,聚焦显示强调色下划线,保留失焦保存、Enter 与 Escape。标题上方为吸顶的细工具栏,依次放置收起笔记列表、记录索引、条数与更新时间、AI 评论开关,均为带工具提示和可访问名称的图标按钮。记录流左右留白 `24px`(窄于 `600px` 时 `12px`),正文最大宽度 `880px` 并居中;吸顶工具栏不受此限制,始终铺满记录流宽度,AI 开关贴在右边缘,底部分隔线横贯整栏。
- 桌面笔记详情的列表标题行、正文工具栏与 AI 评论方向标签行顶部对齐,首行统一使用 `40px` 控件高度并垂直居中;AI 区域不叠加面板和表单的顶部留白。
- 文字记录的新建与编辑区域铺满卡片内沿,格式工具栏紧接头部,输入区域紧接工具栏;编辑器外层不再增加内边距或圆角。正文保留编辑器内部的文字留白,底部操作栏独立保留内边距与顶部分隔线;画布仍保留 `12px` 内容间距。
- 新建记录输入区空闲时收成一行虚线提示框,隐藏类型切换、格式工具栏和保存栏;聚焦、已有草稿、选择画布或出现校验错误时展开,初始约四行并随内容增高。保存成功或切换笔记后恢复为一行。此规则不作用于旧记录编辑器。
- 已保存记录的 AI 分析、编辑、删除为 `28px` 图标按钮,精细指针设备上悬停或键盘聚焦记录时才显示;删除悬停时使用危险色,确认条保持原样。粗指针设备始终显示。笔记内部列表、记录流、AI 区域和文字编辑区在精细指针设备上使用细原生滚动条,轨道透明;强制颜色与粗指针设备保留系统默认。说明、摘要和输入提示使用 `--font-body` 与 `--text-secondary`,时间和短标签保留 `--font-caption`。
- 左侧记录索引默认收起,只在用户展开后记住展开状态;展开宽度默认 `168px`,可拖动右边界在 `140–320px` 内调整栏宽,缩略图随列宽等比适配。开关位于笔记工具栏,提供 `aria-controls`、展开状态和动作提示;收起后完全移除索引列、分隔条、间隙、边框及轨道,不占宽度,隐藏内容不可聚焦。右侧 AI 评论独立显示、滚动和收起,不与索引共享页签。每条记录一个导航项:画布显示真实首页缩略图及总页数,文字显示摘要,两者均显示类型和时间。两侧分别记住桌面展开状态和宽度,共用原有布局偏好;宽布局分隔条共用样式及 `separator` 语义,方向键每次调整 `16px`,Home、End 跳到当前上下限。两侧宽度按可用空间协调限制,为主区保留至少 `300px`,窗口缩小时不覆盖保存的偏好。内容容器不超过 `800px` 时索引默认隐藏,由同一标题栏按钮开闭标题栏下方固定 `168px` 的抽屉,不显示调宽分隔条;标题栏自身保留控件高度,不保留轨道。选择记录或按 Escape 后关闭,Escape 将焦点返回开关。AI 移至底部,高度不超过可用布局的 40% 且最多 `280px`,不显示分隔条。
AI 评论默认不占列:当前笔记已有评论、正在分析或存在草稿评论时自动展开;用户对该笔记手动收起后保持收起,直到发起新的分析。无评论时手动展开即视为固定,固定状态跨笔记和重启保留,再次收起即取消固定。AI 评论的唯一显隐入口位于笔记工具栏右端,使用紧凑面板图标按钮;展开时应用共享选中样式,图标随显示或隐藏动作切换,并提供对应的工具提示、可访问名称、`aria-expanded` 和 `aria-controls`。评论面板内部不再显示重复的“AI 评论”标题或隐藏按钮行,保留区域的可访问名称、评论方向选择及各记录的分析操作。
- 笔记详情的内部记录索引使用透明底色,与主内容表面连续,仅保留细调宽分隔线。每条索引记录保留 `1px` 的 `--border-subtle` 淡边框,不加卡片底色或阴影,悬停使用轻微次级底色;选中记录使用淡蓝背景和左侧 `3px` 蓝色内侧标记,悬停时保留选中态。缩略图保留纸张边界,键盘焦点环与强制颜色模式选中边框保持可见。窄屏抽屉使用不透明的主内容表面,避免下层正文透出;深色主题沿用语义令牌。
- 笔记详情的标题、正文和记录流不再包裹额外的大外框,记录流容器只保留上述左右留白;缩略图列表顶部与正文区顶部对齐,左边界与索引栏左边界对齐,仅在右侧和底部保留 `8px` 留白,键盘焦点环向内绘制以避免滚动容器裁切。每条已保存记录使用一层 `1px` 的 `--border-subtle` 淡边框和 `--radius-control` 圆角,保留透明底色与记录间留白,不加阴影或头部下划线。记录编辑时保留输入区边框、页面安全边距及 AI 区域分隔。此规则仅作用于笔记详情,笔记列表与待办保持独立布局。
- 画布新建、编辑和只读态统一保留 `12px` 内容间距;详情中已保存画布去掉预览外框和工具栏底色、下划线,保留灰色画布衬底与白色纸张,编辑态保留编辑器边框。记录头部操作按内容宽度排列,空间不足时自然换行。文字新建的保存区与编辑器属于同一卡片,空白编辑区使用最小高度,不随容器百分比高度拉长。文字工具栏图标、菜单和原生滚动条须跟随主题。
- 标签使用中性胶囊样式(`--surface-subtle` 底色、`--border-subtle` 边框、`--font-caption`),不使用颜色区分。总览搜索框下方是标签筛选行:“全部”加各标签和对应篇数,均为 `aria-pressed` 切换按钮;可以多选,多选时只显示同时带有全部所选标签的笔记。选中时使用 `--accent-subtle` 底色和强调色边框。卡片最多显示 3 个标签,其余合并为“+N”。打开笔记后,左侧列表不显示筛选行,只显示“已筛选:A · B”提示条和清除按钮。
- 笔记标题下方是标签编辑行:已有标签后带 × 移除按钮,末尾是虚线“+ 标签”按钮。点击后展开组合框,按输入内容过滤已有标签,也可以新建。方向键切换选项,Enter、逗号或中文逗号确认,Escape 关闭并把焦点还给“+ 标签”。达到 10 个时,“+ 标签”禁用并说明上限。
- 列表标题行右侧只放“管理标签”,不再放 `+` 新建按钮。“新建笔记”统一放在页头右上角,是主按钮,始终位于“切换到待办 / 切换到笔记”或“返回总览”的右侧。打开笔记时点击,桌面端展开左侧列表并显示标题输入,窄屏先回到列表。“管理标签”在总览显示图标加文字,打开笔记后只显示图标。点击后打开共享 `custom-task-dialog` 对话框,按使用篇数列出标签,每行提供重命名和删除图标按钮。重命名在行内编辑;新名称与已有标签相同时,提示保存后会合并。删除在行内二次确认,并说明涉及的篇数。Escape 依次退出行内编辑或确认,最后关闭对话框。
- 整篇笔记的置顶、取消置顶和删除放在列表行的“更多”菜单,打开详情时仍可操作,标题编辑区不重复提供入口。按钮在悬停、键盘聚焦或菜单展开时显示,触屏设备常显;菜单沿用会话列表样式,通过 Portal 避免滚动区域裁切,支持方向键、Home、End 和 Escape。删除在菜单内二次确认并说明笔记标题及全部记录;删除当前笔记还须处理未保存草稿,删除其他笔记保留当前内容与草稿。打开菜单不切换笔记。
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
- 未完成待办徽标的高度不得超过主导航图标;数量从 0 变为可见或再次隐藏时,主导航项行高和相邻菜单位置必须保持不变。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- Agent / MCP 或其他入口成功写入笔记后,已打开的工作台自动更新笔记列表、当前详情、待办及来源记录,不增加刷新按钮。保留仍存在的笔记与待办选择、搜索和筛选,以及未保存的标题、输入区和记录编辑草稿;普通后台刷新不重建编辑器。
- 外部删除当前笔记时,无草稿则返回总览;有草稿则保留编辑区并提示先复制草稿。外部删除正在编辑的记录时,同样保留该编辑器并明确提示记录已删除;保存失败不得清空草稿。
- 当前记录草稿非空或标题尚未保存时,返回总览必须先就地确认;编辑另一条记录等会丢弃正文草稿的操作沿用同一保护。继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 文字和画布记录保存修改成功后退出编辑状态,显示已保存正文;后续自动评论成功或失败均不重新打开编辑器。再次修改须点击编辑入口,以该修订的编辑器初始化内容作为修改比较基线。编辑器归一化不算用户修改,后续输入、画布变更和保存失败仍保留离开保护;同修订的后台刷新不替换已建立的基线。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
### 13.8 设置中心
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
- 设置中心以整窗遮罩上的大 Modal 打开,保留底层工作区、会话草稿和助手工作栏,不切换一级页面。固定标题区和右上角关闭按钮,左侧分类与右侧内容各自滚动。关闭后返回原工作区并恢复入口焦点。
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);窄窗口仍保留左右布局,长分类名称可换行。内容区在可用空间内伸缩,最大宽度为 `960px`。
- 设置 Modal 通过 Portal 挂载到 `document.body`,复用共享背景隔离与焦点恢复机制。打开后聚焦关闭按钮,Tab 在弹窗内循环;Escape 先交由嵌套弹窗处理,设置自身的 Escape 与关闭按钮共用未保存确认,确认出现后再按 Escape 返回编辑。点击遮罩不关闭;保存、测试和清理数据期间禁止关闭。每次打开重新挂载设置,模型和消息通道等直达入口使用本次指定分类,普通入口默认打开 Agent Runtime。
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与右侧标题来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
- 左侧按“常规 / 模型与执行 / 功能与扩展 / 系统”分组,顺序与键盘导航一致。分类按钮最小高度 `40px`,只显示名称;分类说明集中在右侧页头,外层标题不再重复功能简介。组名使用辅助字号,普通项使用正文和中等字重,选中项使用强调底色与加粗文字。
- 设置区块使用共享圆角、间距和表面令牌;标题为 `--font-section-title`,持久说明至少为 `--font-caption`。嵌套设置区块以顶部细分隔线和留白组织,不叠加卡片背景与四周边框。“能力与工具”的上级页签使用默认 `PageTabs` 外观,与 MCP 内部的分段页签区分。
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
- 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。
- 智能心跳的单条配置不在设置中心重复管理。设置中心如需呈现平台级说明,只提供
“打开智能心跳”导航,不复制创建、暂停、恢复或删除表单。
- 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。
- “模型连接”的认证字段按“认证方式 → API Key → 凭据状态”排列;只有选择 API Key 时显示输入框。已保存的 API Key 属于该连接,修改模型服务地址或临时选择“无需认证”不得自动清除,也不得要求重新输入;仅由明确的“清除凭据”或“删除连接”操作移除。
- “模型连接”页头在保存操作下方,将模型类型页签与全局模型下载源入口放在同一行;当前来源文字与“模型下载源”次按钮整体右对齐,窄窗口保持同行,空间不足时导航行可横向滚动。按钮打开挂载到 `document.body` 的 Portal 弹窗,复用共享 Modal 焦点管理;弹窗内使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片,选中状态同时依靠 Radio、边框和背景表达。Radio 只修改草稿,点击“确认”才保存;取消或关闭不改变已保存来源。读取失败提供重试并禁用确认,不以默认值伪装已保存选择;保存失败保留草稿并允许确认重试,保存期间禁止重复提交和关闭。向量、语音和 OCR 的更换下载源入口直接打开模型连接页的同一弹窗,跨分类时沿用未保存修改保护。详细规则见[模型下载源设计](docs/architecture/model-download-source-design.md#7-模型下载源界面)。
- 设置中的连接列表与详情共用 `.model-connection-manager`,按容器可用宽度响应:不超过 `640px` 时上下排列,列表内部滚动高度最多 `180px`;更宽时恢复左右排列。LLM、向量连接和角色编辑复用此规则,不以整窗宽度代替表单宽度。接口协议的中英文当前值须完整可读。回归范围见[窄容器布局修复记录](docs/quality/responsive-controls-review-2026-09-27.md)。
- “保存并测试模型”必须在保存后发送一次有界的真实模型请求。文本连接校验本次随机测试文本,图像连接校验实际返回的内联图片;仅收到成功 HTTP 状态、模型列表或健康检查响应不算测试成功。界面持续说明该操作可能产生少量服务商用量费用,成功反馈明确写为“真实生成测试通过”。
- 所有显式保存的设置草稿都参与离开保护:关闭、切换分类、主侧栏或工作区导航及托盘导航不得静默丢弃,统一通过设置中心的就地确认提供继续编辑与明确放弃入口,保存失败后保留输入。“平台功能 / 通用设置”承载全局快捷唤起的共享 Switch、可访问 accelerator 录制输入、恢复默认、保存及注册、停用或冲突状态,不新增分类或页签;注册或持久化失败时保留上一组可用快捷键和当前草稿,保存或停用成功后同步更新输入区的快捷键提示。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“魔法笔记”,默认进入通用设置。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后,下拉框置灰且不可操作。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查和下载页,不显示底层服务商名称。
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
更新,保存失败时保留最后确认状态。
- 通用设置按快捷唤起、会话与通知、工作目录分组,复用现有卡片。“桌面通知”为默认开启的持久 Switch,保存后立即控制窗口未聚焦时的任务完成、失败和取消系统通知,关闭不影响应用内通知。旧配置缺少该字段时默认开启,明确关闭后重启仍保持关闭;保存失败保留最后确认状态并显示应用内错误通知。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、同行帮助和原生单选下拉框放在同一行,用途说明收进 `InlineHelp`,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
- Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及 Ask/Execute 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
- Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及描述,不提供统一的原生逐工具开关;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
- “能力与默认配置”只显示一个模块标题,刷新入口位于该标题右侧,能力状态压缩为一行并排在默认 Agent 或 Continue 预设编辑器之前;不得再复制“Runtime 原生能力”等同义标题、说明或状态结论。刷新只更新能力快照,不覆盖未保存的原生定制草稿。
- OpenCode 的默认 Agent 使用原生下拉选择;Continue 预设编辑器允许管理名称、说明、启用的 Rules 以及 Prompt 名称、说明和正文,并可展开查看原生 Rules 与启用预设 Rules 的最终合并顺序。持久启停仍使用共享 Switch,添加与删除使用明确按钮和可访问名称。
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。直连模型中的联网搜索与内置浏览器使用一致的折叠卡片和独立总开关;内置浏览器必须明确说明其操作 GoodBuddy 隔离浏览器,不控制客户端已安装的浏览器,开启后可由 Execute 直接使用,不逐次询问。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
- 魔法笔记设置统一从应用中心对应行进入;笔记页和全局设置不再提供重复入口。“显示未完成待办数量”持久 Switch 只控制左侧数量徽标,不改变待办数据。旧入口开关绑定业务启用值,迁为“启用应用”并保留原值,常驻使用独立配置。默认开启仅适用于未配置布尔值,只有独立显隐偏好才映射到常驻。此迁移规则见[应用导航 PRD](./docs/features/application-tool-navigation/prd.md#fr-11-应用导航设置)。
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。联网搜索保留在直连模型;内置浏览器归入内置 MCP,提供持久启停和 Runtime 分配,并明确说明其操作 GoodBuddy 隔离浏览器、不控制客户端已安装的浏览器,开启后可由已分配 Runtime 直接使用,不逐次询问。该开关只控制 Agent 工具访问,关闭时不得禁用浏览器工作栏中的用户手动导航、返回、刷新、停止加载、交互或关闭操作。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
- MCP Server 测试结果在同一展开卡片中分组显示 Tools、Prompts 和 Resources 的支持状态、数量与有界元数据;Prompt 参数标明必填项,Resource 只显示 URI、名称、类型和说明,不读取或渲染 Resource 内容。
- 内置 MCP 中的“图片生成与编辑”是自动管理例外:该行始终可见,标记“内置 · 自动管理”,不提供独立启停、分配保存、移除或凭据编辑。直连 Model、OpenCode、Continue、DeepSeek Harness 的分配只由已保存图片模型的“允许 AI 在会话中调用”设置推导;至少一个开启则全部勾选,没有开启模型则全部取消。使用禁用的原生 Checkbox、Runtime 标签及关联说明,保留可聚焦的“前往图片模型设置”导航;远程适用范围通过 GoodBuddy Agent 说明持续可见。离线、无工具能力或调用错误不改变勾选。图片模型默认关闭会话调用;默认生图模型独立于普通默认连接,关闭或删除后保留失效选择并提示改选,不自动回退。
“安全”设置分类及 `toolApproval` 策略已移除。“清除本地数据”位于“通用 > 平台功能”,使用既有危险操作按钮和二次确认;首次点击与取消均不得清除数据,确认后才调用 Main 清除入口。
### 13.9 文档解析设置
现有本地解析与本轮 HTTP 扩展的实现边界见[文档处理进度](./docs/features/document-processing/progress.md)。
跨场景入口、附件菜单、共享预览及选图返回规则由[文档处理 UI 设计](./docs/features/document-processing/ui-design.md)维护;
当前接线与存储行为见[生产实现说明](./docs/features/document-processing/implementation.md)。文档解析属于全局设置,不新增应用中心成员;本机推理监控只管理本地服务。
- 设置中心新增独立的“文档解析”分类,统一管理聊天附件、知识库导入以及后续文档审阅场景使用的提取、转换和 OCR 策略。OCR 不作为普通对话模型出现在“模型连接”中。
- 分类页头说明文档解析的跨场景作用,右侧依次显示“测试解析”和“保存设置”;保存位于最右侧。测试必须选择真实文件并执行实际解析,不能只检查模型文件或接口连通性。
- 页面首先显示原生解析、文档转换和 OCR 的运行状态,并明确当前可处理格式、回退能力与不可用原因。部分能力未配置时使用“部分可用”状态,不得把原生文本解析一并标记为失败。
@@ -626,9 +887,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- PDF 先读取文本层。仅当页面无有效文本、乱码比例过高或用户选择“始终 OCR”时渲染该页并识别;不得因为单页需要 OCR 而丢弃其他页面已经提取的可靠文本。
- DOCX、XLSX、PPTX 优先保留段落、单元格、公式、备注等原生语义。转换为 PDF 用于补充版面、页码、图表和图片理解,不作为唯一中间格式。
- DOC、XLS、PPT 等旧格式通过受控转换 Provider 生成新式 Office 文档和 PDF。转换子进程必须禁用宏和网络,限制输入、输出、内存、超时与临时目录,并在关闭或取消时清理。
- OCR 来源使用“本地模型 / 远程服务”互斥选择。选择本地后显示模型下载、模型下拉选择和本地运行参数;选择远程后显示 MinerU、PaddleOCR-VL 等服务连接配置。未实现的远程服务入口保持可读但禁用,不再增加与来源选择重复的“隐私与云端处理”授权区。
- OCR 来源使用“本地模型 / 远程服务”互斥选择,远程为 HTTP PaddleOCR-VL;MinerU 不在本轮范围。来源切换保留两套配置,未保存时禁止测试。来源配置、保存和测试规则见[文档解析设置设计](./docs/features/document-processing/ui-design.md#2-文档解析设置),不另加逐文件授权区。
- 用户配置并保存远程 OCR 服务即表示选择该处理路径,不再逐场景重复询问。界面仍须明确显示当前服务名称、处理范围和远程属性,API 密钥只保存在主进程加密设置中,未选中远程服务时不得上传文档。
- 解析结果使用统一文档结构,至少保留文档标题、来源格式、页码或工作表定位、正文块、置信度、处理方式和警告。聊天附件对结果做有界截断,知识库使用完整结果分块和索引。
- 解析结果使用统一文档结构,保留文档标题、来源格式、页码或工作表定位、正文块、处理方式和警告;置信度仅在解析器提供时显示,缺失不补造。聊天文档附件保留完整解析文本,超过暂存容量时报错;知识库使用完整结果分块和索引,具体要求见[文档处理 PRD](./docs/features/document-processing/prd.md#32-场景工作流)。普通图片默认走图片输入,显式 OCR、原图保存和文档选图遵循该功能的规划规则。
- 测试结果显示文件类型、页数、实际工作流、提取字数、OCR 页数、耗时和警告。测试文件不得自动进入聊天上下文或知识库。
## 14. 文案规则
@@ -670,7 +931,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- [ ] 实现 `ScopeBadge` 并覆盖全局、项目、失效和可切换状态。
- [ ] 实现 `EmptyState` 的首次为空、无结果、失败和只读变体。
- [ ] 实现 `danger-ghost`、`danger-solid` 和 `danger-zone`。
- [ ] 统一模型、专家角色和工作模式的单选菜单结构、视觉与键盘行为。
- [ ] 统一模型、专家角色和 Runtime 选项的单选菜单结构、视觉与键盘行为。
### 15.4 页面迁移
@@ -678,7 +939,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- [ ] 将输入快捷键与附件提示置于空输入框内部,输入区下方保持单行说明。
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
- [x] 智能心跳使用 `dashboard`,保留概览、建议、轨迹和计划,并在计划中支持 Global / 多 Project 范围。
- [x] 监督者使用 `supervisor`,工作回顾读取统一监督结果;智能心跳包含 Global / 多 Project 心跳计划、监督建议与历史心跳报告;活动记录显示统一执行,设置只保留算法参数。
- [x] 在现有工作栏中完善任务中心,统一范围、状态、最近进展和筛选,不新建平行任务平台。
- [ ] 活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
@@ -686,6 +947,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 15.5 验收
执行与取证方法见 [Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md),包括真实 Portal、原生键盘、计算样式与 PNG 审阅;视觉 fixture 与完整生产 App 的结果须分别标注。
- [ ] 在浅色与深色主题下检查所有页面、弹窗、菜单、输入框、表格和空状态。
- [ ] 在大于等于 `1200px`、`960px`、`720px` 和小于 `720px` 的窗口宽度检查布局。
- [ ] 仅使用键盘完成导航、筛选、创建、编辑、确认和取消。
+35
View File
@@ -0,0 +1,35 @@
{
"formatVersion": 1,
"agentVersion": "0.15.6",
"protocol": {
"major": 2,
"minor": 0
},
"node": {
"version": "24.19.0",
"source": "https://nodejs.org/dist/v24.19.0/",
"targets": {
"linux-x64": {
"archive": "node-v24.19.0-linux-x64.tar.gz",
"sha256": "f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4",
"binaryPath": "node-v24.19.0-linux-x64/bin/node",
"licensePath": "node-v24.19.0-linux-x64/LICENSE"
},
"darwin-arm64": {
"archive": "node-v24.19.0-darwin-arm64.tar.gz",
"sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d",
"binaryPath": "node-v24.19.0-darwin-arm64/bin/node",
"licensePath": "node-v24.19.0-darwin-arm64/LICENSE"
},
"linux-arm64": {
"archive": "node-v24.19.0-linux-arm64.tar.gz",
"sha256": "d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f",
"binaryPath": "node-v24.19.0-linux-arm64/bin/node",
"licensePath": "node-v24.19.0-linux-arm64/LICENSE"
}
}
},
"koffi": {
"version": "3.1.4"
}
}
+18
View File
@@ -0,0 +1,18 @@
const { createHash } = require('node:crypto')
const { mkdirSync, readFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { execFileSync } = require('node:child_process')
const { readRemoteRuntimeLock, resolveLockedRuntimeInput } = require('./remote-runtime-bundle.cjs')
const [directory, architecture, platform = 'linux'] = process.argv.slice(2)
if (!directory) throw new Error('Continue archive destination is required')
const input = resolveLockedRuntimeInput(readRemoteRuntimeLock(), architecture, platform, 'continue')
const destination = resolve(directory)
mkdirSync(destination, { recursive: true })
execFileSync('npm', ['pack', `${input.packageName}@${input.version}`, '--ignore-scripts',
'--registry=https://registry.npmjs.org', '--pack-destination', destination], { stdio: 'ignore' })
const archive = join(destination, input.archive)
if (`sha512-${createHash('sha512').update(readFileSync(archive)).digest('base64')}` !== input.integrity) {
throw new Error('Continue archive integrity mismatch')
}
process.stdout.write(`${archive}\n`)
+33
View File
@@ -0,0 +1,33 @@
const { readFileSync } = require('node:fs')
const {
detectBinaryArchitecture,
detectElfArchitecture
} = require('./binary-architecture.cjs')
function targetName(architecture, platform = 'linux') {
if (
!(platform === 'linux' && ['x64', 'arm64'].includes(architecture)) &&
!(platform === 'darwin' && architecture === 'arm64')
) {
throw new Error(`Unsupported Agent build target: ${platform}-${architecture}`)
}
return `${platform}-${architecture}`
}
function assertTargetBinary(filePath, architecture, platform, label) {
targetName(architecture, platform)
const header = readFileSync(filePath).subarray(0, 64)
const isMachO = header.length >= 8 &&
(header.readUInt32LE(0) === 0xfeedfacf ||
header.readUInt32BE(0) === 0xfeedfacf)
const actual = platform === 'linux'
? detectElfArchitecture(header)
: isMachO ? detectBinaryArchitecture(header) : undefined
if (actual !== architecture) {
throw new Error(
`${label} architecture mismatch: expected ${platform}-${architecture}, received ${actual ?? 'unknown'}`
)
}
}
module.exports = { targetName, assertTargetBinary }
File diff suppressed because it is too large Load Diff
+960
View File
@@ -0,0 +1,960 @@
const {
createHash,
createPublicKey,
sign,
verify
} = require('node:crypto')
const {
closeSync,
fstatSync,
openSync,
readFileSync,
readSync,
writeFileSync
} = require('node:fs')
const { basename, resolve } = require('node:path')
const { crc32 } = require('node:zlib')
const {
productionSigningKey,
publicKeySpkiBase64,
readTrustedKeyRegistry,
redactSecrets
} = require('./agent-bundle.cjs')
const {
canonicalJson,
descriptorContentDigest,
signatureDomain: packageSignatureDomain
} = require('./agent-package.cjs')
const {
localFileHeaderSignature,
readExactZipBytes,
readZipCentralDirectory
} = require('./zip-central-directory.cjs')
const root = resolve(__dirname, '..')
function supportedTarget(platform, architecture) {
return (platform === 'linux' && ['x64', 'arm64'].includes(architecture)) ||
(platform === 'darwin' && architecture === 'arm64')
}
const catalogName = 'agent-catalog.json'
const catalogSignatureName = 'agent-catalog.sig'
const catalogSignatureDomain = Buffer.from(
'GoodBuddy Agent Package Catalog Signature v1\0',
'utf8'
)
const semanticVersionPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
const sha256Pattern = /^[a-f0-9]{64}$/u
const maximumPackageBytes = 512 * 1024 * 1024
const maximumPackageEntries = 50_002
const maximumPackageEntryBytes = 384 * 1024 * 1024
const maximumExpandedPackageBytes = 1024 * 1024 * 1024
const maximumMetadataBytes = 1024 * 1024
const maximumDescriptorBytes = 4 * 1024 * 1024
const maximumCentralDirectoryBytes = 32 * 1024 * 1024
const maximumCatalogEntries = 200
const windowsReservedNamePattern =
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu
function canonicalBytes(value) {
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
}
function trustedProductionKey(registry, keyId) {
const key = registry.keys.find(
(candidate) =>
candidate.keyId === keyId &&
candidate.environment === 'production'
)
if (
!key ||
registry.revocations.some(
(revocation) => revocation.keyId === keyId
)
) {
throw new Error(`Agent signing key is not trusted: ${keyId}`)
}
return key
}
function verifyDetachedSignature(
bytes,
signatureText,
key,
domain,
label
) {
const signature = Buffer.from(
signatureText.toString('utf8').trim(),
'base64'
)
const publicKey = createPublicKey({
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
format: 'der',
type: 'spki'
})
if (
signature.length !== 64 ||
!verify(
null,
Buffer.concat([domain, bytes]),
publicKey,
signature
)
) {
throw new Error(`${label} signature verification failed`)
}
}
function parseCanonicalJson(bytes, label) {
const value = JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
)
if (!canonicalBytes(value).equals(bytes)) {
throw new Error(`${label} is not canonical`)
}
return value
}
function assertProtocol(value, label) {
if (
typeof value !== 'object' ||
value === null ||
!Number.isSafeInteger(value.major) ||
value.major < 1 ||
!Number.isSafeInteger(value.minor) ||
value.minor < 0
) {
throw new Error(`${label} is invalid`)
}
}
function assertPackageDescriptor(descriptor) {
if (
typeof descriptor !== 'object' ||
descriptor === null ||
descriptor.format !== 'goodbuddy-agent-package' ||
descriptor.formatVersion !== 1 ||
descriptor.product !== 'GoodBuddy' ||
descriptor.component !== 'agent' ||
!semanticVersionPattern.test(descriptor.version ?? '') ||
!semanticVersionPattern.test(
descriptor.minimumDesktopVersion ?? ''
) ||
!supportedTarget(descriptor.platform, descriptor.architecture) ||
!['x64', 'arm64'].includes(descriptor.architecture) ||
typeof descriptor.signingKeyId !== 'string' ||
!/^sha256:[a-f0-9]{64}$/u.test(
descriptor.contentDigest ?? ''
) ||
!Array.isArray(descriptor.files) ||
descriptor.files.length === 0 ||
descriptor.files.length > 50_000 ||
typeof descriptor.remoteRuntime !== 'object' ||
descriptor.remoteRuntime === null ||
!['opencode', 'continue'].includes(descriptor.remoteRuntime.runtimeId) ||
descriptor.remoteRuntime.provider !== descriptor.remoteRuntime.runtimeId ||
!semanticVersionPattern.test(
descriptor.remoteRuntime.version ?? ''
) ||
!/^sha256:[a-f0-9]{64}$/u.test(
descriptor.remoteRuntime.bundleDigest ?? ''
)
) {
throw new Error('Agent package descriptor is invalid')
}
assertProtocol(descriptor.agentProtocol, 'Agent protocol')
assertAdditionalRuntimes(descriptor)
assertProtocol(
descriptor.remoteRuntime.protocol,
'Remote Runtime protocol'
)
const paths = new Set()
for (const file of descriptor.files) {
if (
typeof file !== 'object' ||
file === null ||
!safePackagePath(file.path) ||
!Number.isSafeInteger(file.size) ||
file.size < 0 ||
!sha256Pattern.test(file.sha256 ?? '') ||
!['0644', '0755'].includes(file.mode) ||
paths.has(file.path)
) {
throw new Error('Agent package file inventory is invalid')
}
paths.add(file.path)
}
if (
descriptor.contentDigest !==
descriptorContentDigest(descriptor)
) {
throw new Error('Agent package content identity is invalid')
}
}
function readPackageMetadata(archivePath, registry) {
const archive = resolve(archivePath)
const streamed = streamPackageMetadata(archive)
const descriptorBytes = streamed.descriptor
const signatureBytes = streamed.signature
if (!descriptorBytes || !signatureBytes) {
throw new Error('Agent package metadata is missing')
}
const descriptor = parseCanonicalJson(
Buffer.from(descriptorBytes),
'Agent package descriptor'
)
assertPackageDescriptor(descriptor)
verifyPackageFileInventory(descriptor, streamed.files)
const key = trustedProductionKey(
registry,
descriptor.signingKeyId
)
verifyDetachedSignature(
Buffer.from(descriptorBytes),
Buffer.from(signatureBytes),
key,
packageSignatureDomain,
'Agent package'
)
const expectedArchive =
`goodbuddy-agent-${descriptor.version}` +
`-${descriptor.platform}-${descriptor.architecture}.gbagent`
if (basename(archive) !== expectedArchive) {
throw new Error(
`Agent package archive must be named ${expectedArchive}`
)
}
return {
archive: expectedArchive,
descriptor,
size: streamed.size,
sha256: streamed.sha256
}
}
function safePackagePath(path) {
return (
typeof path === 'string' &&
path.length > 0 &&
path.length <= 512 &&
!path.startsWith('/') &&
!path.endsWith('/') &&
!path.includes('\\') &&
path.split('/').every(
(part) =>
part.length > 0 &&
part !== '.' &&
part !== '..' &&
!part.endsWith('.') &&
!part.endsWith(' ') &&
!part.includes(':') &&
[...part].every(
(character) => character.charCodeAt(0) > 0x1f
) &&
!windowsReservedNamePattern.test(part)
)
)
}
function verifyPackageFileInventory(descriptor, actualFiles) {
const expected = new Set([
...descriptor.files.map((file) => file.path),
'agent-package.json',
'agent-package.sig'
])
if (
actualFiles.size !== expected.size ||
[...actualFiles.keys()].some((path) => !expected.has(path))
) {
throw new Error(
'Agent package contains undeclared or missing files'
)
}
for (const file of descriptor.files) {
const actual = actualFiles.get(file.path)
if (
!actual ||
actual.size !== file.size ||
actual.sha256 !== file.sha256
) {
throw new Error(
`Agent package payload verification failed: ${file.path}`
)
}
}
}
function centralDirectoryEntries(handle, archiveSize) {
const parsed = readZipCentralDirectory(handle, archiveSize, {
label: 'Agent package',
minimumEntries: 1,
maximumEntries: maximumPackageEntries,
maximumCentralDirectoryBytes
})
const entries = []
const seen = new Set()
let expandedBytes = 0
for (const entry of parsed.entries) {
if (
entry.flags !== 0x0800 ||
entry.compression !== 0 ||
entry.compressedSize === 0xffffffff ||
entry.size === 0xffffffff ||
entry.compressedSize !== entry.size ||
entry.size > maximumPackageEntryBytes ||
entry.diskStart !== 0 ||
entry.localOffset === 0xffffffff
) {
throw new Error('Agent package ZIP entry is unsupported')
}
if (
!safePackagePath(entry.name) ||
seen.has(entry.name)
) {
throw new Error(
'Agent package contains an unsafe or duplicate path'
)
}
seen.add(entry.name)
expandedBytes += entry.size
if (expandedBytes > maximumExpandedPackageBytes) {
throw new Error(
'Agent package expanded payload exceeds its limit'
)
}
const local = readExactZipBytes(
handle,
30,
entry.localOffset,
'Agent package local file header'
)
const localNameLength = local.readUInt16LE(26)
const localExtraLength = local.readUInt16LE(28)
if (
local.readUInt32LE(0) !== localFileHeaderSignature ||
local.readUInt16LE(6) !== entry.flags ||
local.readUInt16LE(8) !== entry.compression ||
local.readUInt32LE(14) !== entry.checksum ||
local.readUInt32LE(18) !== entry.compressedSize ||
local.readUInt32LE(22) !== entry.size ||
localNameLength !== entry.nameBytes.length
) {
throw new Error('Agent package local file header is invalid')
}
const localName = readExactZipBytes(
handle,
localNameLength,
entry.localOffset + 30,
'Agent package local file name'
)
if (!localName.equals(entry.nameBytes)) {
throw new Error('Agent package local file name is invalid')
}
const dataOffset =
entry.localOffset +
30 +
localNameLength +
localExtraLength
const recordEnd = dataOffset + entry.compressedSize
if (recordEnd > parsed.centralOffset) {
throw new Error('Agent package entry exceeds its data region')
}
entries.push({
checksum: entry.checksum,
dataOffset,
name: entry.name,
recordStart: entry.localOffset,
recordEnd
})
}
entries.sort(
(left, right) => left.recordStart - right.recordStart
)
for (let index = 1; index < entries.length; index += 1) {
if (entries[index].recordStart < entries[index - 1].recordEnd) {
throw new Error('Agent package ZIP entries overlap')
}
}
return entries
}
function sameFileSnapshot(left, right) {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.size === right.size &&
left.mtimeMs === right.mtimeMs &&
left.ctimeMs === right.ctimeMs
)
}
function streamPackageMetadata(archivePath) {
const handle = openSync(archivePath, 'r')
const values = new Map()
const files = new Map()
try {
const initialStatus = fstatSync(handle)
if (
!initialStatus.isFile() ||
initialStatus.size <= 0 ||
initialStatus.size > maximumPackageBytes
) {
throw new Error(
'Agent package archive is not a bounded file'
)
}
const entries = centralDirectoryEntries(
handle,
initialStatus.size
)
const states = entries.map((entry) => {
const wanted =
entry.name === 'agent-package.json' ||
entry.name === 'agent-package.sig'
const size = entry.recordEnd - entry.dataOffset
const maximum = entry.name === 'agent-package.json'
? maximumDescriptorBytes
: maximumMetadataBytes
if (wanted && size > maximum) {
throw new Error('Agent package metadata exceeds its limit')
}
return {
...entry,
bytes: 0,
contents: wanted
? Buffer.allocUnsafe(size)
: undefined,
crc32: 0,
hash: createHash('sha256')
}
})
const archiveHash = createHash('sha256')
const buffer = Buffer.allocUnsafe(1024 * 1024)
let position = 0
let firstEntry = 0
while (position < initialStatus.size) {
const bytesRead = readSync(
handle,
buffer,
0,
Math.min(buffer.length, initialStatus.size - position),
position
)
if (bytesRead <= 0) {
throw new Error('Agent package archive is truncated')
}
const chunk = buffer.subarray(0, bytesRead)
const chunkEnd = position + bytesRead
archiveHash.update(chunk)
while (
firstEntry < states.length &&
states[firstEntry].recordEnd <= position
) {
firstEntry += 1
}
for (
let index = firstEntry;
index < states.length &&
states[index].dataOffset < chunkEnd;
index += 1
) {
const state = states[index]
const dataStart = Math.max(position, state.dataOffset)
const dataEnd = Math.min(chunkEnd, state.recordEnd)
if (dataStart >= dataEnd) {
continue
}
const data = chunk.subarray(
dataStart - position,
dataEnd - position
)
state.hash.update(data)
state.crc32 = crc32(data, state.crc32)
if (state.contents) {
data.copy(
state.contents,
dataStart - state.dataOffset
)
}
state.bytes += data.byteLength
}
position = chunkEnd
}
if (!sameFileSnapshot(initialStatus, fstatSync(handle))) {
throw new Error(
'Agent package archive changed during verification'
)
}
for (const state of states) {
const size = state.recordEnd - state.dataOffset
if (
state.bytes !== size ||
(state.crc32 >>> 0) !== state.checksum
) {
throw new Error(
`Agent package ZIP checksum verification failed: ${state.name}`
)
}
files.set(state.name, {
size,
sha256: state.hash.digest('hex')
})
if (state.contents) {
values.set(state.name, state.contents)
}
}
return {
descriptor: values.get('agent-package.json'),
signature: values.get('agent-package.sig'),
files,
sha256: archiveHash.digest('hex'),
size: initialStatus.size
}
} finally {
closeSync(handle)
}
}
function catalogEntry(metadata) {
const descriptor = metadata.descriptor
const {
contentDigest: _contentDigest,
files: _files,
signingKeyId: _signingKeyId,
...identity
} = descriptor
void _contentDigest
void _files
void _signingKeyId
return {
...identity,
archive: metadata.archive,
size: metadata.size,
sha256: metadata.sha256,
downloads: {
github: {
url:
'https://github.com/mesalogo/goodbuddy/releases/download/' +
`agent-v${descriptor.version}/${metadata.archive}`
},
mirror: {
url:
'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' +
`agent-releases/v${descriptor.version}/${metadata.archive}`
}
}
}
}
function assertCatalog(catalog) {
if (
typeof catalog !== 'object' ||
catalog === null ||
catalog.formatVersion !== 1 ||
catalog.product !== 'GoodBuddy' ||
catalog.component !== 'agent' ||
typeof catalog.signingKeyId !== 'string' ||
typeof catalog.generatedAt !== 'string' ||
!Number.isFinite(Date.parse(catalog.generatedAt)) ||
new Date(catalog.generatedAt).toISOString() !==
catalog.generatedAt ||
!Array.isArray(catalog.entries) ||
catalog.entries.length === 0 ||
catalog.entries.length > maximumCatalogEntries
) {
throw new Error('Agent catalog is invalid')
}
const identities = new Set()
for (const entry of catalog.entries) {
if (
typeof entry !== 'object' ||
entry === null ||
!semanticVersionPattern.test(entry.version ?? '') ||
!semanticVersionPattern.test(
entry.minimumDesktopVersion ?? ''
) ||
!supportedTarget(entry.platform, entry.architecture) ||
!['x64', 'arm64'].includes(entry.architecture) ||
entry.archive !==
`goodbuddy-agent-${entry.version}` +
`-${entry.platform}-${entry.architecture}.gbagent` ||
!Number.isSafeInteger(entry.size) ||
entry.size <= 0 ||
!sha256Pattern.test(entry.sha256 ?? '') ||
entry.downloads?.github?.url !==
'https://github.com/mesalogo/goodbuddy/releases/download/' +
`agent-v${entry.version}/${entry.archive}` ||
entry.downloads?.mirror?.url !==
'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' +
`agent-releases/v${entry.version}/${entry.archive}` ||
entry.format !== 'goodbuddy-agent-package' ||
entry.formatVersion !== 1 ||
entry.product !== 'GoodBuddy' ||
entry.component !== 'agent' ||
!['opencode', 'continue'].includes(entry.remoteRuntime?.runtimeId) ||
entry.remoteRuntime?.provider !== entry.remoteRuntime?.runtimeId ||
!semanticVersionPattern.test(
entry.remoteRuntime?.version ?? ''
) ||
!/^sha256:[a-f0-9]{64}$/u.test(
entry.remoteRuntime?.bundleDigest ?? ''
)
) {
throw new Error('Agent catalog entry is invalid')
}
assertProtocol(entry.agentProtocol, 'Agent protocol')
assertAdditionalRuntimes(entry)
assertProtocol(
entry.remoteRuntime.protocol,
'Remote Runtime protocol'
)
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
if (identities.has(identity)) {
throw new Error('Agent catalog contains duplicate entries')
}
identities.add(identity)
}
}
function readVerifiedCatalog(catalogPath, signaturePath, registry) {
const bytes = readFileSync(resolve(catalogPath))
const catalog = parseCanonicalJson(bytes, 'Agent catalog')
assertCatalog(catalog)
const key = trustedProductionKey(
registry,
catalog.signingKeyId
)
verifyDetachedSignature(
bytes,
readFileSync(resolve(signaturePath)),
key,
catalogSignatureDomain,
'Agent catalog'
)
return catalog
}
function assertAdditionalRuntimes(value) {
if (value.additionalRuntimes === undefined) return
if (!Array.isArray(value.additionalRuntimes) || value.additionalRuntimes.length > 1) throw new Error('Invalid additional Runtimes')
for (const runtime of value.additionalRuntimes) {
if (!runtime || !['opencode', 'continue'].includes(runtime.runtimeId) || runtime.runtimeId === value.remoteRuntime.runtimeId ||
runtime.provider !== runtime.runtimeId || !semanticVersionPattern.test(runtime.version ?? '') || !/^sha256:[a-f0-9]{64}$/u.test(runtime.bundleDigest ?? '')) {
throw new Error('Invalid additional Runtime identity')
}
assertProtocol(runtime.protocol, 'Additional Runtime protocol')
}
}
function parseVersion(value) {
const match =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/u
.exec(value)
if (!match) {
throw new Error(`Invalid semantic version: ${value}`)
}
return [
BigInt(match[1]),
BigInt(match[2]),
BigInt(match[3]),
match[4]?.split('.')
]
}
function compareVersions(left, right) {
const leftParts = parseVersion(left)
const rightParts = parseVersion(right)
for (const index of [0, 1, 2]) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1
}
}
const leftPre = leftParts[3]
const rightPre = rightParts[3]
if (!leftPre && !rightPre) return 0
if (!leftPre) return 1
if (!rightPre) return -1
for (
let index = 0;
index < Math.max(leftPre.length, rightPre.length);
index += 1
) {
const leftValue = leftPre[index]
const rightValue = rightPre[index]
if (leftValue === undefined) return -1
if (rightValue === undefined) return 1
if (leftValue === rightValue) continue
const leftNumeric = /^\d+$/u.test(leftValue)
const rightNumeric = /^\d+$/u.test(rightValue)
if (leftNumeric && rightNumeric) {
return BigInt(leftValue) > BigInt(rightValue) ? 1 : -1
}
if (leftNumeric !== rightNumeric) {
return leftNumeric ? -1 : 1
}
return leftValue > rightValue ? 1 : -1
}
return 0
}
function createCatalog(options) {
const projectRoot = options.projectRoot ?? root
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const signingIdentity =
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const signingRecord = trustedProductionKey(
registry,
signingIdentity.keyId
)
if (
publicKeySpkiBase64(signingIdentity.privateKey) !==
signingRecord.publicKeySpkiBase64
) {
throw new Error(
'Agent catalog signing key does not match the trusted registry'
)
}
const packages = [
readPackageMetadata(options.x64Package, registry),
readPackageMetadata(options.arm64Package, registry),
...(options.darwinArm64Package ? [readPackageMetadata(options.darwinArm64Package, registry)] : [])
]
const [first, second] = packages
const additionalIdentity = descriptor => canonicalJson((descriptor.additionalRuntimes ?? []).map(({ bundleDigest: _digest, ...identity }) => { void _digest; return identity }))
if (packages.some(value => additionalIdentity(value.descriptor) !== additionalIdentity(first.descriptor))) {
throw new Error('Agent package matrix has inconsistent additional Runtimes')
}
const {
bundleDigest: _firstRuntimeDigest,
...firstRuntimeIdentity
} = first.descriptor.remoteRuntime
const {
bundleDigest: _secondRuntimeDigest,
...secondRuntimeIdentity
} = second.descriptor.remoteRuntime
void _firstRuntimeDigest
void _secondRuntimeDigest
if (
first.descriptor.version !== second.descriptor.version ||
first.descriptor.minimumDesktopVersion !==
second.descriptor.minimumDesktopVersion ||
canonicalJson(first.descriptor.agentProtocol) !==
canonicalJson(second.descriptor.agentProtocol) ||
canonicalJson(firstRuntimeIdentity) !==
canonicalJson(secondRuntimeIdentity) ||
new Set(packages.map(
(metadata) => metadata.descriptor.architecture
).slice(0, 2)).size !== 2 ||
first.descriptor.platform !== 'linux' ||
second.descriptor.platform !== 'linux'
) {
throw new Error(
'Agent package matrix does not describe one coherent release'
)
}
if (packages[2]) {
const mac = packages[2].descriptor
const { bundleDigest: _digest, ...runtimeIdentity } = mac.remoteRuntime
void _digest
if (
mac.platform !== 'darwin' || mac.architecture !== 'arm64' ||
mac.version !== first.descriptor.version ||
mac.minimumDesktopVersion !== first.descriptor.minimumDesktopVersion ||
canonicalJson(mac.agentProtocol) !== canonicalJson(first.descriptor.agentProtocol) ||
canonicalJson(runtimeIdentity) !== canonicalJson(firstRuntimeIdentity)
) throw new Error('Agent package matrix does not describe one coherent release')
}
const previous =
options.previousCatalog && options.previousSignature
? readVerifiedCatalog(
options.previousCatalog,
options.previousSignature,
registry
)
: undefined
if (
Boolean(options.previousCatalog) !==
Boolean(options.previousSignature)
) {
throw new Error(
'Previous Agent catalog and signature must be provided together'
)
}
const entries = new Map()
for (const entry of previous?.entries ?? []) {
entries.set(`${entry.version}:${entry.platform}:${entry.architecture}`, entry)
}
for (const metadata of packages) {
const entry = catalogEntry(metadata)
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
const existing = entries.get(identity)
if (
existing &&
canonicalJson(existing) !== canonicalJson(entry)
) {
throw new Error(
`Agent release identity is immutable: ${identity}`
)
}
entries.set(identity, entry)
}
const sortedEntries = [...entries.values()].sort(
(left, right) =>
compareVersions(right.version, left.version) ||
left.platform.localeCompare(right.platform, 'en') ||
left.architecture.localeCompare(right.architecture, 'en')
)
if (sortedEntries.length > maximumCatalogEntries) {
throw new Error('Agent catalog exceeds its retention limit')
}
const catalog = {
formatVersion: 1,
product: 'GoodBuddy',
component: 'agent',
signingKeyId: signingIdentity.keyId,
generatedAt: new Date(options.generatedAt).toISOString(),
entries: sortedEntries
}
assertCatalog(catalog)
const bytes = canonicalBytes(catalog)
const signature = sign(
null,
Buffer.concat([catalogSignatureDomain, bytes]),
signingIdentity.privateKey
)
writeFileSync(resolve(options.outputCatalog), bytes)
writeFileSync(
resolve(options.outputSignature),
`${signature.toString('base64')}\n`,
'utf8'
)
return {
version: first.descriptor.version,
catalog: basename(options.outputCatalog),
signature: basename(options.outputSignature),
entries: sortedEntries.length
}
}
function parseArguments(argv) {
const command = argv[0]
if (!['create', 'verify'].includes(command)) {
throw new Error(
'Usage: agent-catalog.cjs <create|verify> [options]'
)
}
const options = {}
for (let index = 1; index < argv.length; index += 2) {
const flag = argv[index]
const value = argv[index + 1]
if (
![
'--x64-package',
'--arm64-package',
'--darwin-arm64-package',
'--previous-catalog',
'--previous-signature',
'--catalog',
'--signature',
'--generated-at'
].includes(flag) ||
!value ||
value.startsWith('--') ||
options[flag] !== undefined
) {
throw new Error('Invalid Agent catalog argument')
}
options[flag] = value
}
for (const flag of ['--catalog', '--signature']) {
if (!options[flag]) {
throw new Error(`Agent catalog argument is required: ${flag}`)
}
}
if (command === 'verify') {
return {
command,
catalog: resolve(options['--catalog']),
signature: resolve(options['--signature'])
}
}
for (const flag of [
'--x64-package',
'--arm64-package',
'--generated-at'
]) {
if (!options[flag]) {
throw new Error(`Agent catalog argument is required: ${flag}`)
}
}
return {
command,
x64Package: resolve(options['--x64-package']),
arm64Package: resolve(options['--arm64-package']),
...(options['--darwin-arm64-package'] ? {
darwinArm64Package: resolve(options['--darwin-arm64-package'])
} : {}),
...(options['--previous-catalog']
? {
previousCatalog: resolve(
options['--previous-catalog']
)
}
: {}),
...(options['--previous-signature']
? {
previousSignature: resolve(
options['--previous-signature']
)
}
: {}),
outputCatalog: resolve(options['--catalog']),
outputSignature: resolve(options['--signature']),
generatedAt: options['--generated-at']
}
}
function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const registry = readTrustedKeyRegistry(root)
const result =
options.command === 'create'
? createCatalog(options)
: {
entries: readVerifiedCatalog(
options.catalog,
options.signature,
registry
).entries.length
}
process.stdout.write(`${JSON.stringify(result)}\n`)
}
module.exports = {
catalogName,
catalogSignatureDomain,
catalogSignatureName,
canonicalBytes,
catalogEntry,
createCatalog,
parseArguments,
readPackageMetadata,
readVerifiedCatalog
}
if (require.main === module) {
try {
main()
} catch (error) {
process.stderr.write(
`${redactSecrets(
error instanceof Error ? error.message : String(error)
)}\n`
)
process.exitCode = 1
}
}
+274
View File
@@ -0,0 +1,274 @@
const { generateKeyPairSync } = require('node:crypto')
const {
mkdirSync,
rmSync
} = require('node:fs')
const {
basename,
dirname,
join,
resolve
} = require('node:path')
const { spawnSync } = require('node:child_process')
const {
buildAgentBundle,
publicKeySpkiBase64,
readRuntimeLock,
supportedArchitectures
} = require('./agent-bundle.cjs')
const {
buildRuntimeBundle,
readRemoteRuntimeLock
} = require('./remote-runtime-bundle.cjs')
const {
assembleAgentPackage
} = require('./agent-package.cjs')
const { sha256File } = require('./file-hash.cjs')
const { targetName } = require('./agent-build-target.cjs')
const ciSigningKeyId = 'goodbuddy-agent-ci-ephemeral'
function parseArguments(argv) {
const options = {}
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
if (
![
'--arch',
'--platform',
'--node-archive',
'--opencode-archive',
'--continue-archive',
'--output-directory',
'--archive'
].includes(argument)
) {
throw new Error(`Unknown Agent CI bundle argument: ${argument}`)
}
const value = argv[index + 1]
if (!value || value.startsWith('--')) {
throw new Error(`${argument} requires a value`)
}
const key = argument.slice(2).replaceAll('-', '')
if (options[key] !== undefined) {
throw new Error(`${argument} may be provided only once`)
}
options[key] = value
index += 1
}
if (!supportedArchitectures.includes(options.arch)) {
throw new Error('--arch must be x64 or arm64')
}
const platform = options.platform ?? 'linux'
targetName(options.arch, platform)
for (const key of [
'nodearchive',
'opencodearchive',
'continuearchive',
'outputdirectory',
'archive'
]) {
if (!options[key]) {
throw new Error(
'Usage: agent-ci-bundle.cjs --arch <x64|arm64> --node-archive <path> --opencode-archive <path> --output-directory <path> --archive <path>'
)
}
}
return {
arch: options.arch,
platform,
nodeArchive: resolve(options.nodearchive),
opencodeArchive: resolve(options.opencodearchive),
continueArchive: resolve(options.continuearchive),
outputDirectory: resolve(options.outputdirectory),
archive: resolve(options.archive)
}
}
function readCiLocks(platform = 'linux', architecture = 'x64') {
const projectRoot = resolve(__dirname, '..')
targetName(architecture, platform)
const lock = readRuntimeLock(projectRoot)
const runtimeLock = readRemoteRuntimeLock(projectRoot)
return { lock, runtimeLock }
}
function ephemeralSigningIdentity() {
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
return {
privateKey,
registry: {
formatVersion: 1,
keys: [
{
keyId: ciSigningKeyId,
publicKeySpkiBase64: publicKeySpkiBase64(publicKey),
environment: 'test'
}
],
revocations: []
}
}
}
async function buildCiAgentBundle(options) {
const projectRoot = resolve(__dirname, '..')
const platform = options.platform ?? 'linux'
const target = targetName(options.arch, platform)
if (process.platform !== platform || process.arch !== options.arch) {
throw new Error(`Agent CI build requires a native ${target} host`)
}
const { lock, runtimeLock } = readCiLocks(platform, options.arch)
const signing = ephemeralSigningIdentity()
const firstArchiveRoot = resolve(
join(
dirname(options.archive),
`.agent-ci-first-${process.pid}`
)
)
mkdirSync(firstArchiveRoot, { recursive: true })
const firstArchive = join(
firstArchiveRoot,
basename(options.archive)
)
const runtimeRoot = resolve(
`${options.outputDirectory}-runtime`
)
try {
const identity = {
keyId: ciSigningKeyId,
privateKey: signing.privateKey
}
rmSync(options.outputDirectory, {
recursive: true,
force: true
})
buildAgentBundle({
platform,
lock,
projectRoot,
arch: options.arch,
runtimeArchive: options.nodeArchive,
outputDirectory: options.outputDirectory,
registry: signing.registry,
testSigningIdentity: identity
})
const runtime = buildRuntimeBundle({
platform,
lock: runtimeLock,
projectRoot,
architecture: options.arch,
runtimeArchive: options.opencodeArchive,
outputRoot: runtimeRoot,
registry: signing.registry,
testSigningIdentity: identity
})
const continueRuntime = buildRuntimeBundle({
platform, runtimeId: 'continue', lock: runtimeLock, projectRoot, architecture: options.arch,
runtimeArchive: options.continueArchive, outputRoot: runtimeRoot,
registry: signing.registry, testSigningIdentity: identity
})
const assemble = (archive) =>
assembleAgentPackage({
platform,
projectRoot,
architecture: options.arch,
minimumDesktopVersion:
require('../package.json').version,
output: archive,
agentBundle: options.outputDirectory,
runtimeBundle: runtime.bundleDirectory,
additionalRuntimeBundles: [continueRuntime.bundleDirectory],
agentLock: lock,
runtimeLock,
registry: signing.registry,
testSigningIdentity: identity
})
assemble(firstArchive)
const firstDigest = await sha256File(firstArchive)
const second = assemble(options.archive)
const archiveDigest = await sha256File(options.archive)
if (firstDigest !== archiveDigest) {
throw new Error('Agent CI archives are not deterministic')
}
const smoke = spawnSync(
resolve(options.outputDirectory, 'goodbuddy-agent'),
['ci-invalid-command'],
{
encoding: 'utf8',
timeout: 10_000,
windowsHide: true
}
)
if (
smoke.error ||
smoke.status !== 2 ||
!smoke.stderr.includes(
'Expected daemon, attach-or-bootstrap, doctor'
)
) {
throw new Error('Built Agent native launch smoke failed')
}
const nativeChecks = [
[
resolve(options.outputDirectory, 'node'),
['-e', `const k = require(${JSON.stringify(resolve(options.outputDirectory, 'lib/node_modules/koffi'))}); if (k.sizeof('int') !== 4) process.exit(1)`]
],
[join(runtime.bundleDirectory, 'bin', 'opencode'), ['--version']]
]
for (const [executable, args] of nativeChecks) {
const result = spawnSync(executable, args, {
encoding: 'utf8',
timeout: 30_000,
windowsHide: true
})
if (
result.error ||
result.status !== 0 ||
(args[0] === '--version' &&
result.stdout.trim() !== runtimeLock.runtimes.opencode.version)
) {
throw new Error(`Built native dependency smoke failed: ${basename(executable)}`)
}
}
return {
platform,
agentVersion: lock.agentVersion,
remoteRuntimeVersion:
runtimeLock.runtimes.opencode.version,
architecture: options.arch,
archive: second.archive,
archiveSha256: archiveDigest
}
} finally {
rmSync(runtimeRoot, { recursive: true, force: true })
rmSync(firstArchive, { force: true })
rmSync(firstArchiveRoot, { recursive: true, force: true })
}
}
async function main(argv = process.argv.slice(2)) {
const result = await buildCiAgentBundle(parseArguments(argv))
process.stdout.write(
`${JSON.stringify(result)}\n`
)
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
)
process.exitCode = 1
})
}
module.exports = {
buildCiAgentBundle,
parseArguments,
readCiLocks
}
+677
View File
@@ -0,0 +1,677 @@
const {
createHash,
sign
} = require('node:crypto')
const {
closeSync,
copyFileSync,
fsyncSync,
lstatSync,
mkdirSync,
mkdtempSync,
openSync,
readSync,
readdirSync,
rmSync,
statSync,
writeSync,
writeFileSync
} = require('node:fs')
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
const { tmpdir } = require('node:os')
const { targetName } = require('./agent-build-target.cjs')
const { crc32 } = require('node:zlib')
const {
buildAgentBundle,
expectedManifestMode: expectedAgentManifestMode,
preflightProductionSigningKey,
productionSigningKey,
publicKeySpkiBase64,
readRuntimeLock,
readTrustedKeyRegistry,
redactSecrets,
verifyLockedBundle
} = require('./agent-bundle.cjs')
const {
buildRuntimeBundle,
canonicalJson,
readRemoteRuntimeLock,
verifyBundleDirectory: verifyRuntimeBundle
} = require('./remote-runtime-bundle.cjs')
const {
centralDirectoryHeaderSignature: zipCentralHeaderSignature,
endOfCentralDirectorySignature: zipEndSignature,
localFileHeaderSignature: zipLocalHeaderSignature
} = require('./zip-central-directory.cjs')
const root = join(__dirname, '..')
const packageDescriptorName = 'agent-package.json'
const packageSignatureName = 'agent-package.sig'
const signatureDomain = Buffer.from(
'GoodBuddy Agent Package Descriptor Signature v1\0',
'utf8'
)
const zipUtf8Flag = 0x0800
const zipVersion = 20
const zipMadeByUnix = (3 << 8) | zipVersion
const zipDosDate = (1 << 5) | 1
const semanticVersionPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
function descriptorBytes(descriptor) {
return Buffer.from(`${JSON.stringify(descriptor, null, 2)}\n`, 'utf8')
}
function descriptorContentDigest(descriptor) {
const { contentDigest: _contentDigest, ...content } = descriptor
void _contentDigest
return `sha256:${createHash('sha256')
.update(canonicalJson(content))
.digest('hex')}`
}
function listFiles(directory) {
const rootPath = resolve(directory)
const output = []
const visit = (current) => {
const currentStat = lstatSync(current)
if (currentStat.isSymbolicLink()) {
throw new Error(`Agent package source contains a symlink: ${current}`)
}
if (currentStat.isFile()) {
output.push(current)
return
}
if (!currentStat.isDirectory()) {
throw new Error(
`Agent package source contains an unsupported entry: ${current}`
)
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
visit(join(current, entry.name))
}
}
visit(rootPath)
return output.sort((left, right) =>
Buffer.from(relative(rootPath, left).split(sep).join('/')).compare(
Buffer.from(relative(rootPath, right).split(sep).join('/'))
)
)
}
function fileDigests(filePath) {
const hash = createHash('sha256')
const handle = openSync(filePath, 'r')
const buffer = Buffer.allocUnsafe(1024 * 1024)
let checksum = 0
try {
while (true) {
const bytesRead = readSync(
handle,
buffer,
0,
buffer.length,
null
)
if (bytesRead === 0) {
break
}
const chunk = buffer.subarray(0, bytesRead)
hash.update(chunk)
checksum = crc32(chunk, checksum)
}
} finally {
closeSync(handle)
}
return {
crc32: checksum >>> 0,
sha256: hash.digest('hex')
}
}
function copyTree(source, destination) {
for (const filePath of listFiles(source)) {
const name = relative(source, filePath).split(sep).join('/')
const target = join(destination, ...name.split('/'))
mkdirSync(dirname(target), { recursive: true })
copyFileSync(filePath, target)
}
}
function packageFileMode(path) {
if (path.startsWith('agent/')) {
return expectedAgentManifestMode(path.slice('agent/'.length))
}
return /(?:\/bin\/opencode|\/lib\/continue\/dist\/cn\.js)$/u.test(path) ? '0755' : '0644'
}
function stagePackagePayload(options) {
const staging = options.staging
copyTree(options.agentBundle, join(staging, 'agent'))
copyTree(
options.runtimeBundle,
join(
staging,
'runtime',
options.runtimeId ?? 'opencode',
basename(options.runtimeBundle)
)
)
for (const runtime of options.additionalRuntimes ?? []) {
copyTree(runtime.bundleDirectory, join(staging, 'runtime', runtime.manifest.runtimeId, basename(runtime.bundleDirectory)))
}
for (const [value, destination] of [
[
options.registry,
'agent-release-keys.json'
],
[
options.agentLock,
'agent-runtime-lock.json'
],
[
options.runtimeLock,
'remote-runtime-lock.json'
]
]) {
writeFileSync(
join(staging, destination),
`${JSON.stringify(value, null, 2)}\n`,
'utf8'
)
}
}
function payloadFiles(staging) {
const crc32ByPath = new Map()
const files = listFiles(staging)
.map((filePath) => {
const path = relative(staging, filePath).split(sep).join('/')
const size = statSync(filePath).size
const digests = fileDigests(filePath)
crc32ByPath.set(path, digests.crc32)
return {
path,
size,
sha256: digests.sha256,
mode: packageFileMode(path)
}
})
.filter(
(file) =>
file.path !== packageDescriptorName &&
file.path !== packageSignatureName
)
return { crc32ByPath, files }
}
function createPackageArchive(
staging,
output,
knownCrc32 = new Map()
) {
mkdirSync(dirname(output), { recursive: true })
const entries = listFiles(staging).map((filePath) => {
const name = relative(staging, filePath).split(sep).join('/')
const nameBytes = Buffer.from(name, 'utf8')
const size = statSync(filePath).size
if (
nameBytes.length === 0 ||
nameBytes.length > 0xffff ||
size > 0xffffffff
) {
throw new Error(`Agent package ZIP entry is too large: ${name}`)
}
return {
filePath,
name,
nameBytes,
size,
crc32:
knownCrc32.get(name) ?? fileDigests(filePath).crc32,
mode: packageFileMode(name),
offset: 0
}
})
if (entries.length === 0 || entries.length > 0xffff) {
throw new Error('Agent package ZIP entry count is invalid')
}
const outputHandle = openSync(output, 'w')
let outputOffset = 0
const archiveHash = createHash('sha256')
const writeBytes = (data) => {
let offset = 0
while (offset < data.byteLength) {
const written = writeSync(
outputHandle,
data,
offset,
data.byteLength - offset
)
if (written <= 0) {
throw new Error('Agent package archive write made no progress')
}
offset += written
}
archiveHash.update(data)
outputOffset += data.byteLength
}
const buffer = Buffer.allocUnsafe(1024 * 1024)
try {
for (const entry of entries) {
entry.offset = outputOffset
const header = Buffer.alloc(30)
header.writeUInt32LE(zipLocalHeaderSignature, 0)
header.writeUInt16LE(zipVersion, 4)
header.writeUInt16LE(zipUtf8Flag, 6)
header.writeUInt16LE(0, 8)
header.writeUInt16LE(0, 10)
header.writeUInt16LE(zipDosDate, 12)
header.writeUInt32LE(entry.crc32, 14)
header.writeUInt32LE(entry.size, 18)
header.writeUInt32LE(entry.size, 22)
header.writeUInt16LE(entry.nameBytes.length, 26)
header.writeUInt16LE(0, 28)
writeBytes(header)
writeBytes(entry.nameBytes)
const inputHandle = openSync(entry.filePath, 'r')
try {
let position = 0
while (position < entry.size) {
const bytesRead = readSync(
inputHandle,
buffer,
0,
Math.min(buffer.length, entry.size - position),
null
)
if (bytesRead <= 0) {
throw new Error(
`Agent package source read made no progress: ${entry.name}`
)
}
position += bytesRead
writeBytes(buffer.subarray(0, bytesRead))
}
} finally {
closeSync(inputHandle)
}
}
const centralOffset = outputOffset
for (const entry of entries) {
const record = Buffer.alloc(46)
record.writeUInt32LE(zipCentralHeaderSignature, 0)
record.writeUInt16LE(zipMadeByUnix, 4)
record.writeUInt16LE(zipVersion, 6)
record.writeUInt16LE(zipUtf8Flag, 8)
record.writeUInt16LE(0, 10)
record.writeUInt16LE(0, 12)
record.writeUInt16LE(zipDosDate, 14)
record.writeUInt32LE(entry.crc32, 16)
record.writeUInt32LE(entry.size, 20)
record.writeUInt32LE(entry.size, 24)
record.writeUInt16LE(entry.nameBytes.length, 28)
record.writeUInt16LE(0, 30)
record.writeUInt16LE(0, 32)
record.writeUInt16LE(0, 34)
record.writeUInt16LE(0, 36)
const unixMode =
0o100000 | Number.parseInt(entry.mode, 8)
record.writeUInt32LE((unixMode << 16) >>> 0, 38)
record.writeUInt32LE(entry.offset, 42)
writeBytes(record)
writeBytes(entry.nameBytes)
}
const centralSize = outputOffset - centralOffset
const end = Buffer.alloc(22)
end.writeUInt32LE(zipEndSignature, 0)
end.writeUInt16LE(0, 4)
end.writeUInt16LE(0, 6)
end.writeUInt16LE(entries.length, 8)
end.writeUInt16LE(entries.length, 10)
end.writeUInt32LE(centralSize, 12)
end.writeUInt32LE(centralOffset, 16)
end.writeUInt16LE(0, 20)
writeBytes(end)
fsyncSync(outputHandle)
} finally {
closeSync(outputHandle)
}
return {
size: outputOffset,
sha256: archiveHash.digest('hex')
}
}
function assembleAgentPackage(options) {
const projectRoot = options.projectRoot ?? root
const architecture = options.architecture
const platform = options.platform ?? 'linux'
const target = targetName(architecture, platform)
if (
!['x64', 'arm64'].includes(architecture) ||
!semanticVersionPattern.test(options.minimumDesktopVersion ?? '') ||
typeof options.output !== 'string'
) {
throw new Error('Agent package identity is invalid')
}
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const agentLock =
options.agentLock ?? readRuntimeLock(projectRoot)
const runtimeLock =
options.runtimeLock ?? readRemoteRuntimeLock(projectRoot)
const signingIdentity =
options.testSigningIdentity ??
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const signingRecord = options.testSigningIdentity
? registry.keys.find(
(key) => key.keyId === signingIdentity.keyId
)
: preflightProductionSigningKey({
projectRoot,
registry,
keyId: signingIdentity.keyId
})
if (!signingRecord) {
throw new Error('Agent package signing key is not registered')
}
if (
publicKeySpkiBase64(signingIdentity.privateKey) !==
signingRecord.publicKeySpkiBase64
) {
throw new Error(
'Agent package signing private key does not match the trusted registry'
)
}
const agent = verifyLockedBundle(
options.agentBundle,
architecture,
{
platform,
projectRoot,
registry,
lock: agentLock,
verificationEnvironment: options.testSigningIdentity
? 'test'
: 'production'
}
)
const runtime = verifyRuntimeBundle(options.runtimeBundle, {
platform,
projectRoot,
architecture,
registry,
lock: runtimeLock,
verificationEnvironment: options.testSigningIdentity
? 'test'
: 'production'
})
const expectedArchiveName =
`goodbuddy-agent-${agent.manifest.agentVersion}` +
`-${target}.gbagent`
if (basename(options.output) !== expectedArchiveName) {
throw new Error(
`Agent package output must be named ${expectedArchiveName}`
)
}
const staging = mkdtempSync(
join(tmpdir(), `goodbuddy-agent-package-${target}-`)
)
try {
const additionalRuntimes = (options.additionalRuntimeBundles ?? []).map(directory => verifyRuntimeBundle(directory, {
platform, projectRoot, architecture, registry, lock: runtimeLock,
verificationEnvironment: options.testSigningIdentity ? 'test' : 'production'
}))
if (new Set([runtime, ...additionalRuntimes].map(value => value.manifest.runtimeId)).size !== additionalRuntimes.length + 1) {
throw new Error('Duplicate packaged Runtime')
}
stagePackagePayload({
staging,
registry,
agentLock,
runtimeLock,
agentBundle: resolve(options.agentBundle),
runtimeBundle: runtime.bundleDirectory,
runtimeId: runtime.manifest.runtimeId,
additionalRuntimes
})
const payload = payloadFiles(staging)
const initial = {
format: 'goodbuddy-agent-package',
formatVersion: 1,
product: 'GoodBuddy',
component: 'agent',
version: agent.manifest.agentVersion,
minimumDesktopVersion: options.minimumDesktopVersion,
platform,
architecture,
signingKeyId: signingIdentity.keyId,
agentProtocol: agent.manifest.protocol,
remoteRuntime: {
runtimeId: runtime.manifest.runtimeId,
provider: runtime.manifest.provider,
version: runtime.manifest.runtimeVersion,
bundleDigest: runtime.manifest.bundleDigest,
protocol: runtime.manifest.protocol
},
...(additionalRuntimes.length ? { additionalRuntimes: additionalRuntimes.map(({ manifest }) => ({
runtimeId: manifest.runtimeId, provider: manifest.provider, version: manifest.runtimeVersion,
bundleDigest: manifest.bundleDigest, protocol: manifest.protocol
})) } : {}),
contentDigest: `sha256:${'0'.repeat(64)}`,
files: payload.files
}
const descriptor = {
...initial,
contentDigest: descriptorContentDigest(initial)
}
const bytes = descriptorBytes(descriptor)
const signature = sign(
null,
Buffer.concat([signatureDomain, bytes]),
signingIdentity.privateKey
)
writeFileSync(join(staging, packageDescriptorName), bytes)
writeFileSync(
join(staging, packageSignatureName),
`${signature.toString('base64')}\n`,
'utf8'
)
const archive = createPackageArchive(
staging,
options.output,
payload.crc32ByPath
)
return {
descriptor,
archive: basename(options.output),
size: archive.size,
sha256: archive.sha256
}
} finally {
rmSync(staging, { recursive: true, force: true })
}
}
function buildAgentPackage(options) {
const projectRoot = options.projectRoot ?? root
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const signingIdentity =
options.testSigningIdentity ??
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const agentRoot = mkdtempSync(
join(tmpdir(), `goodbuddy-agent-build-${options.architecture}-`)
)
const runtimeRoot = mkdtempSync(
join(tmpdir(), `goodbuddy-runtime-build-${options.architecture}-`)
)
try {
const agentBundle = buildAgentBundle({
platform: options.platform ?? 'linux',
projectRoot,
arch: options.architecture,
runtimeArchive: options.nodeArchive,
outputDirectory: join(agentRoot, 'agent'),
registry,
...(options.agentLock ? { lock: options.agentLock } : {}),
...(options.testSigningIdentity
? { testSigningIdentity: options.testSigningIdentity }
: { signingIdentity })
})
const runtime = buildRuntimeBundle({
runtimeId: options.runtimeId,
platform: options.platform ?? 'linux',
projectRoot,
architecture: options.architecture,
runtimeArchive: options.runtimeArchive,
outputRoot: runtimeRoot,
registry,
...(options.runtimeLock ? { lock: options.runtimeLock } : {}),
...(options.testSigningIdentity
? { testSigningIdentity: options.testSigningIdentity }
: { signingIdentity })
})
const additionalRuntimeBundles = []
if (!options.runtimeId || options.runtimeId === 'opencode') {
if (!options.continueArchive) throw new Error('Default Agent package requires the locked Continue archive')
additionalRuntimeBundles.push(buildRuntimeBundle({
runtimeId: 'continue', platform: options.platform ?? 'linux', projectRoot,
architecture: options.architecture, runtimeArchive: options.continueArchive, outputRoot: runtimeRoot,
registry, ...(options.runtimeLock ? { lock: options.runtimeLock } : {}),
...(options.testSigningIdentity ? { testSigningIdentity: options.testSigningIdentity } : { signingIdentity })
}).bundleDirectory)
}
return assembleAgentPackage({
...options,
projectRoot,
registry,
...(options.testSigningIdentity
? {}
: { signingIdentity }),
agentBundle,
runtimeBundle: runtime.bundleDirectory,
additionalRuntimeBundles
})
} finally {
rmSync(agentRoot, { recursive: true, force: true })
rmSync(runtimeRoot, { recursive: true, force: true })
}
}
function parseArguments(argv) {
const command = argv[0]
if (!['build', 'assemble'].includes(command)) {
throw new Error(
'Usage: agent-package.cjs <build|assemble> --arch <x64|arm64> --minimum-desktop-version <version> --output <archive>'
)
}
const options = {}
for (let index = 1; index < argv.length; index += 2) {
const flag = argv[index]
const value = argv[index + 1]
if (
![
'--arch',
'--platform',
'--minimum-desktop-version',
'--node-archive',
'--runtime-archive',
'--runtime-id',
'--continue-archive',
'--continue-bundle',
'--agent-bundle',
'--runtime-bundle',
'--output'
].includes(flag) ||
!value
) {
throw new Error('Invalid Agent package argument')
}
options[
flag.slice(2).replace(/-([a-z])/gu, (_match, valuePart) =>
valuePart.toUpperCase()
)
] = value
}
if (
!['x64', 'arm64'].includes(options.arch) ||
!semanticVersionPattern.test(
options.minimumDesktopVersion ?? ''
) ||
!options.output
) {
throw new Error('Agent package arguments are incomplete')
}
const common = {
runtimeId: options.runtimeId ?? 'opencode',
platform: options.platform ?? 'linux',
architecture: options.arch,
minimumDesktopVersion: options.minimumDesktopVersion,
output: resolve(options.output)
}
if (!['opencode', 'continue'].includes(common.runtimeId)) {
throw new Error('Unsupported Agent package Runtime')
}
targetName(common.architecture, common.platform)
if (command === 'build') {
if (!options.nodeArchive || !options.runtimeArchive) {
throw new Error('Agent package build inputs are incomplete')
}
return {
command,
...common,
nodeArchive: resolve(options.nodeArchive),
runtimeArchive: resolve(options.runtimeArchive),
continueArchive: options.continueArchive ? resolve(options.continueArchive) : undefined
}
}
if (!options.agentBundle || !options.runtimeBundle) {
throw new Error('Agent package assembly inputs are incomplete')
}
return {
command,
...common,
agentBundle: resolve(options.agentBundle),
runtimeBundle: resolve(options.runtimeBundle),
additionalRuntimeBundles: options.continueBundle ? [resolve(options.continueBundle)] : []
}
}
function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const result =
options.command === 'build'
? buildAgentPackage(options)
: assembleAgentPackage(options)
process.stdout.write(`${JSON.stringify(result)}\n`)
}
module.exports = {
assembleAgentPackage,
buildAgentPackage,
canonicalJson,
descriptorBytes,
descriptorContentDigest,
parseArguments,
signatureDomain
}
if (require.main === module) {
try {
main()
} catch (error) {
process.stderr.write(
`${redactSecrets(
error instanceof Error ? error.message : String(error)
)}\n`
)
process.exitCode = 1
}
}
+7 -5
View File
@@ -19,10 +19,10 @@ const manifestName = 'release-manifest.json'
const targetDefinitions = [
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb'] }
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
]
const allowedExtensions = {
nsis: '.exe',
@@ -30,7 +30,8 @@ const allowedExtensions = {
dmg: '.dmg',
zip: '.zip',
AppImage: '.AppImage',
deb: '.deb'
deb: '.deb',
rpm: '.rpm'
}
function parseArguments(argv) {
@@ -319,6 +320,7 @@ async function main(argv = process.argv.slice(2)) {
module.exports = {
aggregateRelease,
assertSafeName,
expectedFormatForFile,
parseArguments,
targetDefinitions
}
+71
View File
@@ -0,0 +1,71 @@
function detectElfArchitecture(buffer) {
if (
buffer.length < 20 ||
buffer[0] !== 0x7f ||
buffer.toString('ascii', 1, 4) !== 'ELF' ||
![1, 2].includes(buffer[5])
) {
return undefined
}
const machine =
buffer[5] === 2
? buffer.readUInt16BE(18)
: buffer.readUInt16LE(18)
if (machine === 62) {
return 'x64'
}
if (machine === 183) {
return 'arm64'
}
return undefined
}
function detectBinaryArchitecture(buffer) {
if (
buffer.length >= 64 &&
buffer[0] === 0x4d &&
buffer[1] === 0x5a
) {
const peOffset = buffer.readUInt32LE(0x3c)
if (
peOffset + 6 <= buffer.length &&
buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
) {
const machine = buffer.readUInt16LE(peOffset + 4)
if (machine === 0x8664) {
return 'x64'
}
if (machine === 0xaa64) {
return 'arm64'
}
}
}
const elfArchitecture = detectElfArchitecture(buffer)
if (elfArchitecture) {
return elfArchitecture
}
if (buffer.length >= 8) {
const littleMagic = buffer.readUInt32LE(0)
const bigMagic = buffer.readUInt32BE(0)
const cpuType =
littleMagic === 0xfeedfacf
? buffer.readUInt32LE(4)
: bigMagic === 0xfeedfacf
? buffer.readUInt32BE(4)
: undefined
if (cpuType === 0x01000007) {
return 'x64'
}
if (cpuType === 0x0100000c) {
return 'arm64'
}
}
return undefined
}
module.exports = {
detectBinaryArchitecture,
detectElfArchitecture
}
+115 -87
View File
@@ -29,19 +29,6 @@ const portablePath = process.env.GOODBUDDY_OUT_DIR
const markerName = '.goodbuddy-portable.json'
const portableLocales = new Set(['zh-CN.pak', 'en-US.pak'])
if (process.platform !== 'win32' || process.arch !== 'x64') {
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
}
if (
portablePath === parse(portablePath).root ||
portablePath === root ||
portablePath === outputRoot
) {
throw new Error(
`拒绝使用不安全的输出目录:${portablePath}`
)
}
function assertOutputUnlocked(directory) {
if (!existsSync(directory)) {
return
@@ -142,6 +129,9 @@ function portableRequiredPaths(directory) {
return [
join(directory, 'GoodBuddy.exe'),
join(directory, 'resources', 'app.asar'),
join(directory, 'resources', 'agent-runtime-lock.json'),
join(directory, 'resources', 'remote-runtime-lock.json'),
join(directory, 'resources', 'agent-release-keys.json'),
join(directory, 'resources', 'icon.ico'),
join(directory, 'resources', 'tray-icon.png'),
join(
@@ -151,6 +141,13 @@ function portableRequiredPaths(directory) {
'opencode',
'opencode.exe'
),
join(
directory,
'resources',
'runtimes',
'opencode',
'rg.exe'
),
join(
directory,
'resources',
@@ -162,7 +159,10 @@ function portableRequiredPaths(directory) {
}
function assertPortableOutput(directory) {
const requiredPaths = portableRequiredPaths(directory)
const requiredPaths = [
...portableRequiredPaths(directory),
join(directory, 'resources', 'runtimes', 'dsh', 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
]
const missing = requiredPaths.filter(
(filePath) => !statSync(filePath, { throwIfNoEntry: false })?.isFile()
)
@@ -219,7 +219,10 @@ function writePortableMarker(directory) {
)
}
function replacePortableOutput(source, destination) {
function replacePortableOutput(
source,
destination
) {
const replacement = `${destination}.replacement-${process.pid}`
const backup = `${destination}.previous-${process.pid}`
mkdirSync(dirname(destination), { recursive: true })
@@ -279,36 +282,76 @@ function replacePortableOutput(source, destination) {
}
}
const electronDist = ensureElectronRuntime()
mkdirSync(outputRoot, { recursive: true })
rmSync(stagingRoot, { recursive: true, force: true })
function main() {
if (process.platform !== 'win32' || process.arch !== 'x64') {
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
}
if (
portablePath === parse(portablePath).root ||
portablePath === root ||
portablePath === outputRoot
) {
throw new Error(
`拒绝使用不安全的输出目录:${portablePath}`
)
}
const electronDist = ensureElectronRuntime()
mkdirSync(outputRoot, { recursive: true })
rmSync(stagingRoot, { recursive: true, force: true })
for (const [label, script, args] of [
[
'Node 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.node.json']
],
[
'Renderer 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.web.json']
],
[
'Production bundle',
join(
root,
'node_modules',
'electron-vite',
'bin',
'electron-vite.js'
),
['build']
]
]) {
const buildResult = spawnSync(
for (const [label, script, args] of [
[
'Node 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.node.json']
],
[
'Renderer 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.web.json']
],
[
'Production bundle',
join(
root,
'node_modules',
'electron-vite',
'bin',
'electron-vite.js'
),
['build']
]
]) {
const buildResult = spawnSync(
process.execPath,
[script, ...args],
{
cwd: root,
env: process.env,
shell: false,
stdio: 'inherit',
windowsHide: true
}
)
if (buildResult.error) {
throw buildResult.error
}
if (buildResult.status !== 0) {
throw new Error(
`${label}失败(code ${buildResult.status ?? 1})`
)
}
}
const result = spawnSync(
process.execPath,
[script, ...args],
[
join(root, 'build', 'run-electron-builder.cjs'),
'--dir',
'--x64',
`--config.directories.output=${stagingRoot}`,
`--config.electronDist=${electronDist}`
],
{
cwd: root,
env: process.env,
@@ -317,55 +360,40 @@ for (const [label, script, args] of [
windowsHide: true
}
)
if (buildResult.error) {
throw buildResult.error
if (result.error) {
rmSync(stagingRoot, { recursive: true, force: true })
throw result.error
}
if (buildResult.status !== 0) {
if (result.status !== 0) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error(
`${label}失败(code ${buildResult.status ?? 1})`
`Electron Builder 构建失败(code ${result.status ?? 1})`
)
}
}
const result = spawnSync(
process.execPath,
[
join(root, 'node_modules', 'electron-builder', 'cli.js'),
'--dir',
'--x64',
`--config.directories.output=${stagingRoot}`,
`--config.electronDist=${electronDist}`
],
{
cwd: root,
env: process.env,
shell: false,
stdio: 'inherit',
windowsHide: true
if (!statSync(unpackedPath, {
throwIfNoEntry: false
})?.isDirectory()) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error('Electron Builder 未生成 portable 目录')
}
try {
pruneLocales(unpackedPath)
assertPortableOutput(unpackedPath)
replacePortableOutput(unpackedPath, portablePath)
} finally {
rmSync(stagingRoot, { recursive: true, force: true })
}
)
if (result.error) {
rmSync(stagingRoot, { recursive: true, force: true })
throw result.error
}
if (result.status !== 0) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error(
`Electron Builder 构建失败(code ${result.status ?? 1})`
)
}
if (!statSync(unpackedPath, { throwIfNoEntry: false })?.isDirectory()) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error('Electron Builder 未生成 portable 目录')
}
try {
pruneLocales(unpackedPath)
assertPortableOutput(unpackedPath)
replacePortableOutput(unpackedPath, portablePath)
} finally {
rmSync(stagingRoot, { recursive: true, force: true })
console.log(`Portable 目录构建完成:${portablePath}`)
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
}
console.log(`Portable 目录构建完成:${portablePath}`)
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
module.exports = {
assertPortableOutput,
portableRequiredPaths
}
if (require.main === module) {
main()
}
+158 -163
View File
@@ -5,6 +5,7 @@ const {
createWriteStream,
existsSync,
closeSync,
fstatSync,
mkdirSync,
mkdtempSync,
openSync,
@@ -35,6 +36,16 @@ const {
} = require('@electron/asar')
const { Zip, ZipDeflate } = require('fflate')
const { sha256File } = require('./file-hash.cjs')
const {
detectBinaryArchitecture
} = require('./binary-architecture.cjs')
const {
readZipCentralDirectory
} = require('./zip-central-directory.cjs')
const {
verifyOpenCodeConfig,
verifyHarnessBundleImports
} = require('./verify-desktop-runtimes.cjs')
const root = join(__dirname, '..')
const packageJson = JSON.parse(
@@ -45,17 +56,28 @@ const packageLock = JSON.parse(
)
const productName = packageJson.build?.productName ?? packageJson.name
const releaseRoot = join(root, 'dist', 'release')
const releaseBuilderConfig = join(
root,
'build',
'electron-builder.release.cjs'
)
const manifestName = 'release-manifest.json'
const portableMarkerName = '.goodbuddy-portable.json'
const harnessHostEntry =
'out/main/deepseek-harness-host-bootstrap.js'
const harnessBundleManifest = 'out/main/package.json'
const harnessPackageVersions = {
'@deepseek-ai/dsh-agent': '0.1.0-rc.6',
'@napi-rs/canvas': '1.0.3',
'node-pty': '1.1.0'
'@deepseek-ai/dsh-agent':
packageLock.packages['node_modules/@deepseek-ai/dsh-agent']
?.version,
'@napi-rs/canvas':
packageLock.packages['node_modules/@napi-rs/canvas']
?.version,
'node-pty':
packageLock.packages['node_modules/node-pty']?.version
}
const koffiVersion = '3.1.4'
const koffiVersion =
packageLock.packages['node_modules/koffi']?.version
const harnessLicenseFiles = [
'agent-client-protocol-Apache-2.0.txt',
'deepseek-cordis-MIT.txt',
@@ -70,9 +92,15 @@ const portableRequiredFiles = [
'resources/release-notes.json',
'resources/icon.ico',
'resources/tray-icon.png',
'resources/tool-environment/managed-python-artifacts.json',
'resources/runtimes/opencode/opencode.exe',
'resources/runtimes/opencode/rg.exe',
'resources/licenses/vscode-ripgrep-MIT.txt',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
'resources/runtimes/continue/package.json',
'resources/runtimes/npm/bin/npm-cli.js',
'resources/runtimes/npm/bin/npx-cli.js',
'resources/runtimes/npm/package.json',
'resources/runtimes/npm/node_modules/graceful-fs/package.json'
]
@@ -102,11 +130,12 @@ const platformDefinitions = {
supportedFormats: ['nsis', 'portable'],
unpackedPattern: /^win(?:-.+)?-unpacked$/u,
executable: [`${productName}.exe`],
runtimeExecutable: 'opencode.exe'
runtimeExecutable: 'opencode.exe',
ripgrepExecutable: 'rg.exe'
},
macos: {
builderFlag: '--mac',
defaultFormats: ['dmg', 'zip'],
defaultFormats: ['dmg'],
supportedFormats: ['dmg', 'zip'],
unpackedPattern: /^mac(?:-.+)?$/u,
executable: [
@@ -115,15 +144,17 @@ const platformDefinitions = {
'MacOS',
productName
],
runtimeExecutable: 'opencode'
runtimeExecutable: 'opencode',
ripgrepExecutable: 'rg'
},
linux: {
builderFlag: '--linux',
defaultFormats: ['AppImage', 'deb'],
supportedFormats: ['AppImage', 'deb'],
defaultFormats: ['AppImage', 'deb', 'rpm'],
supportedFormats: ['AppImage', 'deb', 'rpm'],
unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
executable: [packageJson.name],
runtimeExecutable: 'opencode'
runtimeExecutable: 'opencode',
ripgrepExecutable: 'rg'
}
}
const formatExtensions = {
@@ -132,7 +163,8 @@ const formatExtensions = {
dmg: '.dmg',
zip: '.zip',
AppImage: '.AppImage',
deb: '.deb'
deb: '.deb',
rpm: '.rpm'
}
function normalizePlatform(value) {
@@ -327,7 +359,9 @@ function buildElectronBuilderArguments(options, outputDirectory) {
)
)]
const builderArguments = [
join(root, 'node_modules', 'electron-builder', 'cli.js'),
join(root, 'build', 'run-electron-builder.cjs'),
'--config',
releaseBuilderConfig,
definition.builderFlag,
...builderFormats,
`--${options.arch}`,
@@ -382,61 +416,6 @@ function electronBuilderEnvironment(
return builderEnvironment
}
function detectBinaryArchitecture(buffer) {
if (
buffer.length >= 64 &&
buffer[0] === 0x4d &&
buffer[1] === 0x5a
) {
const peOffset = buffer.readUInt32LE(0x3c)
if (
peOffset + 6 <= buffer.length &&
buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
) {
const machine = buffer.readUInt16LE(peOffset + 4)
if (machine === 0x8664) {
return 'x64'
}
if (machine === 0xaa64) {
return 'arm64'
}
}
}
if (
buffer.length >= 20 &&
buffer[0] === 0x7f &&
buffer.toString('ascii', 1, 4) === 'ELF'
) {
const machine =
buffer[5] === 2
? buffer.readUInt16BE(18)
: buffer.readUInt16LE(18)
if (machine === 62) {
return 'x64'
}
if (machine === 183) {
return 'arm64'
}
}
if (buffer.length >= 8) {
const littleMagic = buffer.readUInt32LE(0)
const bigMagic = buffer.readUInt32BE(0)
const cpuType =
littleMagic === 0xfeedfacf
? buffer.readUInt32LE(4)
: bigMagic === 0xfeedfacf
? buffer.readUInt32BE(4)
: undefined
if (cpuType === 0x01000007) {
return 'x64'
}
if (cpuType === 0x0100000c) {
return 'arm64'
}
}
return undefined
}
function binaryArchitecture(filePath) {
return detectBinaryArchitecture(readChunk(filePath, 4096))
}
@@ -560,9 +539,10 @@ function targetHarnessPaths(options) {
koffiPackage,
koffiBinary,
nodePtyBinary:
options.platform === 'linux'
? 'build/Release/pty.node'
: `prebuilds/${platformName}-${options.arch}/pty.node`,
`prebuilds/${platformName}-${options.arch}/` +
(options.platform === 'windows'
? 'conpty.node'
: 'pty.node'),
nodePtyDirectory: `${platformName}-${options.arch}`
}
}
@@ -851,6 +831,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...harnessHostEntry.split('/')),
'DeepSeek Harness 可执行 Host'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...harnessHostEntry.split('/')), 'utf8'),
harnessHostEntry
)
const harnessLlmChunk = [...entries]
.map((entry) => entry.slice(1).split(sep).join('/'))
.find((entry) =>
@@ -899,6 +883,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...chunkPath.split('/')),
'DeepSeek Harness 可执行 module chunk'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...chunkPath.split('/')), 'utf8'),
chunkPath
)
}
for (const [packageName, expectedVersion] of Object.entries(
harnessPackageVersions
@@ -927,6 +915,10 @@ function verifyHarnessPackage(
join(npmRoot, 'bin', 'npm-cli.js'),
'DSH 插件安装 npm CLI'
)
assertFile(
join(npmRoot, 'bin', 'npx-cli.js'),
'DSH 插件安装 npx CLI'
)
if (
!Array.isArray(npmManifest.bundleDependencies) ||
npmManifest.bundleDependencies.length === 0
@@ -1038,7 +1030,7 @@ function verifyHarnessPackage(
'DeepSeek Harness Canvas'
)
if (options.platform === 'darwin') {
if (options.platform === 'macos') {
const helper = join(
unpackedRoot,
'node_modules',
@@ -1083,10 +1075,41 @@ function verifyUnpackedOutput(directory, options) {
'opencode',
definition.runtimeExecutable
)
const ripgrepExecutable = join(
resources,
'runtimes',
'opencode',
definition.ripgrepExecutable
)
assertFile(applicationExecutable, '应用主程序')
assertFile(join(resources, 'app.asar'), '应用 ASAR')
assertFile(join(resources, 'release-notes.json'), '版本更新说明')
assertFile(
join(
resources,
'tool-environment',
'managed-python-artifacts.json'
),
'托管 Python 产物目录'
)
assertFile(
join(resources, 'agent-release-keys.json'),
'Agent 受信任签名密钥'
)
assertFile(
join(resources, 'agent-runtime-lock.json'),
'Agent Runtime 锁定清单'
)
assertFile(
join(resources, 'remote-runtime-lock.json'),
'Remote Runtime 锁定清单'
)
assertFile(runtimeExecutable, 'OpenCode Runtime')
assertFile(ripgrepExecutable, 'ripgrep Runtime')
verifyOpenCodeConfig(
resources,
join(root, '.runtime-resources', 'opencode-config')
)
assertFile(
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
'Continue Runtime'
@@ -1101,10 +1124,21 @@ function verifyUnpackedOutput(directory, options) {
),
'DSH 插件安装 npm Runtime'
)
assertFile(
join(
resources,
'runtimes',
'npm',
'bin',
'npx-cli.js'
),
'DSH 插件安装 npx Runtime'
)
verifyHarnessPackage(resources, options)
for (const [filePath, label] of [
[applicationExecutable, '应用主程序'],
[runtimeExecutable, 'OpenCode Runtime']
[runtimeExecutable, 'OpenCode Runtime'],
[ripgrepExecutable, 'ripgrep Runtime']
]) {
const actualArchitecture = binaryArchitecture(filePath)
if (actualArchitecture !== options.arch) {
@@ -1300,98 +1334,42 @@ async function createPortableZip(
}
function readZipEntryNames(filePath) {
const fileSize = statSync(filePath).size
if (fileSize < 22) {
throw new Error('Portable ZIP 缺少中央目录')
}
const endChunkSize = Math.min(fileSize, 65_557)
const endChunkStart = fileSize - endChunkSize
const endChunk = readChunk(
filePath,
endChunkSize,
endChunkStart
)
let endOffset = -1
for (let index = endChunk.length - 22; index >= 0; index -= 1) {
if (
endChunk.readUInt32LE(index) === 0x06054b50 &&
index + 22 + endChunk.readUInt16LE(index + 20) ===
endChunk.length
) {
endOffset = index
break
}
}
if (endOffset < 0) {
throw new Error('Portable ZIP 缺少中央目录')
}
const diskNumber = endChunk.readUInt16LE(endOffset + 4)
const centralDisk = endChunk.readUInt16LE(endOffset + 6)
const diskEntryCount = endChunk.readUInt16LE(endOffset + 8)
const entryCount = endChunk.readUInt16LE(endOffset + 10)
const centralSize = endChunk.readUInt32LE(endOffset + 12)
const centralOffset = endChunk.readUInt32LE(endOffset + 16)
const absoluteEndOffset = endChunkStart + endOffset
if (
diskNumber !== 0 ||
centralDisk !== 0 ||
diskEntryCount !== entryCount ||
entryCount === 0xffff ||
centralSize === 0xffffffff ||
centralOffset === 0xffffffff ||
entryCount < portableRequiredFiles.length + 1 ||
entryCount > maxPortableZipEntries ||
centralSize < 46 ||
centralSize > maxPortableCentralDirectoryBytes ||
centralOffset + centralSize !== absoluteEndOffset
) {
throw new Error('Portable ZIP 中央目录无效')
}
const centralDirectory = readChunk(
filePath,
centralSize,
centralOffset
)
const names = []
let offset = 0
for (let index = 0; index < entryCount; index += 1) {
if (
offset + 46 > centralDirectory.length ||
centralDirectory.readUInt32LE(offset) !== 0x02014b50
) {
throw new Error('Portable ZIP 中央目录条目无效')
}
const nameLength = centralDirectory.readUInt16LE(offset + 28)
const extraLength = centralDirectory.readUInt16LE(offset + 30)
const commentLength = centralDirectory.readUInt16LE(offset + 32)
const entryLength = 46 + nameLength + extraLength + commentLength
if (offset + entryLength > centralDirectory.length) {
throw new Error('Portable ZIP 中央目录条目越界')
}
const name = centralDirectory
.subarray(offset + 46, offset + 46 + nameLength)
.toString(
centralDirectory.readUInt16LE(offset + 8) & 0x0800
? 'utf8'
: 'latin1'
const handle = openSync(filePath, 'r')
try {
let parsed
try {
parsed = readZipCentralDirectory(
handle,
fstatSync(handle).size,
{
label: 'Portable ZIP',
minimumEntries: portableRequiredFiles.length + 1,
maximumEntries: maxPortableZipEntries,
maximumCentralDirectoryBytes:
maxPortableCentralDirectoryBytes
}
)
.replaceAll('\\', '/')
if (
!name ||
name.startsWith('/') ||
/^[a-z]:\//iu.test(name) ||
name.includes('\0') ||
name.split('/').some((part) => part === '..')
) {
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
} catch (error) {
throw new Error('Portable ZIP 中央目录无效', {
cause: error
})
}
names.push(name)
offset += entryLength
return parsed.entries.map((entry) => {
const name = entry.name.replaceAll('\\', '/')
if (
!name ||
name.startsWith('/') ||
/^[a-z]:\//iu.test(name) ||
name.includes('\0') ||
name.split('/').some((part) => part === '..')
) {
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
}
return name
})
} finally {
closeSync(handle)
}
if (offset !== centralDirectory.length) {
throw new Error('Portable ZIP 中央目录数量不一致')
}
return names
}
function verifyPortableZip(filePath) {
@@ -1482,6 +1460,12 @@ function verifyArtifactSignature(filePath, format, arch) {
) {
throw new Error('deb 产物不是有效的 ar 归档')
}
if (
format === 'rpm' &&
readChunk(filePath, 4).toString('hex') !== 'edabeedb'
) {
throw new Error('rpm 产物缺少 RPM lead magic')
}
}
async function writeManifest(directory, options) {
@@ -1614,8 +1598,8 @@ function printHelp() {
默认格式:
windows: nsis, portable (ZIP)
macos: dmg, zip
linux: AppImage, deb`)
macos: dmg
linux: AppImage, deb, rpm`)
}
async function main(argv = process.argv.slice(2)) {
@@ -1676,6 +1660,16 @@ async function main(argv = process.argv.slice(2)) {
stagingDirectory,
options
)
if (
process.env.GITHUB_ACTIONS === 'true' &&
options.platform === 'windows' &&
options.arch === 'x64'
) {
await run(process.execPath, [
join(root, 'build', 'run-packaged-deepseek-harness-smoke.cjs'),
unpackedDirectory
])
}
if (
options.platform === 'windows' &&
options.formats.includes('portable')
@@ -1718,6 +1712,7 @@ module.exports = {
replaceOutput,
stageTargetRuntimeDependencies,
targetRuntimePackageNames,
targetHarnessPaths,
verifyHarnessPackage,
verifyArchiveIntegrity,
verifyUnpackedOutput,
@@ -0,0 +1,74 @@
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const { spawnSync } = require('node:child_process')
const maximumInstallerBytes = 256 * 1024
function verifyControlPlanePackageInstaller(
filePath = resolve(
'out',
'main',
'remote-package-installer.mjs'
)
) {
const bytes = readFileSync(filePath)
if (
bytes.byteLength === 0 ||
bytes.byteLength > maximumInstallerBytes
) {
throw new Error(
'Control-plane package installer exceeds its transfer limit'
)
}
const source = new TextDecoder('utf-8', {
fatal: true,
ignoreBOM: true
}).decode(bytes)
if (
/(?:^|\n)\s*(?:import|export)\b[^\r\n]*\bfrom\s*['"]\.{1,2}\//u.test(
source
) ||
/(?:^|\n)\s*import\s*['"]\.{1,2}\//u.test(source) ||
/\bimport\s*\(\s*['"]\.{1,2}\//u.test(source)
) {
throw new Error(
'Control-plane package installer is not a standalone bundle'
)
}
const syntax = spawnSync(
process.execPath,
['--check', filePath],
{
encoding: 'utf8',
windowsHide: true
}
)
if (syntax.status !== 0) {
throw new Error(
`Control-plane package installer is not executable: ${
syntax.stderr.trim() || syntax.stdout.trim()
}`
)
}
return {
size: bytes.byteLength,
sha256: createHash('sha256').update(bytes).digest('hex')
}
}
if (require.main === module) {
const result = verifyControlPlanePackageInstaller(
process.argv[2]
? resolve(process.argv[2])
: undefined
)
process.stdout.write(
`Control-plane package installer: ${result.size} bytes, sha256 ${result.sha256}\n`
)
}
module.exports = {
maximumInstallerBytes,
verifyControlPlanePackageInstaller
}
+99
View File
@@ -0,0 +1,99 @@
// PERF-17 regression gate for the PERF-11 full-App benchmark report.
// Usage: node build/check-perf-thresholds.cjs <result.json | report directory> [thresholds.json]
// Exit codes: 0 all limits met, 1 a limit, scenario or status failed,
// 2 the report cannot be judged (throttled run, unreadable input).
const { existsSync, readFileSync, statSync } = require('node:fs')
const { join, resolve } = require('node:path')
const defaultThresholdsPath = join(__dirname, 'perf-thresholds.json')
function metricValue(scenario, path) {
let value = scenario
for (const key of path.split('.')) {
if (value === null || value === undefined) return undefined
value = value[key]
}
return value
}
// Pure: judges a parsed report against parsed thresholds.
// Returns { ok, refused, reason?, rows: [{ scenario, metric, value, limit, pass, note? }] }.
function evaluate(report, thresholds) {
const rows = []
const throttle = Number(report?.environment?.rendererCpuThrottle ?? 1)
const maxThrottle = Number(thresholds.maxRendererCpuThrottle ?? 1)
if (!(throttle <= maxThrottle)) {
return { ok: false, refused: true, reason: `renderer CPU throttle ${throttle}x exceeds ${maxThrottle}x; thresholds apply to non-throttled runs only`, rows }
}
const requiredStatus = thresholds.requiredStatus ?? 'passed'
rows.push({ scenario: '(report)', metric: 'status', value: report?.status ?? null, limit: `= ${requiredStatus}`, pass: report?.status === requiredStatus })
const scenarios = Array.isArray(report?.scenarios) ? report.scenarios : []
for (const [id, limits] of Object.entries(thresholds.scenarios ?? {})) {
const scenario = scenarios.find(entry => entry?.id === id)
if (!scenario) {
const optional = limits.optional === true
rows.push({ scenario: id, metric: '(present)', value: 'missing', limit: optional ? 'optional' : 'required', pass: optional })
continue
}
for (const [metric, bound] of Object.entries(limits)) {
if (metric === 'optional') continue
const raw = metricValue(scenario, metric)
const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : null
let pass = value !== null
const limitParts = []
if (typeof bound.max === 'number') { limitParts.push(`<= ${bound.max}`); pass = pass && value <= bound.max }
if (typeof bound.min === 'number') { limitParts.push(`>= ${bound.min}`); pass = pass && value >= bound.min }
rows.push({ scenario: id, metric, value, limit: limitParts.join(', '), pass, ...(value === null ? { note: 'metric missing' } : {}) })
}
}
return { ok: rows.every(row => row.pass), refused: false, rows }
}
function resolveReportPath(input) {
const path = resolve(input)
return existsSync(path) && statSync(path).isDirectory() ? join(path, 'result.json') : path
}
function formatValue(value) {
if (typeof value !== 'number') return String(value)
return Number.isInteger(value) ? String(value) : value.toFixed(value < 1 ? 3 : 1)
}
function run(argv) {
const [input, thresholdsArg] = argv
if (!input) {
console.error('usage: node build/check-perf-thresholds.cjs <result.json | report directory> [thresholds.json]')
return 2
}
const reportPath = resolveReportPath(input)
const thresholdsPath = resolve(thresholdsArg || defaultThresholdsPath)
let report
let thresholds
try {
report = JSON.parse(readFileSync(reportPath, 'utf8'))
thresholds = JSON.parse(readFileSync(thresholdsPath, 'utf8'))
} catch (error) {
console.error(`cannot read report or thresholds: ${error instanceof Error ? error.message : error}`)
return 2
}
const result = evaluate(report, thresholds)
console.log(`\nperf thresholds: ${reportPath}`)
if (result.refused) {
console.error(`refused: ${result.reason}`)
return 2
}
console.table(result.rows.map(row => ({
scenario: row.scenario,
metric: row.metric,
value: formatValue(row.value),
limit: row.limit,
pass: row.pass ? 'ok' : `FAIL${row.note ? ` (${row.note})` : ''}`
})))
const failed = result.rows.filter(row => !row.pass).length
console.log(failed ? `${failed} of ${result.rows.length} checks failed` : `all ${result.rows.length} checks passed`)
return result.ok ? 0 : 1
}
module.exports = { evaluate, run, defaultThresholdsPath }
if (require.main === module) process.exitCode = run(process.argv.slice(2))
-386
View File
@@ -1,386 +0,0 @@
const { existsSync, readFileSync, rmSync } = require('node:fs')
const { gzipSync } = require('node:zlib')
const { resolve } = require('node:path')
const rendererBundleBudgets = Object.freeze({
initial: Object.freeze({ raw: 3_500_000, gzip: 720_000 }),
knowledge: Object.freeze({ raw: 330_000, gzip: 50_000 }),
graph: Object.freeze({ raw: 3_500_000, gzip: 720_000 }),
activity: Object.freeze({ raw: 55_000, gzip: 9_000 }),
magicNotes: Object.freeze({ raw: 630_000, gzip: 130_000 }),
settings: Object.freeze({ raw: 650_000, gzip: 105_000 })
})
function normalizePath(value) {
return value.replaceAll('\\', '/')
}
function findEntryKey(manifest) {
const entries = Object.entries(manifest).filter(
([, item]) => item && item.isEntry
)
if (entries.length !== 1) {
throw new Error(
`Expected one renderer entry in the manifest, found ${entries.length}`
)
}
return entries[0][0]
}
function findSourceKey(manifest, sourceSuffix) {
const normalizedSuffix = normalizePath(sourceSuffix)
const matches = Object.entries(manifest).filter(([key, item]) => {
const source = normalizePath(item.src || key)
return source.endsWith(normalizedSuffix)
})
if (matches.length !== 1) {
throw new Error(
`Expected one manifest entry for ${sourceSuffix}, found ${matches.length}`
)
}
return matches[0][0]
}
function findModuleOwnerKeys(manifest, moduleManifest, pattern) {
const keysByFile = new Map(
Object.entries(manifest).map(([key, item]) => [item.file, key])
)
const keys = new Set()
for (const [file, modules] of Object.entries(moduleManifest)) {
if (modules.some((id) => pattern.test(normalizePath(id)))) {
const key = keysByFile.get(normalizePath(file))
if (!key) {
throw new Error(`Module manifest chunk is missing: ${file}`)
}
keys.add(key)
}
}
if (keys.size === 0) {
throw new Error(`No renderer chunk owns modules matching ${pattern}`)
}
return keys
}
function assertSafeModuleManifest(moduleManifest) {
for (const modules of Object.values(moduleManifest)) {
for (const id of modules) {
const normalized = normalizePath(id)
if (
normalized !== id ||
normalized.startsWith('/') ||
normalized.startsWith('../') ||
/[A-Za-z]:\//u.test(normalized) ||
normalized.includes('/Users/') ||
normalized.includes('/home/')
) {
throw new Error(`Unsafe renderer module manifest path: ${id}`)
}
}
}
}
function collectSynchronousClosure(manifest, rootKey) {
if (!manifest[rootKey]) {
throw new Error(`Manifest entry is missing: ${rootKey}`)
}
const visited = new Set()
const pending = [rootKey]
while (pending.length > 0) {
const key = pending.pop()
if (visited.has(key)) {
continue
}
const item = manifest[key]
if (!item) {
throw new Error(`Manifest import is missing: ${key}`)
}
visited.add(key)
for (const imported of item.imports || []) {
pending.push(imported)
}
}
return visited
}
function collectAssetFiles(manifest, keys) {
const files = new Set()
for (const key of keys) {
const item = manifest[key]
if (!item) {
throw new Error(`Manifest entry is missing: ${key}`)
}
files.add(normalizePath(item.file))
for (const cssFile of item.css || []) {
files.add(normalizePath(cssFile))
}
}
return files
}
function measureFiles(files, readAsset, metricCache = new Map()) {
let raw = 0
let gzip = 0
const measuredFiles = [...files].sort()
for (const file of measuredFiles) {
let metrics = metricCache.get(file)
if (!metrics) {
const bytes = Buffer.from(readAsset(file))
metrics = {
raw: bytes.byteLength,
gzip: gzipSync(bytes).byteLength
}
metricCache.set(file, metrics)
}
raw += metrics.raw
gzip += metrics.gzip
}
return { raw, gzip, files: measuredFiles }
}
function withoutItems(keys, excluded) {
return new Set([...keys].filter((key) => !excluded.has(key)))
}
function describeEntry(
manifest,
key,
initialKeys,
initialFiles,
readAsset,
metricCache
) {
const closureKeys = collectSynchronousClosure(manifest, key)
const closureFiles = collectAssetFiles(manifest, closureKeys)
const incrementalKeys = withoutItems(closureKeys, initialKeys)
const incrementalFiles = withoutItems(closureFiles, initialFiles)
const root = measureFiles(
collectAssetFiles(manifest, new Set([key])),
readAsset,
metricCache
)
return {
key,
file: manifest[key].file,
rootRaw: root.raw,
rootGzip: root.gzip,
closureKeys,
incrementalKeys,
...measureFiles(incrementalFiles, readAsset, metricCache)
}
}
function assertDisjoint(description, keys, forbiddenKeys) {
const matches = [...forbiddenKeys].filter((key) => keys.has(key))
if (matches.length > 0) {
throw new Error(
`${description} synchronously includes ${matches.join(', ')}`
)
}
}
/**
* @typedef {object} RendererAnalysisKeys
* @property {string} initial
* @property {string} knowledge
* @property {string} graph
* @property {string} activity
* @property {string} magicNotes
* @property {string} settings
* @property {string[]} g6
*/
/**
* Analyzes one renderer manifest.
*
* `keys` is the authoritative set of discovered roots and module owners;
* notably, `keys.g6` is always an array because G6 may span several chunks.
*
* @returns {{ keys: RendererAnalysisKeys, sections: Record<string, object> }}
*/
function analyzeRendererManifest(manifest, readAsset, moduleManifest) {
assertSafeModuleManifest(moduleManifest)
const metricCache = new Map()
const keys = {
initial: findEntryKey(manifest),
knowledge: findSourceKey(manifest, 'KnowledgeWorkspace.tsx'),
graph: findSourceKey(manifest, 'KnowledgeGraphChart.tsx'),
activity: findSourceKey(manifest, 'ActivityPanel.tsx'),
magicNotes: findSourceKey(manifest, 'MagicNotesWorkspace.tsx'),
settings: findSourceKey(manifest, 'SettingsPanel.tsx')
}
const g6Keys = findModuleOwnerKeys(
manifest,
moduleManifest,
/(?:^|\/)node_modules\/@antv\/g6\//u
)
const initialKeys = collectSynchronousClosure(manifest, keys.initial)
const initialFiles = collectAssetFiles(manifest, initialKeys)
const initialRoot = measureFiles(
collectAssetFiles(manifest, new Set([keys.initial])),
readAsset,
metricCache
)
const initial = {
key: keys.initial,
file: manifest[keys.initial].file,
rootRaw: initialRoot.raw,
rootGzip: initialRoot.gzip,
closureKeys: initialKeys,
incrementalKeys: initialKeys,
...measureFiles(initialFiles, readAsset, metricCache)
}
const sections = {
initial,
knowledge: describeEntry(
manifest,
keys.knowledge,
initialKeys,
initialFiles,
readAsset,
metricCache
),
graph: describeEntry(
manifest,
keys.graph,
initialKeys,
initialFiles,
readAsset,
metricCache
),
activity: describeEntry(
manifest,
keys.activity,
initialKeys,
initialFiles,
readAsset,
metricCache
),
magicNotes: describeEntry(
manifest,
keys.magicNotes,
initialKeys,
initialFiles,
readAsset,
metricCache
),
settings: describeEntry(
manifest,
keys.settings,
initialKeys,
initialFiles,
readAsset,
metricCache
)
}
assertDisjoint(
'The renderer entry',
initialKeys,
new Set([keys.knowledge, keys.graph, keys.activity, ...g6Keys])
)
assertDisjoint(
'The Knowledge shell',
sections.knowledge.closureKeys,
new Set([keys.graph, ...g6Keys])
)
for (const g6Key of g6Keys) {
if (!sections.graph.closureKeys.has(g6Key)) {
throw new Error('The graph chunk no longer synchronously owns G6')
}
}
return { keys: { ...keys, g6: [...g6Keys] }, sections }
}
function checkBudgets(analysis, budgets = rendererBundleBudgets) {
const failures = []
for (const [name, budget] of Object.entries(budgets)) {
const section = analysis.sections[name]
if (!section) {
failures.push(`Unknown budget section: ${name}`)
continue
}
for (const metric of ['raw', 'gzip']) {
if (section[metric] > budget[metric]) {
failures.push(
`${name} ${metric} ${section[metric]} exceeds ${budget[metric]}`
)
}
}
}
if (failures.length > 0) {
throw new Error(`Renderer bundle budget failed:\n- ${failures.join('\n- ')}`)
}
}
function formatBytes(bytes) {
return `${(bytes / 1000).toFixed(2)} kB`
}
function formatReport(analysis) {
return [
'Renderer bundle budget:',
...Object.entries(analysis.sections).map(
([name, section]) =>
`- ${name}: root ${formatBytes(section.rootRaw)} raw / ` +
`${formatBytes(section.rootGzip)} gzip, ` +
`incremental closure ${formatBytes(section.raw)} raw / ` +
`${formatBytes(section.gzip)} gzip`
)
].join('\n')
}
function checkRendererBundle(root = process.cwd()) {
const rendererRoot = resolve(root, 'out', 'renderer')
const manifestPath = resolve(rendererRoot, '.vite', 'manifest.json')
const moduleManifestPath = resolve(
rendererRoot,
'.vite',
'module-manifest.json'
)
if (!existsSync(manifestPath)) {
throw new Error(`Renderer manifest not found: ${manifestPath}`)
}
if (!existsSync(moduleManifestPath)) {
throw new Error(
`Renderer module manifest not found: ${moduleManifestPath}`
)
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const moduleManifest = JSON.parse(
readFileSync(moduleManifestPath, 'utf8')
)
const analysis = analyzeRendererManifest(
manifest,
(file) => readFileSync(resolve(rendererRoot, file)),
moduleManifest
)
checkBudgets(analysis)
rmSync(moduleManifestPath)
return analysis
}
module.exports = {
analyzeRendererManifest,
checkBudgets,
checkRendererBundle,
collectAssetFiles,
collectSynchronousClosure,
findEntryKey,
findModuleOwnerKeys,
findSourceKey,
formatReport,
assertSafeModuleManifest,
measureFiles,
rendererBundleBudgets
}
if (require.main === module) {
try {
const analysis = checkRendererBundle()
console.log(formatReport(analysis))
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
}
}
+16 -36
View File
@@ -4,15 +4,17 @@ const {
writeFileSync
} = require('node:fs')
const { dirname, resolve } = require('node:path')
const {
expectedFormatForFile,
targetDefinitions
} = require('./aggregate-release.cjs')
const supportedTargets = new Map([
['windows-x64', ['nsis', 'portable']],
['windows-arm64', ['nsis', 'portable']],
['macos-x64', ['dmg', 'zip']],
['macos-arm64', ['dmg', 'zip']],
['linux-x64', ['AppImage', 'deb']],
['linux-arm64', ['AppImage', 'deb']]
])
const supportedTargets = new Map(
targetDefinitions.map((target) => [
`${target.platform}-${target.arch}`,
target
])
)
function parseArguments(argv) {
const options = {}
@@ -81,32 +83,6 @@ function assertFile(file, targetKey) {
}
}
function formatForFile(fileName, platform) {
if (platform === 'windows') {
if (/-setup\.exe$/u.test(fileName)) {
return 'nsis'
}
if (/-portable\.zip$/u.test(fileName)) {
return 'portable'
}
} else if (platform === 'macos') {
if (fileName.endsWith('.dmg')) {
return 'dmg'
}
if (fileName.endsWith('.zip')) {
return 'zip'
}
} else if (platform === 'linux') {
if (fileName.endsWith('.AppImage')) {
return 'AppImage'
}
if (fileName.endsWith('.deb')) {
return 'deb'
}
}
return undefined
}
function createSiteRelease(manifest, baseUrlValue) {
if (
!manifest ||
@@ -123,7 +99,8 @@ function createSiteRelease(manifest, baseUrlValue) {
const targets = {}
for (const target of manifest.targets) {
const key = `${target?.platform}-${target?.arch}`
const expectedFormats = supportedTargets.get(key)
const expectedTarget = supportedTargets.get(key)
const expectedFormats = expectedTarget?.formats
if (
!expectedFormats ||
seenTargets.has(key) ||
@@ -140,7 +117,10 @@ function createSiteRelease(manifest, baseUrlValue) {
const files = {}
for (const file of target.files) {
assertFile(file, key)
const format = formatForFile(file.name, target.platform)
const format = expectedFormatForFile(
file.name,
expectedTarget
)
if (!format || !expectedFormats.includes(format) || files[format]) {
throw new Error(`发布文件格式无效:${file.name}`)
}
+3 -2
View File
@@ -17,7 +17,7 @@ const {
const { app, utilityProcess } = require('electron/main')
const protocol = 'goodbuddy.deepseek-harness.control'
const controlVersion = 2
const controlVersion = 3
const byteProtocol = 'goodbuddy.deepseek-harness.byte-stream'
const byteProtocolVersion = 1
const configuredHostPath =
@@ -183,7 +183,8 @@ async function run() {
provider: 'goodbuddy',
model: 'qwen-plus',
supportsImageInput: false,
harnessVersion: '0.1.0-rc.6',
requestHeaders: {},
harnessVersion: '0.1.7-rc.2',
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
skillPackages: [],
extensionPackages: [],
+1
View File
@@ -0,0 +1 @@
module.exports = require('../package.json').build
+1
View File
@@ -0,0 +1 @@
results
+54
View File
@@ -0,0 +1,54 @@
FROM ubuntu:26.04
ARG DEBIAN_FRONTEND=noninteractive
ARG RUST_TOOLCHAIN=1.93.1
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
binutils-loongarch64-linux-gnu \
build-essential \
ca-certificates \
cmake \
dpkg-dev \
fakeroot \
file \
g++-loongarch64-linux-gnu \
gcc-loongarch64-linux-gnu \
libc6-dev-loong64-cross \
make \
ninja-build \
node-gyp \
nodejs \
npm \
pkg-config \
python3 \
qemu-user \
rustup \
tar \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN rustup set profile minimal \
&& rustup toolchain install "${RUST_TOOLCHAIN}" \
--target loongarch64-unknown-linux-gnu
ENV CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
AR=loongarch64-linux-gnu-ar \
RANLIB=loongarch64-linux-gnu-ranlib \
STRIP=loongarch64-linux-gnu-strip \
CARGO_BUILD_TARGET=loongarch64-unknown-linux-gnu \
CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER=loongarch64-linux-gnu-gcc \
RUSTUP_TOOLCHAIN=${RUST_TOOLCHAIN}
COPY toolchain.cmake /opt/goodbuddy/loongarch64-toolchain.cmake
COPY probe.sh /usr/local/bin/goodbuddy-loongarch-cross-probe
COPY preview-deb.sh /usr/local/bin/goodbuddy-loongarch-preview-deb
RUN chmod 0755 \
/usr/local/bin/goodbuddy-loongarch-cross-probe \
/usr/local/bin/goodbuddy-loongarch-preview-deb
WORKDIR /work
ENTRYPOINT ["/usr/local/bin/goodbuddy-loongarch-cross-probe"]
+78
View File
@@ -0,0 +1,78 @@
# LoongArch cross-compilation probe
This directory defines an isolated Ubuntu 26.04 amd64 environment for probing
GoodBuddy's Linux `loong64` prerequisites. It does not build Electron or
OpenCode.
The image includes:
- GCC and G++ targeting `loongarch64-linux-gnu`
- A LoongArch glibc development sysroot
- QEMU user-mode execution
- CMake and Ninja
- Rust's `loongarch64-unknown-linux-gnu` target
- Node.js, npm, and node-gyp
The probe validates:
- C, C++, CMake, and Rust LoongArch ELF output
- C, C++, and Rust execution through QEMU
- A minimal NAPI-RS Rust addon cross-build
- A Debian package with `Architecture: loong64`
- Koffi 3.1.4's published LoongArch glibc binding and the absence of a musl
binding
- The current npm availability of an `@napi-rs/canvas` LoongArch package
- A source cross-build of node-pty 1.1.0
The node-pty probe uses its upstream node-gyp 11.4.2 dependency. Ubuntu's
distribution node-gyp unconditionally links Linux addons with `-lnode`, which
is appropriate for Ubuntu's shared Node.js package but not for Electron native
addons.
Build and run it from this directory on an amd64 Docker host:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
.
mkdir -p results
docker run --rm \
--volume "$PWD/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
Each run writes to a new UTC timestamped directory below `results`. A passing
run contains `summary.txt`, `SHA256SUMS`, the generated ELF files, and the
probe DEB.
QEMU only checks user-mode execution against the cross sysroot. Native Electron
loading, PTY behavior, desktop integration, GPU, audio, and packaging must
still be validated on a real Debian `loong64` host.
Koffi 3.1.4 currently publishes only `linux_loong64/koffi.node`, not a
`musl_loong64` binding. That is sufficient for a Debian desktop package but
does not satisfy GoodBuddy Agent's current dual-glibc/musl bundle contract.
The NAPI-RS probe confirms that a Rust Node addon can be cross-compiled. It
does not build Canvas itself, whose Skia dependency requires a separate port
and native validation.
## Experimental desktop preview
`prepare-input.cjs` creates the architecture-neutral GoodBuddy input archive.
`preview-deb.sh` combines it with the verified community Electron 42.3.0
LoongArch shell and produces an isolated `goodbuddy-loongarch-preview` DEB.
The preview:
- uses a separate application name and user-data directory;
- includes the direct model path and Continue runtime;
- cross-builds the input lockfile's node-pty version against Electron 42.3.0;
- includes Koffi's LoongArch glibc binding;
- does not include GoodBuddy Agent, Remote Runtime, OpenCode, or native Canvas;
- is not part of the production release matrix.
See
[`docs/development/loongarch-preview-build.md`](../../docs/development/loongarch-preview-build.md)
for the complete build and validation procedure.
+108
View File
@@ -0,0 +1,108 @@
const {
existsSync,
mkdirSync,
renameSync,
rmSync,
statSync
} = require('node:fs')
const { dirname, join, resolve } = require('node:path')
const tar = require('tar')
const root = join(__dirname, '..', '..')
const defaultOutput = join(
root,
'dist',
'loongarch-cross',
'goodbuddy-loongarch-cross-input.tgz'
)
const inputPaths = [
'LICENSE',
'agent-runtime-lock.json',
'package.json',
'package-lock.json',
'remote-runtime-lock.json',
'out',
'resources/agent-release-keys.json',
'resources/release-notes.json',
'resources/skills',
'build/icon.png',
'build/icon-tray.png',
'node_modules/@continuedev/cli/package.json',
'node_modules/@continuedev/cli/dist/cn.js',
'node_modules/@continuedev/cli/dist/index.js',
'node_modules/@continuedev/cli/dist/xhr-sync-worker.js',
'node_modules/npm'
]
function parseArguments(argv) {
const options = { output: defaultOutput }
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
if (argument !== '--output') {
throw new Error(`未知参数:${argument}`)
}
const value = argv[index + 1]
if (!value) {
throw new Error('--output 缺少值')
}
options.output = resolve(value)
index += 1
}
return options
}
function verifyInputs(projectRoot = root) {
for (const relativePath of inputPaths) {
if (!existsSync(join(projectRoot, relativePath))) {
throw new Error(`龙芯交叉构建输入缺失:${relativePath}`)
}
}
}
async function createInputArchive(outputPath, projectRoot = root) {
verifyInputs(projectRoot)
const resolvedOutput = resolve(outputPath)
const temporaryOutput = `${resolvedOutput}.partial-${process.pid}`
mkdirSync(dirname(resolvedOutput), { recursive: true })
rmSync(temporaryOutput, { force: true })
try {
await tar.c(
{
cwd: projectRoot,
file: temporaryOutput,
gzip: true,
portable: true,
noMtime: true
},
inputPaths
)
if (!statSync(temporaryOutput).isFile()) {
throw new Error('龙芯交叉构建输入归档未生成')
}
rmSync(resolvedOutput, { force: true })
renameSync(temporaryOutput, resolvedOutput)
} finally {
rmSync(temporaryOutput, { force: true })
}
return resolvedOutput
}
async function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const output = await createInputArchive(options.output)
console.log(`龙芯交叉构建输入已生成:${output}`)
}
module.exports = {
createInputArchive,
inputPaths,
parseArguments,
verifyInputs
}
if (require.main === module) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env bash
set -euo pipefail
input_archive="${1:?missing GoodBuddy input archive}"
electron_archive="${2:?missing Electron loong64 archive}"
output_root="${3:-/preview-output}"
electron_version="42.3.0"
electron_sha256="92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e"
koffi_version="3.1.4"
node_gyp_version="11.4.2"
preview_iteration="1"
application_directory="GoodBuddy-LoongArch-Preview"
executable_name="goodbuddy-loongarch-preview"
assert_file() {
if ! test -f "$1"; then
echo "missing required file: $1" >&2
exit 1
fi
}
assert_loongarch_elf() {
local file_path="$1"
assert_file "${file_path}"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
assert_file "${input_archive}"
assert_file "${electron_archive}"
mkdir -p "${output_root}"
actual_electron_sha256="$(
sha256sum "${electron_archive}" | cut -d ' ' -f 1
)"
if test "${actual_electron_sha256}" != "${electron_sha256}"; then
echo "Electron loong64 archive digest mismatch" >&2
exit 1
fi
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
work_root="${output_root}/.work-${run_id}"
input_root="${work_root}/input"
package_root="${work_root}/package"
application_root="${package_root}/opt/${application_directory}"
resources_root="${application_root}/resources"
app_root="${resources_root}/app"
artifact_root="${output_root}/${run_id}"
if test -e "${work_root}" || test -e "${artifact_root}"; then
echo "refusing to reuse preview build directory" >&2
exit 1
fi
cleanup() {
rm -rf "${work_root}"
}
trap cleanup EXIT
mkdir -p "${input_root}" "${app_root}" "${artifact_root}"
tar -xzf "${input_archive}" -C "${input_root}"
project_version="$(
node -p \
"require('${input_root}/package.json').version"
)"
node_pty_version="$(
node -p \
"require('${input_root}/package-lock.json').packages['node_modules/node-pty'].version"
)"
preview_version="${project_version}-loong64-preview.${preview_iteration}"
deb_version="${project_version}~loong64preview${preview_iteration}"
artifact_name="GoodBuddy-${project_version}-linux-loong64-preview.deb"
echo "== extract verified Electron shell =="
python3 -m zipfile -e "${electron_archive}" "${application_root}"
assert_loongarch_elf "${application_root}/electron"
mv "${application_root}/electron" \
"${application_root}/${executable_name}"
chmod 0755 "${application_root}/${executable_name}"
rm -f "${resources_root}/default_app.asar"
if test -f "${application_root}/chrome-sandbox"; then
chmod 4755 "${application_root}/chrome-sandbox"
fi
echo "== install production JavaScript dependencies =="
cp "${input_root}/package.json" "${app_root}/package.json"
cp "${input_root}/package-lock.json" "${app_root}/package-lock.json"
(
cd "${app_root}"
npm ci \
--ignore-scripts \
--omit=dev \
--cpu=loong64 \
--os=linux
)
rm -rf \
"${app_root}/node_modules/@esbuild" \
"${app_root}/node_modules/@img" \
"${app_root}/node_modules/@rollup"
find "${app_root}/node_modules/@koromix" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-exec rm -rf -- {} + 2>/dev/null || true
find "${app_root}/node_modules/@napi-rs" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-name 'canvas-*' \
-exec rm -rf -- {} + 2>/dev/null || true
node - "${input_root}" "${app_root}/package.json" \
"${preview_version}" <<'NODE'
const { readFileSync, writeFileSync } = require('node:fs')
const [inputRoot, outputPath, version] = process.argv.slice(2)
const metadata = JSON.parse(
readFileSync(`${inputRoot}/package.json`, 'utf8')
)
metadata.name = 'goodbuddy-loongarch-preview'
metadata.productName = 'GoodBuddy LoongArch Preview'
metadata.desktopName = 'GoodBuddy LoongArch Preview'
metadata.version = version
metadata.main = './out/main/index.js'
delete metadata.build
delete metadata.devDependencies
delete metadata.optionalDependencies
writeFileSync(outputPath, `${JSON.stringify(metadata, null, 2)}\n`)
NODE
rm -f "${app_root}/package-lock.json"
echo "== stage target Koffi binding =="
koffi_stage="${work_root}/koffi"
mkdir -p "${koffi_stage}"
koffi_pack_json="$(
npm pack \
"@koromix/koffi-linux-loong64@${koffi_version}" \
--ignore-scripts \
--json \
--pack-destination "${koffi_stage}"
)"
koffi_archive="$(
node -e \
'const value=JSON.parse(process.argv[1]); process.stdout.write(value[0].filename)' \
"${koffi_pack_json}"
)"
koffi_integrity="$(
node -p \
"require('${input_root}/package-lock.json').packages['node_modules/@koromix/koffi-linux-loong64'].integrity"
)"
node - "${koffi_stage}/${koffi_archive}" \
"${koffi_integrity}" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [filePath, integrity] = process.argv.slice(2)
const [algorithm, expected] = integrity.split('-', 2)
const actual = createHash(algorithm)
.update(readFileSync(filePath))
.digest('base64')
if (actual !== expected) {
throw new Error('Koffi archive integrity mismatch')
}
NODE
koffi_target="${app_root}/node_modules/@koromix/koffi-linux-loong64"
mkdir -p "${koffi_target}"
tar \
-xzf "${koffi_stage}/${koffi_archive}" \
-C "${koffi_target}" \
--strip-components 1
koffi_binding="${koffi_target}/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
echo "== cross-build node-pty ${node_pty_version} for Electron ${electron_version} =="
node_gyp_root="${work_root}/node-gyp"
npm install \
--ignore-scripts \
--prefix "${node_gyp_root}" \
"node-gyp@${node_gyp_version}"
node_pty_root="${app_root}/node_modules/node-pty"
test "$(
node -p "require('${node_pty_root}/package.json').version"
)" = "${node_pty_version}"
rm -rf "${node_pty_root}/build" "${node_pty_root}/prebuilds"
(
cd "${node_pty_root}"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target="${electron_version}" \
npm_config_disturl=https://electronjs.org/headers \
"${node_gyp_root}/node_modules/.bin/node-gyp" rebuild \
--arch=loong64
)
node_pty_binding="${node_pty_root}/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== stage GoodBuddy application and architecture-neutral runtimes =="
cp -a "${input_root}/out" "${app_root}/out"
mkdir -p \
"${resources_root}/licenses" \
"${resources_root}/runtimes" \
"${resources_root}/skills"
cp -a "${input_root}/resources/skills/." "${resources_root}/skills/"
cp "${input_root}/resources/release-notes.json" \
"${resources_root}/release-notes.json"
cp "${input_root}/resources/agent-release-keys.json" \
"${resources_root}/agent-release-keys.json"
cp "${input_root}/agent-runtime-lock.json" \
"${resources_root}/agent-runtime-lock.json"
cp "${input_root}/remote-runtime-lock.json" \
"${resources_root}/remote-runtime-lock.json"
cp "${input_root}/build/icon.png" "${resources_root}/icon.png"
cp "${input_root}/build/icon-tray.png" \
"${resources_root}/tray-icon.png"
cp "${input_root}/LICENSE" \
"${resources_root}/licenses/GoodBuddy-0BSD.txt"
mkdir -p "${resources_root}/runtimes/continue/dist"
cp "${input_root}/node_modules/@continuedev/cli/package.json" \
"${resources_root}/runtimes/continue/package.json"
cp "${input_root}/node_modules/@continuedev/cli/dist/cn.js" \
"${resources_root}/runtimes/continue/dist/cn.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/index.js" \
"${resources_root}/runtimes/continue/dist/index.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/xhr-sync-worker.js" \
"${resources_root}/runtimes/continue/dist/xhr-sync-worker.js"
cp -a "${input_root}/node_modules/npm" \
"${resources_root}/runtimes/npm"
echo "== reject foreign native addons =="
while IFS= read -r -d '' native_addon; do
assert_loongarch_elf "${native_addon}"
done < <(
find "${app_root}/node_modules" \
-type f \
-name '*.node' \
-print0
)
if test -e "${resources_root}/agents" ||
test -e "${resources_root}/remote-runtimes" ||
test -e "${resources_root}/runtimes/opencode"
then
echo "preview package unexpectedly contains Agent or OpenCode payload" >&2
exit 1
fi
echo "== build isolated preview DEB =="
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/bin" \
"${package_root}/usr/share/applications" \
"${package_root}/usr/share/icons/hicolor/512x512/apps"
ln -s \
"/opt/${application_directory}/${executable_name}" \
"${package_root}/usr/bin/${executable_name}"
cp "${input_root}/build/icon.png" \
"${package_root}/usr/share/icons/hicolor/512x512/apps/${executable_name}.png"
cat > "${package_root}/usr/share/applications/${executable_name}.desktop" <<EOF
[Desktop Entry]
Name=GoodBuddy LoongArch Preview
Comment=GoodBuddy LoongArch 主体预览版
Exec=/opt/${application_directory}/${executable_name} %U
Terminal=false
Type=Application
Icon=${executable_name}
Categories=Utility;
StartupWMClass=GoodBuddy LoongArch Preview
EOF
cat > "${package_root}/DEBIAN/control" <<EOF
Package: goodbuddy-loongarch-preview
Version: ${deb_version}
Architecture: loong64
Maintainer: MesaLogo
Section: utils
Priority: optional
Depends: libasound2t64, libatspi2.0-0t64, libgbm1, libgtk-3-0t64, libnotify4, libnss3, libpulse0, libsecret-1-0, libuuid1, libxss1, libxtst6, xdg-utils
Description: GoodBuddy LoongArch desktop preview
Experimental LoongArch desktop build without bundled Agent, Remote Runtime,
OpenCode, or native Canvas image decoding.
EOF
artifact="${artifact_root}/${artifact_name}"
dpkg-deb --root-owner-group --build "${package_root}" "${artifact}"
echo "== verify preview package =="
test "$(dpkg-deb --field "${artifact}" Architecture)" = "loong64"
verify_root="${work_root}/verify"
mkdir "${verify_root}"
dpkg-deb --extract "${artifact}" "${verify_root}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/${executable_name}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${node_pty_binding#${app_root}/}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${koffi_binding#${app_root}/}"
(
cd "${artifact_root}"
sha256sum "${artifact_name}" > SHA256SUMS
)
cat > "${artifact_root}/preview-manifest.json" <<EOF
{
"formatVersion": 1,
"product": "GoodBuddy LoongArch Preview",
"goodBuddyVersion": "${project_version}",
"previewVersion": "${preview_version}",
"electronVersion": "${electron_version}",
"architecture": "loong64",
"artifact": "${artifact_name}",
"bundledAgent": false,
"bundledRemoteRuntime": false,
"bundledOpenCode": false,
"nativeCanvas": false,
"continueRuntime": true,
"nodePty": true,
"koffiGlibc": true,
"nativeLaunchVerified": false
}
EOF
cat "${artifact_root}/preview-manifest.json"
echo "artifact=${artifact}"
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env bash
set -euo pipefail
results_root="${1:-/results}"
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
results="${results_root}/${run_id}"
mkdir -p "${results}"
log="${results}/probe.log"
exec > >(tee "${log}") 2>&1
assert_loongarch_elf() {
local file_path="$1"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
echo "== tool versions =="
loongarch64-linux-gnu-gcc --version | head -n 1
loongarch64-linux-gnu-g++ --version | head -n 1
loongarch64-linux-gnu-ld --version | head -n 1
qemu-loongarch64 --version | head -n 1
node --version
npm --version
node-gyp --version
rustc --version
cargo --version
cmake --version | head -n 1
echo "== C probe =="
cat > "${results}/hello.c" <<'EOF'
#include <stdio.h>
int main(void) {
puts("goodbuddy-loongarch-c-ok");
return 0;
}
EOF
loongarch64-linux-gnu-gcc \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.c" \
-o "${results}/hello-c"
assert_loongarch_elf "${results}/hello-c"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-c"
)" = "goodbuddy-loongarch-c-ok"
echo "== C++ probe =="
cat > "${results}/hello.cpp" <<'EOF'
#include <iostream>
#include <string_view>
int main() {
constexpr std::string_view message{"goodbuddy-loongarch-cpp-ok"};
std::cout << message << '\n';
return 0;
}
EOF
loongarch64-linux-gnu-g++ \
-std=c++20 \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.cpp" \
-o "${results}/hello-cpp"
assert_loongarch_elf "${results}/hello-cpp"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-cpp"
)" = "goodbuddy-loongarch-cpp-ok"
echo "== CMake probe =="
mkdir "${results}/cmake-source"
cat > "${results}/cmake-source/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.20)
project(goodbuddy_loongarch_probe LANGUAGES C)
add_executable(goodbuddy-cmake-probe main.c)
target_compile_options(
goodbuddy-cmake-probe
PRIVATE -Wall -Wextra -Werror
)
EOF
cp "${results}/hello.c" "${results}/cmake-source/main.c"
cmake \
-S "${results}/cmake-source" \
-B "${results}/cmake-build" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=/opt/goodbuddy/loongarch64-toolchain.cmake
cmake --build "${results}/cmake-build"
assert_loongarch_elf \
"${results}/cmake-build/goodbuddy-cmake-probe"
echo "== Rust probe =="
mkdir -p "${results}/rust-probe/src"
cat > "${results}/rust-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-rust-probe"
version = "0.1.0"
edition = "2024"
[dependencies]
EOF
cat > "${results}/rust-probe/src/main.rs" <<'EOF'
fn main() {
println!("goodbuddy-loongarch-rust-ok");
}
EOF
cargo build \
--release \
--manifest-path "${results}/rust-probe/Cargo.toml"
rust_binary="$(
find "${results}/rust-probe/target" \
-type f \
-name goodbuddy-loongarch-rust-probe \
-print \
-quit
)"
test -n "${rust_binary}"
assert_loongarch_elf "${rust_binary}"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${rust_binary}"
)" = "goodbuddy-loongarch-rust-ok"
echo "== NAPI-RS Rust addon probe =="
mkdir -p "${results}/napi-rs-probe/src"
cat > "${results}/napi-rs-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-napi-probe"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = { version = "3", default-features = false, features = ["napi8"] }
napi-derive = "3"
[build-dependencies]
napi-build = "2"
EOF
cat > "${results}/napi-rs-probe/build.rs" <<'EOF'
fn main() {
napi_build::setup();
}
EOF
cat > "${results}/napi-rs-probe/src/lib.rs" <<'EOF'
use napi_derive::napi;
#[napi]
pub fn architecture_probe() -> &'static str {
"loong64"
}
EOF
cargo build \
--release \
--manifest-path "${results}/napi-rs-probe/Cargo.toml"
napi_binding="$(
find "${results}/napi-rs-probe/target" \
-type f \
-name 'libgoodbuddy_loongarch_napi_probe.so' \
-print \
-quit
)"
test -n "${napi_binding}"
assert_loongarch_elf "${napi_binding}"
echo "== Debian package probe =="
package_root="${results}/deb-root"
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe"
cp "${results}/hello-c" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe/hello"
cat > "${package_root}/DEBIAN/control" <<'EOF'
Package: goodbuddy-loongarch-cross-probe
Version: 1.0.0
Architecture: loong64
Maintainer: MesaLogo
Description: GoodBuddy LoongArch cross-compilation probe
EOF
dpkg-deb --build \
"${package_root}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb"
test "$(
dpkg-deb \
--field \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
Architecture
)" = "loong64"
echo "== Koffi prebuilt probe =="
mkdir "${results}/koffi"
npm pack \
"@koromix/koffi-linux-loong64@3.1.4" \
--ignore-scripts \
--pack-destination "${results}/koffi"
koffi_archive="$(
find "${results}/koffi" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${koffi_archive}"
mkdir "${results}/koffi/package"
tar \
-xzf "${koffi_archive}" \
-C "${results}/koffi/package" \
--strip-components 1
koffi_binding="${results}/koffi/package/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
if test -e "${results}/koffi/package/musl_loong64/koffi.node"; then
echo "Unexpected Koffi musl binding is present"
exit 1
fi
echo "Koffi loong64 currently provides a glibc binding only"
echo "== Canvas package availability probe =="
canvas_package_status=unavailable
if npm view \
"@napi-rs/canvas-linux-loong64-gnu" \
version \
> "${results}/canvas-package-version.txt" \
2> "${results}/canvas-package-error.txt"
then
canvas_package_status=available
fi
echo "@napi-rs/canvas loong64 package: ${canvas_package_status}"
echo "== node-pty cross-build probe =="
mkdir "${results}/node-pty"
npm pack \
"node-pty@1.1.0" \
--ignore-scripts \
--pack-destination "${results}/node-pty"
node_pty_archive="$(
find "${results}/node-pty" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${node_pty_archive}"
mkdir "${results}/node-pty/package"
tar \
-xzf "${node_pty_archive}" \
-C "${results}/node-pty/package" \
--strip-components 1
(
cd "${results}/node-pty/package"
npm install --ignore-scripts --omit=dev
npm install \
--ignore-scripts \
--no-save \
"node-gyp@11.4.2"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target=43.2.0 \
npm_config_disturl=https://electronjs.org/headers \
./node_modules/.bin/node-gyp rebuild \
--arch=loong64
)
node_pty_binding="${results}/node-pty/package/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== summary =="
sha256sum \
"${results}/hello-c" \
"${results}/hello-cpp" \
"${rust_binary}" \
"${napi_binding}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
"${koffi_binding}" \
"${node_pty_binding}" \
> "${results}/SHA256SUMS"
cat > "${results}/summary.txt" <<EOF
result=pass
target=loong64
c=pass
cpp=pass
cmake=pass
rust=pass
napi_rs=pass
qemu=pass
deb=pass
koffi=pass
koffi_musl=unavailable
canvas_package=${canvas_package_status}
node_pty=pass
EOF
cat "${results}/summary.txt"
echo "results=${results}"
+14
View File
@@ -0,0 +1,14 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR loongarch64)
set(CMAKE_C_COMPILER loongarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER loongarch64-linux-gnu-g++)
set(CMAKE_AR loongarch64-linux-gnu-ar)
set(CMAKE_RANLIB loongarch64-linux-gnu-ranlib)
set(CMAKE_STRIP loongarch64-linux-gnu-strip)
set(CMAKE_FIND_ROOT_PATH /usr/loongarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+65
View File
@@ -0,0 +1,65 @@
const { cp, mkdir, readFile, rm } = require('node:fs/promises');
const { existsSync } = require('node:fs');
const { dirname, join, relative } = require('node:path');
const tar = require('tar');
// Preserve npm's installed layout (including nested versions) and every package
// asset. The official loader discovers plugins and browser assets at runtime.
async function prepareNativeDshResources(projectDir, platform, architecture, downloadPackage, lockedIntegrity) {
const target = join(projectDir, '.runtime-resources', `dsh-${architecture}`);
await rm(target, { recursive: true, force: true });
await mkdir(target, { recursive: true });
const visited = new Set();
function locate(name, from) {
for (let directory = from; ; directory = dirname(directory)) {
const candidate = join(directory, 'node_modules', name);
if (existsSync(join(candidate, 'package.json'))) return candidate;
if (dirname(directory) === directory) return undefined;
}
}
async function copyPackage(directory) {
if (visited.has(directory)) return;
visited.add(directory);
const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
await cp(directory, join(target, relative(projectDir, directory)), { recursive: true, verbatimSymlinks: true });
const optional = manifest.optionalDependencies ?? {};
for (const name of Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies, ...optional })) {
const dependency = locate(name, directory);
if (dependency) await copyPackage(dependency);
else if (!(name in optional) && !manifest.peerDependenciesMeta?.[name]?.optional) {
throw new Error(`DS Web bundle is missing ${name} required by ${manifest.name}`);
}
}
}
await copyPackage(join(projectDir, 'node_modules', '@deepseek-ai', 'dsh'));
// Cross-architecture packaging cannot rely on the build host's npm optional
// selection. Fetch the exact target Sharp packages from the existing lock.
const sharp = JSON.parse(await readFile(join(projectDir, 'node_modules', 'sharp', 'package.json'), 'utf8'));
const lock = JSON.parse(await readFile(join(projectDir, 'package-lock.json'), 'utf8'));
const nativePackages = [`@img/sharp-${platform}-${architecture}`];
if (platform !== 'win32') nativePackages.push(`@img/sharp-libvips-${platform}-${architecture}`);
for (const name of nativePackages) {
const version = sharp.optionalDependencies[name];
if (!version) throw new Error(`DS Web Sharp does not support ${platform}/${architecture}`);
const destination = join(target, 'node_modules', 'sharp', 'node_modules', name);
if (existsSync(join(destination, 'package.json'))) continue;
const locked = lock.packages[`node_modules/sharp/node_modules/${name}`] ?? lock.packages[`node_modules/${name}`];
if (locked?.version !== version || !locked.integrity) throw new Error(`Missing locked ${name}@${version}`);
const integrity = locked.integrity;
const archive = await downloadPackage(projectDir, name, version, integrity);
await mkdir(destination, { recursive: true });
await tar.x({ file: archive, cwd: destination, strip: 1 });
}
const koffi = JSON.parse(await readFile(join(projectDir, 'node_modules', 'koffi', 'package.json'), 'utf8'));
const koffiName = `@koromix/koffi-${platform}-${architecture}`;
const koffiDestination = join(target, 'node_modules', koffiName);
if (!existsSync(join(koffiDestination, 'package.json'))) {
const integrity = await lockedIntegrity(projectDir, koffiName, koffi.version);
const archive = await downloadPackage(projectDir, koffiName, koffi.version, integrity);
await mkdir(koffiDestination, { recursive: true });
await tar.x({ file: archive, cwd: koffiDestination, strip: 1 });
}
return target;
}
module.exports = { prepareNativeDshResources };
+33
View File
@@ -0,0 +1,33 @@
const { existsSync } = require("node:fs");
const { join } = require("node:path");
function npmInvocation(projectDir, environment = process.env) {
const npmCli = environment.npm_execpath;
if (npmCli) {
return {
command: process.execPath,
prefixArgs: [npmCli],
};
}
const bundledNpmCli = join(
projectDir,
"node_modules",
"npm",
"bin",
"npm-cli.js",
);
if (existsSync(bundledNpmCli)) {
return {
command: process.execPath,
prefixArgs: [bundledNpmCli],
};
}
return {
command: process.platform === "win32" ? "npm.cmd" : "npm",
prefixArgs: [],
};
}
module.exports = { npmInvocation };
+110
View File
@@ -0,0 +1,110 @@
const { existsSync } = require("node:fs");
const {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const { npmInvocation } = require("./npm-invocation.cjs");
const opencodeVersion = "1.18.29";
const opencodePluginPackage = "@opencode-ai/plugin";
function prepareBundledOpenCodeConfig(projectDir) {
const targetDirectory = join(
projectDir,
".runtime-resources",
"opencode-config",
);
try {
const installed = JSON.parse(
readFileSync(
join(
targetDirectory,
"node_modules",
"@opencode-ai",
"plugin",
"package.json",
),
"utf8",
),
);
if (installed.version === opencodeVersion) {
return targetDirectory;
}
} catch {
// Rebuild an incomplete or stale offline config template.
}
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
rmSync(stagingDirectory, { recursive: true, force: true });
mkdirSync(stagingDirectory, { recursive: true });
try {
writeFileSync(
join(stagingDirectory, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies: {
[opencodePluginPackage]: opencodeVersion,
},
},
null,
2,
)}\n`,
"utf8",
);
writeFileSync(join(stagingDirectory, ".gitignore"), "node_modules\n");
const npm = npmInvocation(projectDir);
const environment = { ...process.env };
delete environment.NODE_TLS_REJECT_UNAUTHORIZED;
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
"install",
"--ignore-scripts",
"--bin-links=false",
"--omit=dev",
"--omit=optional",
"--no-audit",
"--no-fund",
"--save-exact",
],
{
cwd: stagingDirectory,
encoding: "utf8",
env: environment,
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to prepare bundled OpenCode config: ${
result.error?.message ||
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`,
);
}
rmSync(targetDirectory, { recursive: true, force: true });
mkdirSync(join(targetDirectory, ".."), { recursive: true });
renameSync(stagingDirectory, targetDirectory);
return targetDirectory;
} finally {
if (existsSync(stagingDirectory)) {
rmSync(stagingDirectory, { recursive: true, force: true });
}
}
}
module.exports = {
opencodeVersion,
prepareBundledOpenCodeConfig,
};
+89
View File
@@ -0,0 +1,89 @@
{
"note": "PERF-17 regression ceilings for the PERF-11 full-App benchmark (npm run perf:app). They apply ONLY to non-throttled runs (environment.rendererCpuThrottle = 1, the default) with the default GB_PERF_* scale, on the reference machine of the 2026-10-02 baseline (Intel Core Ultra X7 358H x16, 32 GB, DPR 1, 120 Hz). Limits are about 2-3x the 2026-10-02 values so that run-to-run noise does not fail; they catch regressions of the size seen before PERF-12 (e.g. ~130 long tasks / 9 s while streaming). Tighten them as PERF-12..PERF-14 land; do not loosen them to make a regression pass. Every listed scenario must be present unless marked optional, and report.status must equal requiredStatus.",
"requiredStatus": "passed",
"maxRendererCpuThrottle": 1,
"scenarios": {
"idle": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"typing-small": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"latency.p95": { "max": 25 },
"latency.max": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"idle-seeded": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"typing-seeded": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"latency.p95": { "max": 25 },
"latency.max": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations": {
"renderer.longTasks.count": { "max": 5 },
"renderer.longTasks.totalMs": { "max": 500 },
"renderer.frames.jankRatio": { "max": 0.15 },
"latency.p95": { "max": 350 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations-warm": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.jankRatio": { "max": 0.15 },
"latency.p95": { "max": 200 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations-hot": {
"optional": true,
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.jankRatio": { "max": 0.1 },
"latency.p95": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"open-long-conversation": {
"renderer.longTasks.count": { "max": 4 },
"renderer.longTasks.totalMs": { "max": 400 },
"latency.p95": { "max": 400 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"scroll-long-conversation": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"stream-seeded": {
"renderer.longTasks.count": { "max": 10 },
"renderer.longTasks.totalMs": { "max": 1500 },
"renderer.frames.p95": { "max": 34 },
"renderer.frames.jankRatio": { "max": 0.15 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"stream-and-type": {
"renderer.longTasks.count": { "max": 10 },
"renderer.longTasks.totalMs": { "max": 1500 },
"renderer.frames.p95": { "max": 34 },
"renderer.frames.jankRatio": { "max": 0.2 },
"latency.p95": { "max": 50 },
"main.eventLoopDelay.p99": { "max": 100 }
}
}
}
+2 -2
View File
@@ -32,8 +32,8 @@ function validateItems(value, label) {
fail(`${label} contains a non-string item`)
}
const normalized = item.trim()
if (!normalized || normalized.length > 500) {
fail(`${label} contains an empty or oversized item`)
if (!normalized) {
fail(`${label} contains an empty item`)
}
return normalized
})
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
// Full-App interaction benchmark supervisor (PERF-11).
// Starts a loopback fake OpenAI-compatible model on 127.0.0.1:11434 (the
// fresh-profile default model endpoint), launches the current production
// build in an isolated profile through tests/support/app-perf-driver.mjs,
// enforces a total deadline and prints a summary of the driver report.
const { spawn } = require('node:child_process')
const { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs')
const { createServer } = require('node:http')
const { join, resolve } = require('node:path')
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 || (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)
const streamCharsPerSecond = Number(process.env.GB_PERF_STREAM_CPS || 2_000)
const tickIntervalMs = 10
function buildAnswer(targetBytes) {
const blocks = [
'## Section {n}\n\nThis paragraph explains the {n}th step of the benchmark answer with **bold**, `inline code`, and a [link](https://example.com/{n}).\n\n',
'- First item for block {n}\n- Second item with *emphasis*\n - Nested item {n}\n- Third item\n\n',
'```ts\nexport function step{n}(input: number): number {\n const value = input * {n}\n return value + 1\n}\n```\n\n',
'| Column | Value {n} | Notes |\n| --- | --- | --- |\n| alpha | {n} | first |\n| beta | {n}1 | second |\n\n',
// Single-line display math is common in model output. GB_PERF_CORPUS=multiline-math
// puts the delimiters on their own lines instead.
process.env.GB_PERF_CORPUS === 'multiline-math'
? 'Inline math $a_{n} = b^2 + c$ and display math:\n\n$$\n\\sum_{i=1}^{{n}} i = \\frac{{n}({n}+1)}{2}\n$$\n\n'
: 'Inline math $a_{n} = b^2 + c$ and display math:\n\n$$\\sum_{i=1}^{{n}} i = \\frac{{n}({n}+1)}{2}$$\n\n',
'> Quoted note {n}: keep the streaming renderer honest with mixed blocks.\n\n'
]
let text = ''
let n = 1
while (Buffer.byteLength(text) < targetBytes) {
text += blocks[n % blocks.length].replaceAll('{n}', String(n))
n += 1
}
return text
}
function startModelServer(ledger) {
const answer = buildAnswer(streamBytes)
const server = createServer((request, response) => {
const started = Date.now()
const entry = { method: request.method, path: request.url, startedAt: started }
ledger.push(entry)
if (request.method === 'GET' && request.url?.endsWith('/models')) {
response.writeHead(200, { 'content-type': 'application/json' })
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
}
let body = ''
request.on('data', chunk => { body += chunk })
request.on('end', () => {
let parsed = {}
try { parsed = JSON.parse(body) } catch { /* counted below */ }
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
const content = benchmark ? answer : 'Benchmark title'
if (!entry.stream) {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({
id: 'perf', object: 'chat.completion', model: 'qwen3',
choices: [{ index: 0, finish_reason: 'stop', message: { role: 'assistant', content } }],
usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }
}))
entry.finishedAt = Date.now()
return
}
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
const send = payload => response.write(`data: ${JSON.stringify(payload)}\n\n`)
const delta = text => send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] })
let offset = 0
let chunks = 0
const finish = () => {
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [], usage: { prompt_tokens: 10, completion_tokens: Math.ceil(content.length / 4), total_tokens: 10 + Math.ceil(content.length / 4) } })
response.write('data: [DONE]\n\n')
response.end()
entry.finishedAt = Date.now()
entry.chunks = chunks
entry.bytes = Buffer.byteLength(content)
}
if (!benchmark) { delta(content); finish(); return }
// Emit at a fixed character rate, split into small SSE events like a
// fast token stream. Pacing is time-based so coarse timers do not
// change throughput.
const streamStart = Date.now()
const tick = () => {
if (response.destroyed) { entry.aborted = true; return }
const due = Math.min(content.length, Math.ceil(((Date.now() - streamStart) / 1000) * streamCharsPerSecond))
while (offset < due) {
delta(content.slice(offset, offset + chunkChars))
offset += chunkChars
chunks += 1
}
if (offset >= content.length) { finish(); return }
setTimeout(tick, tickIntervalMs)
}
tick()
})
})
return new Promise((resolvePromise, reject) => {
server.once('error', reject)
server.listen(modelPort, '127.0.0.1', () => resolvePromise(server))
})
}
function format(value, digits = 1) {
return value === null || value === undefined || Number.isNaN(value) ? '-' : Number(value).toFixed(digits)
}
function printSummary(report) {
console.log(`\nGoodBuddy full-App benchmark (${report.status})`)
console.log(`source ${report.source?.commit ?? '?'}${report.source?.dirty ? ' (dirty)' : ''}, Electron ${report.environment?.electron}, DPR ${report.environment?.devicePixelRatio}, ${report.environment?.cpu}`)
if (report.startup) {
console.log(`startup: window ${format(report.startup.windowCreatedMs, 0)} ms, ready-to-show ${format(report.startup.readyToShowMs, 0)} ms, composer ready ${format(report.startup.composerReadyMs, 0)} ms`)
}
const rows = (report.scenarios ?? []).map(s => ({
scenario: s.id,
ms: format(s.durationMs, 0),
longTasks: `${s.renderer?.longTasks?.count ?? '-'} / ${format(s.renderer?.longTasks?.totalMs, 0)} / ${format(s.renderer?.longTasks?.maxMs, 0)}`,
'frame p50/p95/max': `${format(s.renderer?.frames?.p50)} / ${format(s.renderer?.frames?.p95)} / ${format(s.renderer?.frames?.max, 0)}`,
'jank%': format(s.renderer?.frames?.jankRatio * 100),
'latency p50/p95/max': s.latency ? `${format(s.latency.p50)} / ${format(s.latency.p95)} / ${format(s.latency.max)}` : '-',
'mainLag p99/max': `${format(s.main?.eventLoopDelay?.p99)} / ${format(s.main?.eventLoopDelay?.max)}`,
'cpu% main/renderer/gpu': `${format(s.processes?.cpu?.Browser, 0)} / ${format(s.processes?.cpu?.Tab, 0)} / ${format(s.processes?.cpu?.GPU, 0)}`,
domNodes: s.renderer?.domNodes ?? '-',
heapMB: format(s.renderer?.heapUsedMB)
}))
if (rows.length) console.table(rows)
if (report.memoryGrowth) {
console.log(`memory growth (${report.memoryGrowth.description}):`)
console.table(report.memoryGrowth.samples)
}
if (report.environment?.rendererCpuThrottle > 1) console.log(`renderer CPU throttled ${report.environment.rendererCpuThrottle}x`)
if (report.error) console.log(`error: ${report.error}`)
}
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(taskDirectory, 'profile-'))
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
mkdirSync(outputDirectory, { recursive: true })
const ledger = []
let server
try {
server = await startModelServer(ledger)
} 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, 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]
if (modelPort !== defaultModelPort) {
// The fake model ignores the key; it only makes the default profile use the URL.
environment.GOODBUDDY_MODEL_API_KEY = 'perf-loopback'
environment.GOODBUDDY_MODEL_BASE_URL = `http://127.0.0.1:${modelPort}/v1`
environment.GOODBUDDY_MODEL_NAME = 'qwen3'
}
environment.GOODBUDDY_WORKSPACE = join(runDirectory, 'workspace')
mkdirSync(environment.GOODBUDDY_WORKSPACE, { recursive: true })
const started = Date.now()
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
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
else child.kill('SIGKILL')
}, totalTimeoutMs)
const exit = await new Promise(resolvePromise => child.once('exit', (code, signal) => resolvePromise({ code, signal })))
clearTimeout(timer)
await new Promise(resolvePromise => server.close(() => resolvePromise()))
const resultPath = join(outputDirectory, 'result.json')
let report
if (existsSync(resultPath)) report = JSON.parse(readFileSync(resultPath, 'utf8'))
else report = { status: 'failed', error: timedOut ? 'Supervisor timeout before report' : 'Driver exited without a report' }
report.supervisor = { elapsedMs: Date.now() - started, timeoutMs: totalTimeoutMs, timedOut, exitCode: exit.code, signal: exit.signal }
report.corpus = { kind: process.env.GB_PERF_CORPUS || 'single-line-math', bytes: streamBytes, charsPerSecond: streamCharsPerSecond, charsPerEvent: chunkChars }
report.modelRequests = {
boundary: 'loopback fake model only; no external requests',
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, 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(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).
if (process.env.GB_PERF_CHECK === '1') process.exitCode = require('./check-perf-thresholds.cjs').run([resultPath])
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+31
View File
@@ -0,0 +1,31 @@
const { spawnSync } = require('node:child_process')
const { rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { buildSync } = require('esbuild')
const output = join(tmpdir(), `goodbuddy-browser-tabs-e2e-${process.pid}.cjs`)
try {
buildSync({
entryPoints: [join(process.cwd(), 'tests', 'browser-tabs-electron-e2e.ts')],
outfile: output,
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
external: ['electron']
})
const environment = { ...process.env }
delete environment.ELECTRON_RUN_AS_NODE
const result = spawnSync(require('electron'), [output], {
cwd: process.cwd(),
env: environment,
stdio: 'inherit',
timeout: 120_000
})
if (result.error) throw result.error
process.exitCode = result.status ?? 1
} finally {
rmSync(output, { force: true })
}
+1 -1
View File
@@ -88,7 +88,7 @@ async function main() {
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version: '0.1.0-rc.6',
version: '0.1.7-rc.2',
private: true,
type: 'module'
},
+109
View File
@@ -0,0 +1,109 @@
// Document parse benchmark (PERF-15/16, P7). Bundles the production parser
// client and worker with esbuild, then imports synthetic large documents
// (hash -> parse -> chunk, as Knowledge import does) twice per round: inline on
// the main thread, then through the parse worker. Records the main event-loop
// max block with perf_hooks.monitorEventLoopDelay.
//
// node build/run-document-parse-perf.cjs
// GB_DOCPERF_ROUNDS=3 GB_DOCPERF_NODE=1 (skip Electron) GB_DOCPERF_OUTPUT=file.json
const { buildSync } = require('esbuild')
const { mkdirSync, rmSync, writeFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
// Measure on the project's Electron (the production runtime), like perf:knowledge.
if (process.env.GB_DOCPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const rounds = Number(process.env.GB_DOCPERF_ROUNDS || 3)
// Under node_modules so the external pdfjs-dist resolves from the repository.
const directory = join(root, 'node_modules', '.cache', `goodbuddy-document-parse-perf-${process.pid}`)
mkdirSync(directory, { recursive: true })
const harness = join(directory, 'harness.cjs')
const workerBundle = join(directory, 'document-parse-worker.cjs')
const harnessSource = join(directory, 'harness.ts')
writeFileSync(harnessSource, [
`export * from ${JSON.stringify(join(root, 'src/main/document-parse-client.ts'))}`,
`export * from ${JSON.stringify(join(root, 'tests/support/document-parse-worker-fixture.ts'))}`
].join('\n'))
for (const [entry, outfile] of [[harnessSource, harness], [join(root, 'src/main/document-parse-worker.ts'), workerBundle]]) {
buildSync({
entryPoints: [entry], bundle: true, platform: 'node', format: 'cjs', target: 'node22',
outfile, logLevel: 'error', external: ['pdfjs-dist', 'electron', 'esbuild']
})
}
const {
DocumentParseWorkerClient, chunkDocumentOffMain, parseDocumentOffMain, sha256OffMain,
createDocx, createLargePdf, sentenceGenerator
} = require(harness)
const settings = {
version: 1, mode: 'structure', targetCharacters: 1600, overlapCharacters: 100,
parentCharacters: 1600, childCharacters: 400, contextualIndexingEnabled: false
}
const round = value => Math.round(value * 10) / 10
async function importOnce(name, buffer, client) {
await sha256OffMain(buffer, client)
const parsed = await parseDocumentOffMain(name, buffer, undefined, client)
return (await chunkDocumentOffMain(parsed, settings, client)).length
}
async function measure(name, buffer, client) {
await importOnce(name, buffer, client) // warm modules / worker
await new Promise(resolve => setTimeout(resolve, 50))
const histogram = monitorEventLoopDelay({ resolution: 5 })
histogram.enable()
// The histogram only measures from its first timer tick on; a block that
// starts before that tick (the inline path is all microtasks) is not seen.
await new Promise(resolve => setTimeout(resolve, 30))
const started = performance.now()
const chunks = await importOnce(name, buffer, client)
const wallMs = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
histogram.disable()
return { maxMs: round(histogram.max / 1e6), p99Ms: round(histogram.percentile(99) / 1e6), wallMs: round(wallMs), chunks }
}
async function main() {
console.log(`[docperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const sentence = sentenceGenerator(11)
const inputs = [
{ name: 'large.pdf', buffer: createLargePdf(500, 60) },
{ name: 'large.docx', buffer: createDocx(Array.from({ length: 4000 }, () => sentence(25))) },
{ name: 'large.md', buffer: Buffer.from(Array.from({ length: 4000 }, (_, index) => `${index % 50 === 0 ? '## Section\n' : ''}${sentence(60)}.`).join('\n')) }
]
const client = new DocumentParseWorkerClient(workerBundle)
const results = []
try {
for (const input of inputs) {
for (let index = 0; index < rounds; index += 1) {
// Alternate inline / worker within each round (P7).
const inline = await measure(input.name, input.buffer, undefined)
const worker = await measure(input.name, input.buffer, client)
if (worker.chunks !== inline.chunks) throw new Error(`chunk count mismatch for ${input.name}`)
results.push({ input: input.name, bytes: input.buffer.byteLength, round: index + 1, inline, worker })
console.log(`[docperf] ${input.name} round ${index + 1}: Main loop max ${inline.maxMs} -> ${worker.maxMs} ms; wall ${inline.wallMs} / ${worker.wallMs} ms; ${inline.chunks} chunks`)
}
}
} finally {
client.close()
}
const output = process.env.GB_DOCPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-document-parse-benchmark', node: process.version, results }, null, 2))
console.table(results.map(r => ({ input: r.input, round: r.round, 'loop max inline/worker ms': `${r.inline.maxMs} / ${r.worker.maxMs}`, 'wall inline/worker ms': `${r.inline.wallMs} / ${r.worker.wallMs}` })))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+32
View File
@@ -0,0 +1,32 @@
'use strict'
function configureElectronBuilderLogLevels(
moduleManager = require(
'app-builder-lib/out/node-module-collector/moduleManager'
)
) {
const duplicateReferenceMessage =
moduleManager.LogMessageByKey.PKG_DUPLICATE_REF
if (
!duplicateReferenceMessage ||
!moduleManager.logMessageLevelByKey[
duplicateReferenceMessage
]
) {
throw new Error(
'当前 electron-builder 不支持依赖引用日志级别配置'
)
}
moduleManager.logMessageLevelByKey[
duplicateReferenceMessage
] = 'debug'
}
if (require.main === module) {
configureElectronBuilderLogLevels()
require('electron-builder/cli')
}
module.exports = {
configureElectronBuilderLogLevels
}
+160
View File
@@ -0,0 +1,160 @@
// 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
// 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, mkdirSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
// Measure on the project's Electron (the production runtime). Older runtimes
// (Node 24.18 / Electron 43) stall worker threads for hundreds of ms on many
// small ArrayBuffer allocations (SQLite BLOB rows); GB_KPERF_NODE=1 opts out.
if (process.env.GB_KPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const sizes = (process.env.GB_KPERF_SIZES || '5000,20000,50000').split(',').map(Number)
const dimensions = Number(process.env.GB_KPERF_DIMENSIONS || 384)
const chunksPerDocument = 100
const queries = Number(process.env.GB_KPERF_QUERIES || 15)
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-reader.ts', readerBundle]
]) {
buildSync({
entryPoints: [join(root, entry)],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outfile,
logLevel: 'error',
// The worker reuses AssistantDatabase, which imports Electron-only modules lazily.
external: ['electron']
})
}
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
const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296)
const words = 'lighthouse harbor policy engine renderer stream vector index budget latency schedule archive network storage contract invoice release window memory review'.split(' ')
const sentence = n => Array.from({ length: n }, () => words[Math.floor(random() * words.length)]).join(' ')
const vector = () => Array.from({ length: dimensions }, () => random() * 2 - 1)
const checksum = value => createHash('sha256').update(value).digest('hex')
function percentile(values, p) {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
function time(body) {
const samples = []
body() // warm statement cache and page cache
for (let index = 0; index < queries; index += 1) {
const started = performance.now()
body()
samples.push(performance.now() - started)
}
return { p50: round(percentile(samples, 50)), p95: round(percentile(samples, 95)), max: round(Math.max(...samples)) }
}
const round = value => Math.round(value * 10) / 10
// 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 })
await body() // warm worker / caches outside the measurement
// Let the warm-up's blocked interval drain before sampling.
await new Promise(resolve => setTimeout(resolve, 50))
histogram.enable()
const started = performance.now()
for (let index = 0; index < queries; index += 1) {
await body()
// Yield like a real IPC handler, so the histogram samples between queries.
await new Promise(resolve => setImmediate(resolve))
}
const wallMs = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
histogram.disable()
const ms = value => round(value / 1e6)
return { p50: ms(histogram.percentile(50)), p99: ms(histogram.percentile(99)), max: ms(histogram.max), perQueryMs: round(wallMs / queries) }
}
const results = []
async function main() {
console.log(`[kperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
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
const seedStarted = performance.now()
for (let documentIndex = 0; documentIndex < size / chunksPerDocument; documentIndex += 1) {
const source = database.upsertSource({ knowledgeBaseId: library.id, type: 'file', location: `C:\\perf\\doc-${documentIndex}.md`, displayName: `doc-${documentIndex}.md`, status: 'ready' })
const chunks = Array.from({ length: chunksPerDocument }, (_, ordinal) => ({ id: `d${documentIndex}-c${ordinal}`, ordinal, content: sentence(80), location: `line ${ordinal + 1}` }))
const document = database.upsertDocument({ knowledgeBaseId: library.id, sourceId: source.id, externalId: `doc-${documentIndex}`, title: `doc-${documentIndex}`, sourceLocation: source.location }, chunks)
const stored = sqlite.prepare("SELECT id, index_content FROM chunks WHERE document_id = ? AND enabled = 1 AND role <> 'parent'").all(document.id)
database.replaceDocumentEmbeddings(document.id, 'perf', 'perf-model', stored.map(row => ({ chunkId: row.id, contentChecksum: checksum(row.index_content), vector: vector() })))
}
const seedMs = performance.now() - seedStarted
database.close()
// 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 = {
chunks: size,
dimensions,
seedSeconds: round(seedMs / 1000),
ftsMs: time(() => reopened.search({ knowledgeBaseId: library.id, query: 'lighthouse budget', limit: 40 })),
vectorMs: time(() => reopened.vectorSearch({ knowledgeBaseId: library.id, provider: 'perf', model: 'perf-model', vector: query, limit: 40 })),
hybridMs: time(() => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
}
// Harness loop delay: owner-local synchronous search vs its explicit read worker.
entry.mainLoopDelaySyncMs = await loopDelay(async () => reopened.hybridSearchWithDiagnostics(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', 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(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 })
})
+283
View File
@@ -0,0 +1,283 @@
// 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
// - 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 +
// JSON.stringify of every record) vs current
// activity-history:update one changed record via the incremental API
// activity startup load whole list (pre-paging get) vs first page (200)
// + summary on the readonly worker
// activity page / summary sync paths, for reference
// Env: GB_MPERF_CONVERSATIONS, GB_MPERF_MESSAGES, GB_MPERF_ACTIVITY,
// 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 { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
if (process.env.GB_MPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const conversationCount = Number(process.env.GB_MPERF_CONVERSATIONS || 1000)
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 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({
entryPoints: [join(root, entry)], bundle: true, platform: 'node', format: 'cjs',
target: 'node22', outfile, logLevel: 'error', external: ['electron']
})
}
const { AssistantDatabase } = require(bundle)
const { ReadonlyQueryReader } = require(readerBundle)
let ownedDatabase, reader
const { activityHistorySnapshotSchema } = require(schemaBundle)
let seed = 42
const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296)
const words = 'lighthouse harbor policy engine renderer stream vector index budget latency schedule archive network storage contract invoice release window memory review 发布 审批 流程'.split(' ')
const sentence = n => Array.from({ length: n }, () => words[Math.floor(random() * words.length)]).join(' ')
const round = value => Math.round(value * 10) / 10
const percentile = (values, p) => {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
/**
* Runs `body` `calls` times (after one warm-up) and reports the per-call wall
* time and the harness event-loop delay histogram (p99/max) over the run.
*/
async function measure(body) {
await body(-1)
await new Promise(resolve => setTimeout(resolve, 50))
// monitorEventLoopDelay is bound to the ~15 ms Windows timer granularity, so
// the longest block is measured as the largest gap between setImmediate
// ticks (each tick is one event-loop turn).
const histogram = monitorEventLoopDelay({ resolution: 1 })
histogram.enable()
let probing = true
let last = performance.now()
let maxGap = 0
const tick = () => {
const now = performance.now()
maxGap = Math.max(maxGap, now - last)
last = now
if (probing) setImmediate(tick)
}
setImmediate(tick)
const samples = []
for (let index = 0; index < calls; index += 1) {
const started = performance.now()
await body(index)
samples.push(performance.now() - started)
// Yield like the IPC loop does between messages.
await new Promise(resolve => setImmediate(resolve))
}
await new Promise(resolve => setTimeout(resolve, 20))
probing = false
histogram.disable()
return {
callP50: round(percentile(samples, 50)), callP95: round(percentile(samples, 95)),
loopP99: round(histogram.percentile(99) / 1e6), loopMax: round(maxGap)
}
}
/** The pre-PERF-15 replace implementation, kept as the "before" reference. */
function legacyReplace(raw, cache, input) {
const snapshot = activityHistorySnapshotSchema.parse(input)
raw.exec('BEGIN IMMEDIATE')
try {
const { data_version: dataVersion } = raw.prepare('PRAGMA data_version').get()
let previous = cache.value?.dataVersion === dataVersion ? cache.value.records : undefined
if (!previous) {
previous = new Map(raw.prepare('SELECT record_key, record_json FROM activity_history_records').all()
.map(row => [row.record_key, row.record_json]))
}
const records = new Map()
const occurrences = new Map()
const order = []
// order_seq only satisfies the current schema; the old code kept order in the JSON list below.
const upsert = raw.prepare(`INSERT INTO activity_history_records (record_key, record_json, order_seq) VALUES (?, ?, ?)
ON CONFLICT(record_key) DO UPDATE SET record_json = excluded.record_json`)
for (const record of snapshot.records) {
const occurrence = occurrences.get(record.id) ?? 0
occurrences.set(record.id, occurrence + 1)
const key = JSON.stringify([record.id, occurrence])
const json = JSON.stringify(record)
order.push(key)
if (previous.get(key) !== json) upsert.run(key, json, snapshot.records.length - order.length + 1)
records.set(key, json)
}
const remove = raw.prepare('DELETE FROM activity_history_records WHERE record_key = ?')
for (const key of previous.keys()) if (!records.has(key)) remove.run(key)
const orderJson = JSON.stringify(order)
const incomplete = Number(snapshot.legacyHistoryMayBeIncomplete)
raw.prepare(`UPDATE activity_history SET record_order_json = ?, legacy_history_may_be_incomplete = ?
WHERE singleton = 1 AND (record_order_json != ? OR legacy_history_may_be_incomplete != ?)`)
.run(orderJson, incomplete, orderJson, incomplete)
raw.exec('COMMIT')
cache.value = { records, dataVersion }
} catch (error) {
raw.exec('ROLLBACK')
throw error
}
}
async function main() {
console.log(`[mperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const workspace = join(directory, 'workspace')
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
// Message counts follow a skewed distribution; the first conversation is the largest.
const weights = Array.from({ length: conversationCount }, (_, index) => 1 / (1 + index * 0.01))
const weightSum = weights.reduce((sum, value) => sum + value, 0)
const counts = weights.map(weight => Math.max(1, Math.round(messageCount * weight / weightSum)))
const ids = counts.map(() => randomUUID())
for (let start = 0; start < conversationCount; start += 50) {
seeding.saveLocalConversations(ids.slice(start, start + 50).map((id, offset) => {
const index = start + offset
return {
header: { id, projectId, title: `Conversation ${index} ${sentence(3)}`, updatedAt: 1_700_000_000_000 + index },
messages: Array.from({ length: counts[index] }, (_, at) => ({
id: randomUUID(), role: at % 2 === 0 ? 'user' : 'assistant', state: 'complete',
content: sentence(at % 2 === 0 ? 30 : 120), createdAt: 1_700_000_000_000 + index * 1000 + at
}))
}
}))
}
const records = Array.from({ length: activityCount }, (_, index) => ({
id: randomUUID(), conversationId: ids[index % conversationCount], requestId: randomUUID(),
...(index % 2 === 0 ? { callId: `call-${index}` } : {}),
scope: index % 3 === 0 ? { kind: 'project', projectId, projectName: 'Perf' } : { kind: 'global' },
kind: index % 5 === 0 ? 'request' : 'tool', title: `Tool ${index}`, detail: sentence(40),
status: 'completed', createdAt: 1_700_000_000_000 + index
}))
seeding.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: false })
const seedSeconds = round((performance.now() - seedStarted) / 1000)
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)
const largest = ids[0]
const typical = ids[Math.floor(conversationCount / 2)]
// The renderer retains the current conversation's history.
const detailIds = [ids[1]]
const results = { conversations: conversationCount, messages: totalMessages, activityRecords: activityCount,
largestConversationMessages: counts[0], seedSeconds }
results.listSummariesSync = await measure(async () => database.listConversationSummaries(detailIds))
results.getLargestSync = await measure(async () => database.getConversation(largest))
results.getTypicalSync = await measure(async () => database.getConversation(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 })
const oneChanged = index => {
const snapshot = unchanged()
const record = snapshot.records[(index + 2) % 50]
record.detail = `${record.detail} ${index}`
return snapshot
}
// Inputs are cloned up front: cloning happens in the renderer / IPC layer.
const run = async (makeInput, call) => {
const inputs = Array.from({ length: calls + 1 }, (_, index) => makeInput(index - 1))
return measure(async index => call(inputs[index + 1]))
}
const legacyCache = {}
results.replaceUnchangedLegacy = await run(unchanged, input => legacyReplace(raw, legacyCache, input))
results.replaceOneChangedLegacy = await run(oneChanged, input => legacyReplace(raw, legacyCache, input))
// Restore a consistent state for the current implementation's cache.
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: false })
results.replaceUnchangedAfter = await run(unchanged, input => database.replaceActivityHistory(input))
results.replaceOneChangedAfter = await run(oneChanged, input => database.replaceActivityHistory(input))
results.updateOneRecord = await run(index => {
const record = { ...records[(index + 2) % 50], detail: `incremental ${index}`, status: 'running' }
return { changes: [{ type: 'upsert', position: 'front', record }] }
}, input => database.updateActivityHistory(input))
// Startup: the renderer used to load the whole list; now the first page and the summary.
const firstPageRequest = { limit: 200 }
const summaryRequest = () => ({ conversationIds: [...new Set(database.getActivityHistoryPage(firstPageRequest).records.map(record => record.conversationId))] })
results.activityFullLoad = await measure(async () => database.getActivityHistory())
results.activityFirstPageSync = await measure(async () => database.getActivityHistoryPage(firstPageRequest))
results.activitySummarySync = await measure(async () => database.getActivityHistorySummary(summaryRequest()))
results.activityStartupWorker = await measure(async () => {
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`)
const rows = [
['conversations:list-summaries', results.listSummariesSync, results.listSummariesWorker],
[`conversations:get (${counts[0]} msgs)`, results.getLargestSync, results.getLargestWorker],
[`conversations:get (${counts[Math.floor(conversationCount / 2)]} msgs)`, results.getTypicalSync, results.getTypicalWorker],
['activity replace, unchanged', results.replaceUnchangedLegacy, results.replaceUnchangedAfter],
['activity replace, 1 changed', results.replaceOneChangedLegacy, results.replaceOneChangedAfter],
['activity update, 1 record', results.replaceOneChangedLegacy, results.updateOneRecord],
['activity startup: full list vs page+summary (worker)', results.activityFullLoad, results.activityStartupWorker],
['activity first page (200), sync', results.activityFullLoad, results.activityFirstPageSync],
['activity summary, sync', results.activityFullLoad, results.activitySummarySync]
]
console.table(rows.map(([name, before, after]) => ({
path: name,
'before call p50/p95 ms': `${before.callP50} / ${before.callP95}`,
'before loop max ms': before.loopMax,
'after call p50/p95 ms': `${after.callP50} / ${after.callP95}`,
'after loop max ms': after.loopMax
})))
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', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
}
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 })
})
+191
View File
@@ -0,0 +1,191 @@
const { dirname, join, resolve } = require('node:path')
const { copyFileSync, mkdtempSync, mkdirSync, rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { pathToFileURL } = require('node:url')
const assert = require('node:assert/strict')
if (!process.versions.electron || process.env.ELECTRON_RUN_AS_NODE === '1') {
const { spawnSync } = require('node:child_process')
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-obsidian-ui-'))
const env = { ...process.env, GOODBUDDY_OBSIDIAN_ACCEPTANCE_ROOT: root }
if (process.argv.includes('--live') || process.argv.includes('--chat')) {
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE = process.argv.includes('--live') ? '1' : '0'
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT = process.argv.includes('--chat') ? '1' : '0'
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CONTINUE_ONLY = process.argv.includes('--continue-only') ? '1' : '0'
require('esbuild').buildSync({ entryPoints: ['tests/obsidian-runtime-acceptance.ts'],
outfile: join(root, 'runtime.cjs'), bundle: true, external: ['electron', 'ssh2', 'node-pty', 'koffi'], platform: 'node', format: 'cjs',
banner: { js: `require = require('node:module').createRequire(${JSON.stringify(resolve('package.json'))});` } })
}
delete env.ELECTRON_RUN_AS_NODE
try {
const result = spawnSync(require('electron'), [__filename], {
env, stdio: 'inherit', timeout: 300_000
})
if (result.error) throw result.error
process.exitCode = result.status ?? 1
} finally {
rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 })
}
} else {
const { app, BrowserWindow } = require('electron')
const root = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_ROOT
assert(root)
const profile = join(root, 'profile')
mkdirSync(profile)
if (process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE === '1' || process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT === '1') {
const settings = process.env.GOODBUDDY_ACCEPTANCE_SETTINGS ?? join(app.getPath('appData'), 'goodbuddy', 'runtime-settings.json')
copyFileSync(join(dirname(settings), 'Local State'), join(profile, 'Local State'))
}
mkdirSync(join(root, 'Temporary Vault'))
app.setPath('userData', profile)
app.setAppPath(resolve('.'))
const timer = setTimeout(() => { console.error('Acceptance timed out'); app.exit(1) }, 280_000)
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
const chat = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT === '1'
? await require(join(root, 'runtime.cjs')).prepareChat(root, profile) : undefined
await import(pathToFileURL(resolve('out/main/index.js')).href)
let window
for (let attempt = 0; attempt < 120; attempt++) {
window = BrowserWindow.getAllWindows().find(item => item.webContents.getURL().includes('index.html'))
if (window && await window.webContents.executeJavaScript('Boolean(document.querySelector("button"))')) break
await wait(250)
}
assert(window, 'Production main must open its BrowserWindow')
assert.equal(app.getPath('userData'), profile)
const preferences = window.webContents.getLastWebPreferences()
assert.equal(preferences.contextIsolation, true)
assert.equal(preferences.nodeIntegration, false)
const js = code => window.webContents.executeJavaScript(code).catch(error => { throw new Error(`${code}: ${error.message}`) })
const click = async expression => {
window.focus()
for (let i=0;i<100;i++) { if (await js(`Boolean(${expression})`)) break; await wait(100) }
const point = await js(`(() => { const element = ${expression}; if (!element) throw new Error('Missing control'); element.scrollIntoView({block:'center'}); const r=element.getBoundingClientRect(); return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)} })()`)
window.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...point })
window.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...point })
await wait(500)
}
if (chat) {
await js(`(async()=>{const p=(await window.goodbuddy.projects.list())[0];await window.goodbuddy.projects.update(p.id,{name:p.name,description:p.description,rootPath:${JSON.stringify(join(root,'chat-workspace'))},runtimeSelection:{provider:'opencode'}})})()`)
window.webContents.reload()
await wait(1000)
}
await click(`document.querySelector('button[aria-label="\u8bbe\u7f6e"]')`)
await click(`Array.from(document.querySelectorAll('button')).find(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('[role="tab"]')).find(b=>b.innerText==='MCP' && b.getClientRects().length)`)
const card = `Array.from(document.querySelectorAll('.capability-card')).find(c=>c.querySelector('strong')?.textContent==='Obsidian')`
const poll = async (expression, attempts=100) => {
for (let i=0;i<attempts;i++) { if (await js(expression)) return; await wait(200) }
console.error('VISIBLE_TABS',await js(`JSON.stringify(Array.from(document.querySelectorAll('[role="tab"]')).map(e=>({text:e.innerText,selected:e.getAttribute('aria-selected')})))`))
throw new Error(`Timed out: ${expression}`)
}
await poll(`Boolean(${card})`)
assert.equal(await js(`(${card}).querySelector('[role="switch"]').checked`), false)
await click(`(${card}).querySelector('select')`)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'End' })
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Return' })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Return' })
await poll(`Boolean((${card}).querySelector('input:not([type="checkbox"])'))`)
const input = `(${card}).querySelector('input:not([type="checkbox"])')`
const vault = join(root, 'Temporary Vault')
await click(input)
await window.webContents.insertText(vault)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
assert.match(await js(`(${card}).querySelector('[role="status"]').textContent`), /18/)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, '')
assert.equal(await js(`(${card}).querySelector('[role="switch"]').checked`), false)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[2]`)
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, vault)
await click(input)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] })
await window.webContents.insertText(join(root, 'missing-vault'))
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="alert"]'))`)
assert.equal(await js(`(${input}).value`), join(root, 'missing-vault'))
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, vault)
console.log('PASS: native UI draft test (18 tools), no implicit save/enable, save through IPC, missing-folder error preserves draft')
if (process.platform === 'win32') {
await click(`(${card}).querySelectorAll('.capability-card__actions button')[0]`)
const user32 = require('koffi').load('user32.dll')
const getWindow = user32.func('uintptr_t __stdcall GetWindow(uintptr_t hwnd, uint32_t command)')
const getPid = user32.func('uint32_t __stdcall GetWindowThreadProcessId(uintptr_t hwnd, _Out_ uint32_t *pid)')
const postMessage = user32.func('bool __stdcall PostMessageW(uintptr_t hwnd, uint32_t message, uintptr_t wparam, intptr_t lparam)')
const handle = Number(window.getNativeWindowHandle().readBigUInt64LE())
const popup = getWindow(handle, 6)
assert(popup && Number(popup) !== handle, 'Native folder dialog must open')
const pid = [0]
getPid(popup, pid)
assert.equal(pid[0], process.pid, 'Only close this acceptance process dialog')
assert(postMessage(popup, 0x10, 0, 0))
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal(await js(`(${input}).value`), join(root, 'missing-vault'))
console.log('PASS: real Windows folder dialog opened and cancelled without changing draft')
}
window.setMinimumSize(640, 480)
window.setSize(760, 720)
await wait(500)
const geometry = await js(`(() => {const c=${card};const r=c.getBoundingClientRect();return {width:innerWidth,left:r.left,right:r.right,scroll:c.scrollWidth,client:c.clientWidth}})()`)
assert(geometry.scroll <= geometry.client + 1 && geometry.right <= geometry.width && geometry.left >= 0, JSON.stringify(geometry))
console.log('PASS: narrow card has no horizontal overflow', JSON.stringify(geometry))
await click(input)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] })
await window.webContents.insertText(vault)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
await js(`(${card}).querySelector('[role="switch"]').focus()`)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Space' })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Space' })
await poll(`(${card}).querySelector('[role="switch"]').checked && !(${card}).querySelector('[role="switch"]').disabled`)
await click(`(${card}).querySelector('button[aria-expanded]')`)
await poll(`(${card}).querySelectorAll('.mcp-server-tools li').length === 19`)
console.log('PASS: keyboard switch enables Obsidian; expanded production UI lists 19 tools')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme=${JSON.stringify(theme)}`)
const bounds = await js(`(()=>{const c=${card};const r=c.getBoundingClientRect();return {right:r.right,width:innerWidth,scroll:c.scrollWidth,client:c.clientWidth,background:getComputedStyle(c).backgroundColor}})()`)
assert(bounds.right <= bounds.width && bounds.scroll <= bounds.client + 1)
console.log(`PASS: ${theme} narrow Obsidian bounds`, JSON.stringify(bounds))
}
if (process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE === '1') {
await require(join(root, 'runtime.cjs')).run(root, await js('window.goodbuddy.capabilities.getSnapshot()'))
}
if (chat) {
await click(`document.querySelector('.settings-panel__header button')`)
window.setSize(1280, 850)
try {
const providers = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CONTINUE_ONLY === '1' ? ['Continue'] : ['OpenCode', 'Continue']
for (const provider of providers) {
if (provider === 'Continue') {
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`)
await js(`document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').click()`)
await click(`Array.from(document.querySelectorAll('.runtime-picker__menu strong')).find(b=>b.innerText==='Continue Runtime')?.nextElementSibling`)
}
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`)
const mode = `document.querySelector('button[aria-label^="\u5de5\u4f5c\u6a21\u5f0f"]')`
if (!(await js(`(${mode}).innerText.includes('Execute')`))) {
await click(mode)
await click(`Array.from(document.querySelectorAll('[role="menuitemradio"]')).find(b=>b.innerText.startsWith('Execute'))`)
}
const marker = `CHAT_${provider}_${require('node:crypto').randomUUID()}`
const before = chat.observations.length
await click(`document.querySelector('textarea')`)
await window.webContents.insertText(`Use only Obsidian tools. Write ${provider}-chat.md containing ${marker} using obsidian_write_note, then read it using obsidian_read_note. Reply with the content read. Do not use other tools.`)
await click(`document.querySelector('button[aria-label="\u53d1\u9001"]')`)
await poll(`Array.from(document.querySelectorAll('.message--assistant')).some(e=>e.innerText.includes(${JSON.stringify(marker)}))`,450)
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`,450)
assert((require('node:fs').readFileSync(join(vault,`${provider}-chat.md`),'utf8')).includes(marker))
console.log(JSON.stringify({ provider, fullChatUi: 'passed', realModelHttpRequests: chat.observations.length-before }))
}
} finally {
console.log(JSON.stringify({ chatModelHttpRequests: chat.observations.length }))
await chat.close()
}
}
await wait(1000)
clearTimeout(timer)
app.exit(0)
})().catch(error => { console.error(error); clearTimeout(timer); app.exit(1) })
}
+125
View File
@@ -0,0 +1,125 @@
const assert = require('node:assert/strict')
const { spawn } = require('node:child_process')
const { mkdtemp, mkdir, readFile, readdir, writeFile, rm, stat } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
async function main() {
const executable = resolve(process.argv[2])
await stat(executable)
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-obsidian-packaged-'))
const profile = join(root, 'profile')
const vault = join(root, 'Packaged Vault')
await mkdir(profile)
await mkdir(vault)
const audit = join(root, 'node-audit.jsonl')
const guard = join(root, 'offline.cjs')
await writeFile(guard, `
const fs = require('node:fs');
const net = require('node:net');
const original = net.Socket.prototype.connect;
net.Socket.prototype.connect = function(...args) {
const options = net._normalizeArgs(args)[0];
const host = options.host || 'localhost';
if (!options.path && !['localhost','127.0.0.1','::1'].includes(host)) throw new Error('Offline acceptance blocked network');
return original.apply(this,args);
};
globalThis.fetch = async () => { throw new Error('Offline acceptance blocked fetch'); };
require('node:module').syncBuiltinESMExports();
fs.appendFileSync(${JSON.stringify(audit)}, JSON.stringify({entry:process.argv[1],exe:process.execPath,electron:process.versions.electron})+'\\n');
`)
const env = { ...process.env, NODE_OPTIONS: `--require ${JSON.stringify(guard)}`,
PATH: join(process.env.SystemRoot, 'System32') }
delete env.Path
delete env.ELECTRON_RUN_AS_NODE
const child = spawn(executable, [`--user-data-dir=${profile}`, '--remote-debugging-port=0', '--inspect=0',
'--proxy-server=http://127.0.0.1:9', '--proxy-bypass-list=<-loopback>'], { env, stdio: ['ignore', 'ignore', 'pipe'] })
let diagnostic = ''
child.stderr.on('data', chunk => { diagnostic += chunk.toString() })
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
let socket
let mainSocket
try {
let port
for (let i=0;i<100;i++) {
try { port = (await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0]; break } catch { await wait(200) }
}
assert(port, 'Packaged app must expose debugging only in the isolated profile')
const mainEndpoint = diagnostic.match(/ws:\/\/127\.0\.0\.1:\d+\/[-a-f0-9]+/u)?.[0]
assert(mainEndpoint, 'The isolated Main inspector must be available')
mainSocket = new WebSocket(mainEndpoint)
await new Promise((resolve, reject) => { mainSocket.onopen=resolve;mainSocket.onerror=reject })
// Packaged Electron strips NODE_OPTIONS at startup. Restore it only for this
// disposable Main so its real MCPVault child receives the offline guard.
await new Promise((resolve,reject) => {
const timer=setTimeout(()=>reject(new Error('Main inspector timed out')),10_000)
mainSocket.onmessage=message=>{const value=JSON.parse(message.data);if(value.id===1){clearTimeout(timer);if(value.error || value.result?.exceptionDetails)reject(new Error('Unable to set child network guard'));else resolve()}}
mainSocket.send(JSON.stringify({id:1,method:'Runtime.evaluate',params:{expression:`process.env.NODE_OPTIONS=${JSON.stringify(env.NODE_OPTIONS)}`}}))
})
mainSocket.close()
let target
for (let i=0;i<100;i++) {
target = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find(item => item.type === 'page' && item.url.includes('index.html'))
if (target) break
await wait(200)
}
assert(target, 'Packaged production renderer must load')
socket = new WebSocket(target.webSocketDebuggerUrl)
await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject })
let id = 0
const pending = new Map()
socket.onmessage = message => { const value = JSON.parse(message.data); if (pending.has(value.id)) { const {resolve,reject}=pending.get(value.id); pending.delete(value.id); if (value.error) reject(new Error(value.error.message)); else resolve(value.result) } }
const send = (method, params={}) => new Promise((resolve,reject) => {
const key=++id
const timer=setTimeout(()=>{pending.delete(key);reject(new Error(`CDP timed out: ${method}`))},10_000)
pending.set(key,{
resolve: value=>{clearTimeout(timer);resolve(value)},
reject: error=>{clearTimeout(timer);reject(error)}
})
socket.send(JSON.stringify({id:key,method,params}))
})
const js = async expression => { const result=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});assert(!result.exceptionDetails,JSON.stringify(result.exceptionDetails));return result.result.value }
const poll = async expression => { for(let i=0;i<100;i++){ if(await js(expression))return;await wait(200) } throw new Error(`Timeout: ${expression}`) }
const click = async expression => {
await js(`(()=>{const e=${expression};if(!e)throw new Error('Missing control');e.scrollIntoView({block:'center'});e.focus();e.click()})()`)
await wait(500)
}
await poll('Boolean(window.goodbuddy && document.querySelector("button"))')
await stat(join(profile,'assistant.sqlite'))
assert.equal(await js('typeof require'), 'undefined')
await click(`document.querySelector('button[aria-label="\u8bbe\u7f6e"]')`)
await poll(`Array.from(document.querySelectorAll('button')).some(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('button')).find(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('[role="tab"]')).find(b=>b.innerText==='MCP' && b.getClientRects().length)`)
const card=`Array.from(document.querySelectorAll('.capability-card')).find(c=>c.querySelector('strong')?.textContent==='Obsidian')`
await poll(`Boolean(${card})`)
await js(`{const e=(${card}).querySelector('select');e.value='folder';e.dispatchEvent(new Event('change',{bubbles:true}));}`)
await poll(`Boolean((${card}).querySelector('input:not([type="checkbox"])'))`)
await click(`(${card}).querySelector('input:not([type="checkbox"])')`)
await send('Input.insertText',{text:vault})
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
assert.match(await js(`(${card}).querySelector('[role="status"]').textContent`),/18/)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[2]`)
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath,vault)
const bin=join(profile,'tool-environment','bin')
const shims=(await readdir(bin,{recursive:true})).filter(name=>name.endsWith('node.cmd'))
assert(shims.length>0)
assert((await readFile(join(bin,shims[0]),'utf8')).includes(executable))
console.log('PASS: packaged app.asar UI -> real IPC -> packaged MCPVault (18 tools), save, managed packaged Electron shim, PATH=System32, Chromium dead proxy')
const records=(await readFile(audit,'utf8')).trim().split('\n').map(line=>JSON.parse(line))
assert(records.some(item=>item.entry?.includes('app.asar.unpacked') && item.entry.endsWith('server.js') && item.exe===executable && item.electron))
console.log('PASS: packaged MCPVault child loaded Node network guards; audited packaged executable and unpacked server entry')
console.log('Boundary: MCPVault Node API guards + Chromium proxy; no OS firewall, physical disconnection, or installer execution')
} finally {
socket?.close()
mainSocket?.close()
if (child.exitCode === null) {
const killer=spawn('taskkill',['/PID',String(child.pid),'/T','/F'],{stdio:'ignore'})
await new Promise(resolve=>killer.once('exit',resolve))
}
await rm(root,{recursive:true,force:true,maxRetries:10,retryDelay:200})
}
}
main().catch(error=>{console.error(error);process.exitCode=1})
+17 -2
View File
@@ -13,6 +13,7 @@ const {
const { statSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { delimiter, join, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const unpackedPath = process.argv[2]
? resolve(process.argv[2])
@@ -109,6 +110,20 @@ async function main() {
const npmProject = join(root, 'npm-project')
const npmFixture = join(root, 'npm-fixture')
await mkdir(project, { recursive: true })
const pluginUrl = pathToFileURL(join(
unpackedPath, 'resources', 'runtimes', 'opencode-config',
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
)).href
const importedPlugin = await run(
executable,
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
root
)
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
}
console.log('Packaged OpenCode offline plugin import: ready')
await copyFile(
resolve('build/deepseek-harness-utility-smoke.cjs'),
@@ -133,7 +148,7 @@ async function main() {
[
'appId: live.digiman.goodbuddy.harness-smoke',
'productName: GoodBuddyHarnessSmoke',
'electronVersion: "43.2.0"',
`electronVersion: "${require('electron/package.json').version}"`,
'asar: true',
'npmRebuild: false',
'files:',
@@ -147,7 +162,7 @@ async function main() {
)
const packageArguments = [
resolve('node_modules/electron-builder/cli.js'),
resolve('build/run-electron-builder.cjs'),
'--projectDir',
project,
'--win',
+381
View File
@@ -0,0 +1,381 @@
// Cold-start benchmark supervisor (PERF-01).
// Launches the current production build (out/, from `npm run build:bundle`)
// GB_PERF_STARTUP_RUNS times (default 5) per variant through
// tests/support/startup-perf-driver.mjs, each launch in a fresh temporary
// user-data directory, and reports median / p95 per startup phase.
//
// Variants (interleaved each round, per docs P7):
// first-run empty user-data dir (creates databases, shows release notes)
// empty copy of a prepared profile: release notes seen, startup update
// check off, 0 conversations
// seeded-300 same as empty plus GB_PERF_STARTUP_SEED (default 300)
// synthetic conversations, reusing the PERF-11 seed shape
//
// Isolation: temporary profile/session/logs, a loopback fake model on an
// ephemeral port (GB_PERF_MODEL_PORT to pin one; 11434 is left alone), all
// non-loopback requests blocked in the driver. Output has phase names, times
// and counts only.
//
// Env: GB_PERF_STARTUP_RUNS, GB_PERF_STARTUP_SEED, GB_PERF_STARTUP_VARIANTS
// (comma list), GB_PERF_STARTUP_SKIP_BUILD=1, GB_PERF_STARTUP_PROFILE=1 (one
// extra CPU-profiled launch per variant, excluded from the table),
// GB_PERF_OUTPUT (report directory), GB_PERF_KEEP_PROFILE=1,
// GB_PERF_STARTUP_APP_ROOT (checkout/worktree to build and launch; default .).
// Variant "real" (opt-in) launches on a fresh copy of GB_PERF_STARTUP_REAL_DATA.
const { spawn, spawnSync, execFileSync } = require('node:child_process')
const { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } = require('node:fs')
const { createServer } = require('node:http')
const { cpus, tmpdir, totalmem } = require('node:os')
const { join, resolve } = require('node:path')
const { npmInvocation } = require('./npm-invocation.cjs')
const root = resolve(__dirname, '..')
// The app under test. Defaults to this checkout; point it at a temporary
// worktree to measure a baseline or a clean HEAD while this tree is mid-edit.
const appRoot = resolve(process.env.GB_PERF_STARTUP_APP_ROOT || root)
const runs = Math.max(1, Number(process.env.GB_PERF_STARTUP_RUNS || 5))
const seedCount = Number(process.env.GB_PERF_STARTUP_SEED || 300)
const seededName = `seeded-${seedCount}`
const variants = (process.env.GB_PERF_STARTUP_VARIANTS || `first-run,empty,${seededName}`).split(',').map(v => v.trim()).filter(Boolean)
const requestedPort = Number(process.env.GB_PERF_MODEL_PORT || 0)
const launchTimeoutMs = Number(process.env.GB_PERF_STARTUP_LAUNCH_TIMEOUT_MS || 150_000)
const keepProfile = process.env.GB_PERF_KEEP_PROFILE === '1'
const cpuProfile = process.env.GB_PERF_STARTUP_PROFILE === '1'
// ------------------------------------------------------------------ build
function newestMtime(path) {
let newest = 0
const stack = [path]
while (stack.length) {
const current = stack.pop()
let info
try { info = statSync(current) } catch { continue }
if (info.isDirectory()) {
for (const entry of readdirSync(current)) if (entry !== 'node_modules') stack.push(join(current, entry))
} else if (!/\.test\.tsx?$/.test(current)) {
newest = Math.max(newest, info.mtimeMs)
}
}
return newest
}
function ensureBuild() {
const outputs = ['out/main/index.js', 'out/preload/index.cjs', 'out/renderer/index.html'].map(file => join(appRoot, file))
const missing = outputs.some(file => !existsSync(file))
const builtAt = missing ? 0 : Math.min(...outputs.map(file => statSync(file).mtimeMs))
const sourceAt = Math.max(...['src/main', 'src/preload', 'src/renderer', 'src/shared', 'electron.vite.config.ts', 'package.json'].map(p => newestMtime(join(appRoot, p))))
const stale = missing || sourceAt > builtAt
if (!stale) return { rebuilt: false }
if (process.env.GB_PERF_STARTUP_SKIP_BUILD === '1') {
if (missing) throw new Error('Missing production build and GB_PERF_STARTUP_SKIP_BUILD=1')
console.log('[startup] build is older than src/, measuring it anyway (GB_PERF_STARTUP_SKIP_BUILD=1)')
return { rebuilt: false, stale: true }
}
console.log(`[startup] ${missing ? 'no production build' : 'production build is older than src/'}; running npm run build:bundle`)
const npm = npmInvocation(appRoot)
const result = spawnSync(npm.command, [...npm.prefixArgs, 'run', 'build:bundle'], { cwd: appRoot, stdio: 'inherit', shell: npm.prefixArgs.length === 0 && process.platform === 'win32' })
if (result.status !== 0) throw new Error(`npm run build:bundle failed (${result.status})`)
return { rebuilt: true }
}
// -------------------------------------------------------------- fake model
function startModelServer(ledger) {
const server = createServer((request, response) => {
ledger.push({ method: request.method, path: request.url })
if (request.method === 'GET' && request.url?.endsWith('/models')) {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ object: 'list', data: [{ id: 'qwen3', object: 'model' }] }))
return
}
if (request.method === 'POST' && request.url?.endsWith('/chat/completions')) {
request.resume()
request.on('end', () => {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ id: 'perf', object: 'chat.completion', model: 'qwen3', choices: [{ index: 0, finish_reason: 'stop', message: { role: 'assistant', content: 'ok' } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }))
})
return
}
response.writeHead(404).end()
})
return new Promise((resolvePromise, reject) => {
server.once('error', reject)
server.listen(requestedPort, '127.0.0.1', () => resolvePromise(server))
})
}
// ------------------------------------------------------------------ launch
function baseEnvironment(modelPort, workspace) {
const environment = { ...process.env, GB_PERF_ROOT: appRoot, GB_PERF_STARTUP_MARKS: '1' }
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]
environment.GOODBUDDY_MODEL_API_KEY = 'perf-loopback'
environment.GOODBUDDY_MODEL_BASE_URL = `http://127.0.0.1:${modelPort}/v1`
environment.GOODBUDDY_MODEL_NAME = 'qwen3'
environment.GOODBUDDY_WORKSPACE = workspace
return environment
}
async function launch({ directory, environment, mode = 'measure', seed = 0, expectConversations = 0, profilePath }) {
const resultPath = join(directory, 'startup-result.json')
const env = { ...environment, GB_STARTUP_DIR: directory, GB_STARTUP_RESULT: resultPath, GB_STARTUP_MODE: mode, GB_STARTUP_SEED: String(seed), GB_STARTUP_EXPECT_CONVERSATIONS: String(expectConversations) }
if (profilePath) env.GB_STARTUP_CPU_PROFILE = profilePath
const stderr = []
const spawnAt = Date.now()
const child = spawn(require('electron'), [join(root, 'tests', 'support', 'startup-perf-driver.mjs')], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: false })
child.stderr.on('data', chunk => { if (stderr.length < 200) stderr.push(String(chunk)) })
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
else child.kill('SIGKILL')
}, launchTimeoutMs)
const exit = await new Promise(resolvePromise => child.once('exit', code => resolvePromise(code)))
const exitAt = Date.now()
clearTimeout(timer)
const report = existsSync(resultPath) ? JSON.parse(readFileSync(resultPath, 'utf8')) : { status: 'failed', error: timedOut ? 'launch timeout' : 'driver exited without a report' }
if (exit !== 0 || timedOut) report.status = 'failed'
report.spawnAt = spawnAt
report.exitMs = exitAt - spawnAt
// stderr is kept only for failures, and only its first line (no user data exists in these profiles).
if (report.status !== 'passed') report.stderrHead = stderr.join('').split(/\r?\n/).filter(Boolean).slice(0, 8)
return report
}
// --------------------------------------------------------------- analysis
function phases(report) {
const t = report.times
const r = report.renderer
const s = report.spawnAt
const marks = Object.fromEntries((report.startupMarks ?? []).map(m => [m.name, m.at]))
const d = (from, to) => (from === undefined || from === null || to === undefined || to === null) ? null : to - from
const result = {
'spawn -> driver start (Electron boot)': d(s, t.driverStart),
'driver start -> Main import done': d(t.driverStart, t.mainImportEnd),
'spawn -> app ready': d(s, t.appReady),
'app ready -> BrowserWindow created': d(t.appReady, t.windowCreated),
'window created -> loadMainWindow': d(t.windowCreated, marks['main:load-main-window']),
'loadMainWindow -> ready-to-show': d(marks['main:load-main-window'], t.readyToShow),
'window created -> ready-to-show': d(t.windowCreated, t.readyToShow),
'ready-to-show -> first visible frame': d(t.readyToShow, r.firstVisibleFrame),
'ready-to-show -> composer interactive': d(t.readyToShow, r.composerInteractive),
'spawn -> ready-to-show': d(s, t.readyToShow),
'spawn -> first visible frame': d(s, r.firstVisibleFrame),
// Only reported by Chromium when the first paint happens while visible.
'spawn -> first contentful paint': d(s, r.firstContentfulPaint),
'spawn -> composer present': d(s, r.composerPresent),
'spawn -> composer interactive': d(s, r.composerInteractive),
'spawn -> sidebar populated (seeded)': d(s, r.sidebarPopulated),
'composer interactive -> sidebar populated': d(r.composerInteractive, r.sidebarPopulated)
}
// Main phases (GB_PERF_STARTUP_MARKS spans; parallel spans overlap).
const main = {
'main: module import (index.js eval)': d(t.mainImportStart, marks['main:module-evaluated']),
'main: whenReady -> window created': d(marks['main:when-ready'], marks['main:create-window:end']),
'main: createMainWindow()': d(marks['main:create-window:start'], marks['main:create-window:end']),
'main: tray': d(marks['main:create-window:end'], marks['main:tray-built'])
}
for (const m of report.startupMarks ?? []) {
if (!m.name.endsWith(':start')) continue
const base = m.name.slice(0, -':start'.length)
if (base === 'main:create-window') continue
main[`${base.replace(/^main:/, 'main: ')}`] = d(m.at, marks[`${base}:end`])
// Synchronous part of the span: from the call until the first await yields.
if (marks[`${base}:sync-end`] !== undefined) main[`${base.replace(/^main:/, 'main: ')} (sync part)`] = d(m.at, marks[`${base}:sync-end`])
}
main['main: whenReady -> loadMainWindow (total)'] = d(marks['main:when-ready'], marks['main:load-main-window'])
// Critical path in Main: consecutive top-level marks (spans that run in
// parallel inside the prerequisites are excluded), so each gap is the time
// Main spent between two points of the sequential startup function.
const nested = /^main:(knowledge-init|knowledge-gateway-start|runtime-hydrate|assistant-db-init)/
const sequence = (report.startupMarks ?? []).filter(m => !nested.test(m.name) && !m.name.endsWith(':sync-end')).sort((a, b) => a.at - b.at)
for (let index = 1; index < sequence.length; index += 1) {
main[`gap: ${sequence[index - 1].name} -> ${sequence[index].name}`] = sequence[index].at - sequence[index - 1].at
}
const renderer = {
'renderer: navigation start -> DOMContentLoaded': d(r.navigationStart, r.domContentLoaded),
'renderer: DOMContentLoaded -> composer present': d(r.domContentLoaded, r.composerPresent),
'renderer: navigation start -> composer present': d(r.navigationStart, r.composerPresent)
}
return { ...result, ...main, ...renderer }
}
function percentile(values, p) {
if (!values.length) return null
const sorted = [...values].sort((a, b) => a - b)
// Nearest rank; with 5 samples p95 is the maximum.
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
function summarize(reportsByVariant) {
const rows = new Map()
for (const [variant, reports] of Object.entries(reportsByVariant)) {
const passed = reports.filter(r => r.status === 'passed')
const perRun = passed.map(phases)
for (const name of new Set(perRun.flatMap(Object.keys))) {
const values = perRun.map(p => p[name]).filter(v => typeof v === 'number' && Number.isFinite(v))
const row = rows.get(name) ?? {}
row[variant] = values.length ? { median: percentile(values, 50), p95: percentile(values, 95), n: values.length } : null
rows.set(name, row)
}
}
return rows
}
function sourceInfo() {
try {
const commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: appRoot, encoding: 'utf8' }).trim()
const dirtyFiles = execFileSync('git', ['status', '--porcelain'], { cwd: appRoot, encoding: 'utf8' }).split('\n').filter(Boolean).length
return { commit, dirty: dirtyFiles > 0, dirtyFiles, appRoot: appRoot === root ? '.' : 'GB_PERF_STARTUP_APP_ROOT' }
} catch {
return { commit: null }
}
}
const fmt = v => (v === null || v === undefined ? '-' : Math.round(v).toString())
// ------------------------------------------------------------------- main
async function main() {
const build = ensureBuild()
const runDirectory = mkdtempSync(join(tmpdir(), 'goodbuddy-startup-perf-'))
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
mkdirSync(outputDirectory, { recursive: true })
const workspace = join(runDirectory, 'workspace')
mkdirSync(workspace, { recursive: true })
const ledger = []
let server
try {
server = await startModelServer(ledger)
} catch (error) {
throw new Error(`Cannot listen on 127.0.0.1:${requestedPort} for the fake model (${error.code ?? error.message}); set GB_PERF_MODEL_PORT to a free port or leave it unset for an ephemeral port`, { cause: error })
}
const modelPort = server.address().port
const environment = baseEnvironment(modelPort, workspace)
const templates = {}
const prepareReports = {}
try {
// Templates are prepared once and copied per launch, so every measured
// launch starts from an identical, never-used-before directory.
for (const variant of variants) {
if (variant === 'first-run') continue
if (variant === 'real') {
// A copy of a real data directory (GB_PERF_STARTUP_REAL_DATA, for
// example the portable build's data folder). Only the stores startup
// reads are copied; the source is never opened by the app. The driver
// blocks every non-loopback request, so configured remote models are
// not called.
const source = process.env.GB_PERF_STARTUP_REAL_DATA
if (!source || !existsSync(join(source, 'assistant.sqlite'))) throw new Error('variant "real" needs GB_PERF_STARTUP_REAL_DATA with assistant.sqlite')
const directory = join(runDirectory, 'template-real')
const profile = join(directory, 'profile')
mkdirSync(profile, { recursive: true })
for (const entry of readdirSync(source)) {
const from = join(source, entry)
const keep = /\.(sqlite|sqlite-wal|sqlite-shm|json)$/i.test(entry) || entry === 'notes' || entry === 'Local Storage'
if (keep && entry !== 'DevToolsActivePort') cpSync(from, join(profile, entry), { recursive: true })
}
templates[variant] = directory
continue
}
const seed = variant === 'empty' ? 0 : variant === seededName ? seedCount : null
if (seed === null) throw new Error(`Unknown variant ${variant}`)
const directory = join(runDirectory, `template-${variant}`)
mkdirSync(directory, { recursive: true })
console.log(`[startup] preparing ${variant} profile${seed ? ` (${seed} conversations)` : ''}`)
const report = await launch({ directory, environment, mode: 'prepare', seed })
prepareReports[variant] = { status: report.status, error: report.error, seeded: report.seeded, exitMs: report.exitMs, stderrHead: report.stderrHead }
if (report.status !== 'passed') throw new Error(`Preparing ${variant} failed: ${report.error ?? 'unknown'}\n${(report.stderrHead ?? []).join('\n')}`)
rmSync(join(directory, 'startup-result.json'), { force: true })
templates[variant] = directory
}
const fresh = (variant, label) => {
const directory = join(runDirectory, `${label}-${variant}`)
if (templates[variant]) {
cpSync(templates[variant], directory, { recursive: true })
} else {
mkdirSync(directory, { recursive: true })
}
return directory
}
// The real profile's sidebar is ready once its first rows show.
const expectedFor = variant => (variant === seededName ? seedCount : variant === 'real' ? 10 : 0)
const results = Object.fromEntries(variants.map(v => [v, []]))
// A discarded warm-up launch per invocation so the first measured launch
// does not also pay for loading Electron binaries from a cold disk cache.
console.log('[startup] warm-up launch (discarded)')
await launch({ directory: fresh(variants.includes('empty') ? 'empty' : variants[0], 'warmup'), environment })
for (let round = 0; round < runs; round += 1) {
for (const variant of variants) {
const directory = fresh(variant, `run${round + 1}`)
const report = await launch({ directory, environment, expectConversations: expectedFor(variant) })
report.round = round + 1
results[variant].push(report)
const p = report.status === 'passed' ? phases(report) : {}
console.log(`[startup] round ${round + 1} ${variant}: ${report.status}, ready ${fmt(p['spawn -> app ready'])} ms, ready-to-show ${fmt(p['spawn -> ready-to-show'])} ms, first frame ${fmt(p['spawn -> first visible frame'])} ms, composer interactive ${fmt(p['spawn -> composer interactive'])} ms${report.error ? `, error ${report.error}` : ''}`)
if (!keepProfile) rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
const profiles = {}
if (cpuProfile) {
for (const variant of variants) {
const directory = fresh(variant, 'profiled')
const profilePath = join(outputDirectory, `main-startup-${variant}.cpuprofile`)
const report = await launch({ directory, environment, profilePath, expectConversations: expectedFor(variant) })
profiles[variant] = { status: report.status, cpuProfile: report.cpuProfile, phases: report.status === 'passed' ? phases(report) : null, file: profilePath }
console.log(`[startup] CPU-profiled ${variant}: ${report.status} -> ${profilePath}`)
}
}
const table = summarize(results)
const report = {
schemaVersion: 1,
kind: 'goodbuddy-startup-benchmark',
source: { ...sourceInfo(), build },
environment: { platform: process.platform, arch: process.arch, electron: require('electron/package.json').version, cpu: `${cpus()[0]?.model?.trim()} x${cpus().length}`, memoryGB: Math.round(totalmem() / 1073741824) },
config: { runs, variants, seedConversations: seedCount, percentile: 'nearest-rank' },
prepare: prepareReports,
phases: Object.fromEntries(table),
runs: Object.fromEntries(Object.entries(results).map(([variant, reports]) => [variant, reports.map(r => ({
round: r.round, status: r.status, error: r.error, stderrHead: r.stderrHead, exitMs: r.exitMs,
phases: r.status === 'passed' ? phases(r) : null,
renderer: r.renderer ? { domNodes: r.renderer.domNodes, conversationRows: r.renderer.conversationRows, scriptResources: r.renderer.scriptResources, scriptBytes: r.renderer.scriptBytes, jsHeapMB: r.renderer.jsHeapMB, startupDialog: r.renderer.startupDialog !== undefined, composerPresentBeforeProbe: r.renderer.alreadyAtInstall === true, longAnimationFrames: r.renderer.longAnimationFrames && { count: r.renderer.longAnimationFrames.count, totalMs: r.renderer.longAnimationFrames.totalMs, maxMs: r.renderer.longAnimationFrames.maxMs, scriptMs: r.renderer.longAnimationFrames.scriptMs } } : null,
mainEventLoopDelay: r.mainEventLoopDelay,
blockedRequests: r.blockedRequests && { fetch: r.blockedRequests.fetch.length, renderer: r.blockedRequests.renderer.length, hosts: [...new Set([...r.blockedRequests.fetch, ...r.blockedRequests.renderer])] }
}))])),
cpuProfiles: profiles,
modelRequests: { boundary: 'loopback fake model only; non-loopback requests blocked', total: ledger.length, paths: [...new Set(ledger.map(e => `${e.method} ${e.path}`))] }
}
const resultPath = join(outputDirectory, 'startup-result.json')
writeFileSync(resultPath, JSON.stringify(report, null, 2))
console.log(`\nGoodBuddy cold-start benchmark: ${runs} runs per variant, ms (median / p95, nearest rank)`)
console.log(`source ${report.source.commit ?? '?'}${report.source.dirty ? ` (dirty, ${report.source.dirtyFiles} files)` : ''}, Electron ${report.environment.electron}, ${report.environment.cpu}, ${report.environment.memoryGB} GB`)
console.table(Object.fromEntries([...table].map(([name, row]) => [name, Object.fromEntries(variants.map(v => [v, row[v] ? `${fmt(row[v].median)} / ${fmt(row[v].p95)}${row[v].n < runs ? ` (n=${row[v].n})` : ''}` : '-']))])))
const failed = Object.values(results).flat().filter(r => r.status !== 'passed').length
if (failed) console.log(`${failed} launch(es) failed; see report`)
const blocked = Object.values(results).flat().flatMap(r => [...(r.blockedRequests?.fetch ?? []), ...(r.blockedRequests?.renderer ?? [])])
console.log(`blocked non-loopback requests: ${blocked.length}${blocked.length ? ` (${[...new Set(blocked)].join(', ')})` : ''}; fake model requests: ${ledger.length}`)
console.log(`\nreport: ${resultPath}`)
process.exitCode = failed ? 1 : 0
} finally {
await new Promise(resolvePromise => server.close(() => resolvePromise()))
if (!keepProfile) {
for (const entry of readdirSync(runDirectory)) {
if (entry !== 'artifacts') rmSync(join(runDirectory, entry), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
}
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+78
View File
@@ -0,0 +1,78 @@
// Windows logical-I/O probe against an isolated copy of a quiescent data folder.
// GB_WPERF_DATA must contain assistant.sqlite. No source database is opened.
const { buildSync } = require('esbuild')
const { mkdtempSync, copyFileSync, cpSync, existsSync, rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { performance } = require('node:perf_hooks')
const { execFileSync, spawnSync } = require('node:child_process')
const { deepStrictEqual } = require('node:assert')
if (process.platform !== 'win32') throw new Error('This probe uses Windows process I/O counters')
if (!process.versions.electron) {
const result = spawnSync(require('electron'), [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(result.status ?? 1)
}
const io = () => JSON.parse(execFileSync('powershell.exe', ['-NoProfile', '-Command',
`Get-CimInstance Win32_Process -Filter 'ProcessId=${process.pid}' | Select-Object ReadTransferCount,WriteTransferCount | ConvertTo-Json -Compress`
], { encoding: 'utf8' }))
const source = process.env.GB_WPERF_DATA
if (!source || !existsSync(join(source, 'assistant.sqlite'))) throw new Error('Set GB_WPERF_DATA')
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-write-io-'))
let database
try {
const path = join(directory, 'assistant.sqlite')
for (const suffix of ['', '-wal', '-shm']) {
if (existsSync(join(source, `assistant.sqlite${suffix}`))) {
copyFileSync(join(source, `assistant.sqlite${suffix}`), `${path}${suffix}`)
}
}
if (existsSync(join(source, 'notes'))) cpSync(join(source, 'notes'), join(directory, 'notes'), { recursive: true })
const bundle = join(directory, 'database.cjs')
buildSync({ entryPoints: [resolve(__dirname, '../src/main/assistant/assistant-database.ts')],
outfile: bundle, bundle: true, platform: 'node', format: 'cjs', external: ['electron'], logLevel: 'error' })
const { AssistantDatabase } = require(bundle)
database = new AssistantDatabase(path)
database.initialize(directory)
const raw = database.database
const largest = raw.prepare(`SELECT id, conversation_id, length(metadata_json) AS bytes
FROM messages ORDER BY length(metadata_json) DESC LIMIT 1`).get()
if (!largest) throw new Error('No messages')
const snapshot = database.getConversation(largest.conversation_id)
const message = snapshot.messages.find(item => item.id === largest.id)
// Summary reads must remain small even while a large history is streaming.
database.saveLocalConversations([{ header: { ...snapshot, messages: undefined },
messages: [{ ...message, state: 'streaming' }] }])
const taskId = randomUUID()
database.createTask({ id: taskId, projectId: snapshot.projectId,
conversationId: snapshot.id, title: 'I/O probe', instructions: '', visible: false })
database.startExecutionTiming(taskId)
raw.exec('PRAGMA wal_checkpoint(TRUNCATE)')
console.log(JSON.stringify({ schema: raw.prepare('PRAGMA user_version').get(), metadataBytes: largest.bytes }))
const results = []
for (const [variant, query, count] of [
['read-conversation', () => database.getConversation(snapshot.id), 1],
['read-summary', () => database.listConversationSummaries([]), 1],
['conversation-timing', () => database.getExecutionStats({ conversationId: snapshot.id }), 20],
...(snapshot.projectId ? [['project-timing', () => database.getExecutionStats({ projectId: snapshot.projectId }), 20]] : [])
]) {
const before = io()
const start = performance.now()
for (let index = 0; index < count; index++) query()
const elapsedMs = Math.round(performance.now() - start)
const after = io()
results.push({ variant, calls: count, elapsedMs,
readMiB: Number(((after.ReadTransferCount - before.ReadTransferCount) / 1024 / 1024).toFixed(3)),
writeMiB: Number(((after.WriteTransferCount - before.WriteTransferCount) / 1024 / 1024).toFixed(3)) })
}
database.endExecutionTiming(taskId)
deepStrictEqual(database.getConversation(snapshot.id).messages,
snapshot.messages.map(item => item.id === message.id ? { ...item, state: 'streaming' } : item))
console.table(results)
} finally {
database?.close()
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
+245
View File
@@ -0,0 +1,245 @@
// 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
// 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
// task-events:text 250 ms text batches (persisted buffer)
// 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 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 { Worker } = require('node:worker_threads')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
if (process.env.GB_WPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const source = process.env.GB_WPERF_DATA
if (!source || !existsSync(join(source, 'assistant.sqlite'))) {
console.error('[wperf] set GB_WPERF_DATA to a GoodBuddy data directory containing assistant.sqlite')
process.exit(2)
}
const eventLimit = Number(process.env.GB_WPERF_EVENTS || 2000)
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 [
['src/main/assistant/assistant-database.ts', bundle],
['src/main/assistant/subagent-progress-storage.ts', progressBundle],
['src/main/readonly-query-worker.ts', join(directory, 'readonly-query-worker.cjs')]
]) {
buildSync({
entryPoints: [join(root, entry)], bundle: true, platform: 'node', format: 'cjs',
target: 'node22', outfile, logLevel: 'error', external: ['electron']
})
}
const { AssistantDatabase } = require(bundle)
const { restoreSubagentPayload } = require(progressBundle)
const round = value => Math.round(value * 10) / 10
const percentile = (values, p) => {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
/** Per-call times plus the longest gap between event-loop turns. */
async function measure(count, body, { yieldEvery = 1, rate = 0 } = {}) {
let probing = true
let last = performance.now()
let maxGap = 0
const tick = () => {
const now = performance.now()
maxGap = Math.max(maxGap, now - last)
last = now
if (probing) setImmediate(tick)
}
setImmediate(tick)
const samples = []
const slow = []
const started = performance.now()
for (let index = 0; index < count; index += 1) {
const at = performance.now()
body(index)
const elapsed = performance.now() - at
samples.push(elapsed)
if (elapsed > 50) slow.push([index, round(elapsed)])
if (rate > 0) {
// Real streams arrive at a rate; wait until this call's scheduled time.
const due = started + ((index + 1) * 1000) / rate
const wait = due - performance.now()
await new Promise(resolve => (wait > 1 ? setTimeout(resolve, wait) : setImmediate(resolve)))
} else if ((index + 1) % yieldEvery === 0) await new Promise(resolve => setImmediate(resolve))
}
const total = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
probing = false
return {
calls: count, totalMs: round(total), callP50: round(percentile(samples, 50)),
callP95: round(percentile(samples, 95)), callP99: round(percentile(samples, 99)),
callMax: round(Math.max(...samples)), loopMax: round(maxGap), slowCalls: slow.slice(0, 10)
}
}
async function main() {
console.log(`[wperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const copyStarted = performance.now()
for (const name of ['assistant.sqlite', 'assistant.sqlite-wal', 'assistant.sqlite-shm']) {
if (existsSync(join(source, name))) copyFileSync(join(source, name), join(directory, name))
}
// Initialization checks note files against the database.
if (existsSync(join(source, 'notes'))) cpSync(join(source, 'notes'), join(directory, 'notes'), { recursive: true })
console.log(`[wperf] copied data in ${round((performance.now() - copyStarted) / 1000)} s`)
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
const results = { openMs: round(performance.now() - openStarted) }
// Experiment switch: override the durability level after initialization.
if (process.env.GB_WPERF_SYNCHRONOUS) raw.exec(`PRAGMA synchronous = ${process.env.GB_WPERF_SYNCHRONOUS}`)
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') {
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)
results.taskEvents = count('SELECT COUNT(*) AS n FROM task_events')
results.messages = count('SELECT COUNT(*) AS n FROM messages')
results.activityRecords = count('SELECT COUNT(*) AS n FROM activity_history_records')
// The real task with the most subagent events, restored in order.
const sourceTask = raw.prepare(
`SELECT task_id FROM task_events WHERE kind = 'subagent' GROUP BY task_id ORDER BY COUNT(*) DESC LIMIT 1`
).get()?.task_id
const blocks = new Map()
const subagentEvents = sourceTask ? raw.prepare(
`SELECT payload_json FROM task_events WHERE task_id = ? AND kind = 'subagent' ORDER BY id LIMIT ?`
).all(sourceTask, eventLimit).map(row => restoreSubagentPayload(JSON.parse(row.payload_json), blocks)) : []
const toolEvents = raw.prepare(
`SELECT payload_json FROM task_events WHERE kind = 'tool' ORDER BY id DESC LIMIT ?`
).all(eventLimit).map(row => JSON.parse(row.payload_json))
const conversation = raw.prepare(
`SELECT conversation_id AS id FROM messages GROUP BY conversation_id ORDER BY COUNT(*) DESC LIMIT 1`
).get()?.id
const taskId = randomUUID()
database.createTask({ id: taskId, title: 'write perf', instructions: 'replay', conversationId: conversation })
const replay = (events, kind) => index => {
const event = { ...events[index % events.length], requestId: taskId }
database.appendTaskEvent(taskId, kind, event)
}
if (process.env.GB_WPERF_INSPECT) {
const at = Number(process.env.GB_WPERF_INSPECT)
const { compactSubagentPayload } = require(progressBundle)
const event = subagentEvents[at]
const t0 = performance.now()
const json = JSON.stringify(event)
const t1 = performance.now()
const restoredBlocks = new Map()
for (let i = 0; i < at; i += 1) restoreSubagentPayload(JSON.parse(JSON.stringify(compactSubagentPayload(subagentEvents[i], restoredBlocks))), restoredBlocks)
const t2 = performance.now()
const compact = JSON.stringify(compactSubagentPayload(event, restoredBlocks))
const t3 = performance.now()
console.log(`[wperf] event ${at}: full ${json.length} B (stringify ${round(t1 - t0)} ms), compact ${compact.length} B (compact ${round(t3 - t2)} ms)`)
console.log(`[wperf] event ${at} progress blocks: ${(event.progress ?? []).length}, updates: ${(event.progressUpdates ?? []).length}`)
}
// Paced at the busiest second seen in real data (269 subagent events/s); 0 = back to back.
const rate = Number(process.env.GB_WPERF_RATE ?? 270)
if (subagentEvents.length) {
results.subagentEvents = await measure(subagentEvents.length, replay(subagentEvents, 'subagent'), { rate })
}
if (toolEvents.length) results.toolEvents = await measure(toolEvents.length, replay(toolEvents, 'tool'), { rate })
const delta = 'streamed text '.repeat(40)
results.textBatches = await measure(400, index => database.appendTaskEvent(taskId, 'text',
{ type: 'text', requestId: taskId, delta: `${delta}${index}` }))
// Renderer local save: the conversation header and the streaming assistant message growing.
if (conversation) {
const snapshot = database.getConversation(conversation)
const header = {
id: snapshot.id, projectId: snapshot.projectId, title: snapshot.title,
runtimeSelection: snapshot.runtimeSelection, updatedAt: Date.now()
}
const messageId = randomUUID()
let content = ''
results.conversationSave = await measure(200, index => {
content += 'Lorem ipsum dolor sit amet, streaming reply chunk. '.repeat(8)
database.saveLocalConversations([{
header: { ...header, updatedAt: Date.now() + index },
messages: [{ id: messageId, role: 'assistant', state: 'streaming', content, createdAt: Date.now() }]
}])
})
results.conversationGet = await measure(20, () => database.getConversation(conversation))
}
const sample = database.getActivityHistoryPage({ limit: 50 }).records
if (sample.length) {
results.activityUpdate = await measure(200, index => database.updateActivityHistory({
changes: [{ type: 'upsert', position: 'in-place', record: { ...sample[index % sample.length], detail: `perf ${index}` } }]
}))
}
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`)
const rows = [
['task event: subagent (real stream)', results.subagentEvents],
['task event: tool (real)', results.toolEvents],
['task event: text batch', results.textBatches],
['conversation save (500 ms tick)', results.conversationSave],
['conversation get (largest)', results.conversationGet],
['activity update, 1 record', results.activityUpdate]
].filter(([, value]) => value)
console.table(rows.map(([name, r]) => ({
path: name, calls: r.calls, 'p50 ms': r.callP50, 'p95 ms': r.callP95, 'p99 ms': r.callP99,
'max ms': r.callMax, 'loop max ms': r.loopMax, 'calls/s': Math.round(r.calls / (r.totalMs / 1000))
})))
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', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
}
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 })
})
+208 -146
View File
@@ -1,4 +1,4 @@
const { createHash } = require('node:crypto')
const { createHash } = require("node:crypto");
const {
chmod,
mkdir,
@@ -6,101 +6,85 @@ const {
rename,
rm,
stat,
writeFile
} = require('node:fs/promises')
const { existsSync } = require('node:fs')
const { join } = require('node:path')
const { spawnSync } = require('node:child_process')
const tar = require('tar')
const { sha256File } = require('./file-hash.cjs')
writeFile,
} = require("node:fs/promises");
const { existsSync } = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const tar = require("tar");
const { sha256File } = require("./file-hash.cjs");
const {
opencodeVersion,
prepareBundledOpenCodeConfig,
} = require("./opencode-config.cjs");
const { npmInvocation } = require("./npm-invocation.cjs");
const { prepareNativeDshResources } = require("./native-dsh-resources.cjs");
const opencodeVersion = '1.18.9'
const architectureNames = {
1: 'x64',
3: 'arm64'
}
1: "x64",
3: "arm64",
};
const platformNames = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows'
}
darwin: "darwin",
linux: "linux",
win32: "windows",
};
function sha512Integrity(contents) {
return `sha512-${createHash('sha512').update(contents).digest('base64')}`
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
}
async function lockedIntegrity(projectDir, packageName) {
async function lockedIntegrity(projectDir, packageName, version) {
const lock = JSON.parse(
await readFile(join(projectDir, 'package-lock.json'), 'utf8')
)
const entry = lock.packages?.[`node_modules/${packageName}`]
await readFile(join(projectDir, "package-lock.json"), "utf8"),
);
const entry = lock.packages?.[`node_modules/${packageName}`];
if (
entry?.version !== opencodeVersion ||
typeof entry.integrity !== 'string'
entry?.version !== version ||
typeof entry.integrity !== "string"
) {
throw new Error(
`Missing locked ${packageName}@${opencodeVersion} integrity`
)
`Missing locked ${packageName}@${version} integrity`,
);
}
return entry.integrity
return entry.integrity;
}
function npmInvocation() {
const npmCli = process.env.npm_execpath
if (npmCli) {
return {
command: process.execPath,
prefixArgs: [npmCli]
}
}
if (process.platform === 'win32') {
throw new Error('npm_execpath is required to prepare bundled runtimes')
}
return {
command: 'npm',
prefixArgs: []
}
}
async function downloadPackage(projectDir, packageName, integrity) {
const cacheDirectory = join(
projectDir,
'.runtime-resources',
'cache'
)
await mkdir(cacheDirectory, { recursive: true })
async function downloadPackage(projectDir, packageName, version, integrity) {
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
await mkdir(cacheDirectory, { recursive: true });
const archivePath = join(
cacheDirectory,
`${packageName}-${opencodeVersion}.tgz`
)
`${packageName.replaceAll("/", "-").replaceAll("@", "")}-${version}.tgz`,
);
if (existsSync(archivePath)) {
const cached = await readFile(archivePath)
const cached = await readFile(archivePath);
if (sha512Integrity(cached) === integrity) {
return archivePath
return archivePath;
}
await rm(archivePath, { force: true })
await rm(archivePath, { force: true });
}
const npm = npmInvocation()
const npm = npmInvocation(projectDir);
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
'pack',
`${packageName}@${opencodeVersion}`,
'--ignore-scripts',
'--json',
'--pack-destination',
cacheDirectory
"pack",
`${packageName}@${version}`,
"--ignore-scripts",
"--json",
"--pack-destination",
cacheDirectory,
],
{
cwd: projectDir,
encoding: 'utf8',
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true
}
)
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to fetch ${packageName}: ${
@@ -108,81 +92,151 @@ async function downloadPackage(projectDir, packageName, integrity) {
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`
)
}`,
);
}
const output = JSON.parse(result.stdout)
const downloadedPath = join(cacheDirectory, output[0].filename)
const contents = await readFile(downloadedPath)
const output = JSON.parse(result.stdout);
const downloadedPath = join(cacheDirectory, output[0].filename);
const contents = await readFile(downloadedPath);
if (sha512Integrity(contents) !== integrity) {
await rm(downloadedPath, { force: true })
throw new Error(`Integrity verification failed for ${packageName}`)
await rm(downloadedPath, { force: true });
throw new Error(`Integrity verification failed for ${packageName}`);
}
if (downloadedPath !== archivePath) {
await rm(archivePath, { force: true })
await rename(downloadedPath, archivePath)
await rm(archivePath, { force: true });
await rename(downloadedPath, archivePath);
}
return archivePath
return archivePath;
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName
const architecture = architectureNames[context.arch]
const packagePlatform = platformNames[platform]
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`
)
}
const suffix =
architecture === 'x64' ? `${architecture}-baseline` : architecture
const packageName = `opencode-${packagePlatform}-${suffix}`
const projectDir = context.packager.projectDir
const projectPackage = JSON.parse(
await readFile(join(projectDir, 'package.json'), 'utf8')
)
await writeFile(
join(projectDir, 'out', 'main', 'package.json'),
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version:
projectPackage.dependencies['@deepseek-ai/dsh-llm'],
private: true,
type: 'module'
},
null,
2
)}\n`,
'utf8'
)
const integrity = await lockedIntegrity(projectDir, packageName)
const targetDirectory = join(
projectDir,
'.runtime-resources',
architecture
)
const readyPath = join(targetDirectory, '.ready.json')
const executable = platform === 'win32' ? 'opencode.exe' : 'opencode'
const preparedPath = join(targetDirectory, executable)
const identity = {
packageName,
version: opencodeVersion,
integrity
}
async function prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
version,
) {
const packageName = `@vscode/ripgrep-${platform}-${architecture}`;
const integrity = await lockedIntegrity(projectDir, packageName, version);
const executable = platform === "win32" ? "rg.exe" : "rg";
const preparedPath = join(targetDirectory, executable);
const readyPath = join(targetDirectory, ".ripgrep-ready.json");
const identity = { packageName, version, integrity };
try {
const ready = JSON.parse(await readFile(readyPath, 'utf8'))
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === 'string' &&
(platform === 'win32' ||
typeof ready.executableSha256 === "string" &&
(platform === "win32" || ((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return;
}
} catch {
// Rebuild an incomplete or stale ripgrep cache.
}
const archivePath = await downloadPackage(
projectDir,
packageName,
version,
integrity,
);
const stagingDirectory = `${targetDirectory}.ripgrep-staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const executableSha256 = await sha256File(sourcePath);
if (platform !== "win32") {
await chmod(sourcePath, 0o755);
}
await mkdir(targetDirectory, { recursive: true });
await rm(preparedPath, { force: true });
await rename(sourcePath, preparedPath);
await writeFile(
readyPath,
JSON.stringify({ ...identity, executableSha256 }),
"utf8",
);
} finally {
await rm(stagingDirectory, { recursive: true, force: true });
}
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName;
const architecture = architectureNames[context.arch];
const packagePlatform = platformNames[platform];
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`,
);
}
const suffix =
architecture === "x64" ? `${architecture}-baseline` : architecture;
const packageName = `opencode-${packagePlatform}-${suffix}`;
const projectDir = context.packager.projectDir;
await prepareNativeDshResources(projectDir, platform, architecture, downloadPackage, lockedIntegrity);
await prepareBundledOpenCodeConfig(projectDir);
const projectPackage = JSON.parse(
await readFile(join(projectDir, "package.json"), "utf8"),
);
const ripgrepVersion = projectPackage.dependencies["@vscode/ripgrep"];
if (typeof ripgrepVersion !== "string" || !/^\d+\.\d+\.\d+$/u.test(ripgrepVersion)) {
throw new Error("Missing exact @vscode/ripgrep dependency version");
}
await writeFile(
join(projectDir, "out", "main", "package.json"),
`${JSON.stringify(
{
name: "@deepseek-ai/dsh-llm",
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
private: true,
type: "module",
},
null,
2,
)}\n`,
"utf8",
);
const integrity = await lockedIntegrity(projectDir, packageName, opencodeVersion);
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
const readyPath = join(targetDirectory, ".ready.json");
const executable = platform === "win32" ? "opencode.exe" : "opencode";
const preparedPath = join(targetDirectory, executable);
const identity = {
packageName,
version: opencodeVersion,
integrity,
};
try {
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === "string" &&
(platform === "win32" ||
((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
return;
}
} catch {
// Rebuild an incomplete or stale runtime cache.
@@ -191,32 +245,40 @@ module.exports = async function prepareBundledRuntimes(context) {
const archivePath = await downloadPackage(
projectDir,
packageName,
integrity
)
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`
await rm(stagingDirectory, { recursive: true, force: true })
await mkdir(stagingDirectory, { recursive: true })
opencodeVersion,
integrity,
);
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1
})
const sourcePath = join(stagingDirectory, 'bin', executable)
const stagingExecutable = join(stagingDirectory, executable)
await rename(sourcePath, stagingExecutable)
if (platform !== 'win32') {
await chmod(stagingExecutable, 0o755)
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const stagingExecutable = join(stagingDirectory, executable);
await rename(sourcePath, stagingExecutable);
if (platform !== "win32") {
await chmod(stagingExecutable, 0o755);
}
const executableSha256 = await sha256File(stagingExecutable)
const executableSha256 = await sha256File(stagingExecutable);
await writeFile(
join(stagingDirectory, '.ready.json'),
join(stagingDirectory, ".ready.json"),
JSON.stringify({ ...identity, executableSha256 }),
'utf8'
)
await rm(targetDirectory, { recursive: true, force: true })
await rename(stagingDirectory, targetDirectory)
"utf8",
);
await rm(targetDirectory, { recursive: true, force: true });
await rename(stagingDirectory, targetDirectory);
} finally {
await rm(stagingDirectory, { recursive: true, force: true })
await rm(stagingDirectory, { recursive: true, force: true });
}
}
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
};
+31
View File
@@ -0,0 +1,31 @@
function preflightRegisteredProductionKey(options) {
const keyId = options.keyId
if (!keyId) {
throw new Error(options.missingKeyIdMessage)
}
const signingRecord = options.registry.keys.find(
(key) => key.keyId === keyId
)
if (!signingRecord) {
throw new Error(
`Production ${options.component} public signing key ID "${keyId}" is absent from resources/agent-release-keys.json; provision the matching public key before building a release`
)
}
if (signingRecord.environment !== 'production') {
throw new Error(
`Production ${options.component} signing key ID "${keyId}" is not registered for production`
)
}
if (
options.registry.revocations.some(
(revocation) => revocation.keyId === keyId
)
) {
throw new Error(
`Production ${options.component} signing key ID "${keyId}" is revoked`
)
}
return signingRecord
}
module.exports = { preflightRegisteredProductionKey }
+39
View File
@@ -0,0 +1,39 @@
const { readdirSync, readFileSync, statSync } = require('node:fs')
const { join, relative } = require('node:path')
const { opencodeVersion } = require('./opencode-config.cjs')
function verifyOpenCodeConfig(resources, source) {
const destination = join(resources, 'runtimes', 'opencode-config')
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
throw new Error('Packaged OpenCode plugin version is invalid')
}
const pending = [source]
let files = 0
while (pending.length > 0) {
const directory = pending.pop()
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const file = join(directory, entry.name)
if (entry.isDirectory()) {
pending.push(file)
} else if (entry.isFile()) {
const target = join(destination, relative(source, file))
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
!readFileSync(target).equals(readFileSync(file))) {
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
}
files++
}
}
}
return files
}
function verifyHarnessBundleImports(source, name) {
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
}
}
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
+9 -1
View File
@@ -1,5 +1,13 @@
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const {
targetDefinitions
} = require('./aggregate-release.cjs')
const expectedInstallerCount = targetDefinitions.reduce(
(count, target) => count + target.formats.length,
0
)
function parseArguments(argv) {
if (argv.length !== 2 || argv[0] !== '--manifest' || !argv[1]) {
@@ -12,7 +20,7 @@ async function verifySiteRelease(manifest, request = fetch) {
const files = Object.values(manifest?.targets ?? {}).flatMap(
(target) => Object.values(target?.files ?? {})
)
if (files.length !== 12) {
if (files.length !== expectedInstallerCount) {
throw new Error(`官网发布文件数量错误:${files.length}`)
}
const urls = new Set()
+141
View File
@@ -0,0 +1,141 @@
const { readSync } = require('node:fs')
const localFileHeaderSignature = 0x04034b50
const centralDirectoryHeaderSignature = 0x02014b50
const endOfCentralDirectorySignature = 0x06054b50
function readExactZipBytes(handle, length, position, label) {
const output = Buffer.allocUnsafe(length)
let offset = 0
while (offset < length) {
const bytesRead = readSync(
handle,
output,
offset,
length - offset,
position + offset
)
if (bytesRead <= 0) {
throw new Error(`${label} is truncated`)
}
offset += bytesRead
}
return output
}
function readZipCentralDirectory(handle, archiveSize, limits) {
if (archiveSize < 22) {
throw new Error(`${limits.label} central directory is missing`)
}
const endSize = Math.min(archiveSize, 65_557)
const endStart = archiveSize - endSize
const end = readExactZipBytes(
handle,
endSize,
endStart,
`${limits.label} end record`
)
let endOffset = -1
for (let index = end.length - 22; index >= 0; index -= 1) {
if (
end.readUInt32LE(index) === endOfCentralDirectorySignature &&
index + 22 + end.readUInt16LE(index + 20) === end.length
) {
endOffset = index
break
}
}
if (endOffset < 0) {
throw new Error(`${limits.label} central directory is missing`)
}
const diskNumber = end.readUInt16LE(endOffset + 4)
const centralDisk = end.readUInt16LE(endOffset + 6)
const diskEntryCount = end.readUInt16LE(endOffset + 8)
const entryCount = end.readUInt16LE(endOffset + 10)
const centralSize = end.readUInt32LE(endOffset + 12)
const centralOffset = end.readUInt32LE(endOffset + 16)
const absoluteEndOffset = endStart + endOffset
if (
diskNumber !== 0 ||
centralDisk !== 0 ||
diskEntryCount !== entryCount ||
entryCount === 0 ||
entryCount === 0xffff ||
entryCount < limits.minimumEntries ||
entryCount > limits.maximumEntries ||
centralSize === 0xffffffff ||
centralSize < 46 ||
centralSize > limits.maximumCentralDirectoryBytes ||
centralOffset === 0xffffffff ||
centralOffset + centralSize !== absoluteEndOffset
) {
throw new Error(`${limits.label} central directory is invalid`)
}
const central = readExactZipBytes(
handle,
centralSize,
centralOffset,
`${limits.label} central directory`
)
const entries = []
let offset = 0
for (let index = 0; index < entryCount; index += 1) {
if (
offset + 46 > central.length ||
central.readUInt32LE(offset) !==
centralDirectoryHeaderSignature
) {
throw new Error(
`${limits.label} central directory entry is invalid`
)
}
const nameLength = central.readUInt16LE(offset + 28)
const extraLength = central.readUInt16LE(offset + 30)
const commentLength = central.readUInt16LE(offset + 32)
const entryLength =
46 + nameLength + extraLength + commentLength
if (offset + entryLength > central.length) {
throw new Error(
`${limits.label} central directory entry exceeds its bounds`
)
}
const flags = central.readUInt16LE(offset + 8)
const nameBytes = central.subarray(
offset + 46,
offset + 46 + nameLength
)
const name = new TextDecoder(
flags & 0x0800 ? 'utf-8' : 'latin1',
{ fatal: true }
).decode(nameBytes)
entries.push({
name,
nameBytes: Buffer.from(nameBytes),
flags,
compression: central.readUInt16LE(offset + 10),
checksum: central.readUInt32LE(offset + 16),
compressedSize: central.readUInt32LE(offset + 20),
size: central.readUInt32LE(offset + 24),
diskStart: central.readUInt16LE(offset + 34),
localOffset: central.readUInt32LE(offset + 42)
})
offset += entryLength
}
if (offset !== central.length) {
throw new Error(
`${limits.label} central directory count is invalid`
)
}
return {
centralOffset,
entries
}
}
module.exports = {
centralDirectoryHeaderSignature,
endOfCentralDirectorySignature,
localFileHeaderSignature,
readExactZipBytes,
readZipCentralDirectory
}
-50
View File
@@ -1,50 +0,0 @@
# GoodBuddy 文档导航
GoodBuddy 文档按“文档类型 → 功能域”组织。新增文档应先选择类型,再放入对应功能目录,
避免继续把所有设计平铺到单一 `features` 目录。
## 产品需求
| 功能域 | 入口 |
| --- | --- |
| Task 与 Job | [Task 与 Job 总览](./prd/task-and-job/README.md) |
| Smart Heartbeat | [智能心跳 PRD](./prd/smart-heartbeat/smart-heartbeat-prd.md) |
| 助手工作栏 | [通用助手工作栏与执行空间 PRD](./prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md) |
| 会话监督 | [会话监督 PRD](./prd/supervision/conversation-supervision-prd.md) |
| 记忆 | [分区记忆 PRD](./prd/memory/partitioned-memory-prd.md) |
| 并行实验 | [并行实验工作台 PRD](./prd/experiments/parallel-experiments-prd.md) |
| 持续学习 | [持续学习与评估门 PRD](./prd/learning/continuous-learning-prd.md) |
| 知识库 | [知识库检索与分块增强 PRD](./prd/knowledge/knowledge-rag-enhancement-prd.md) |
| 文档处理 | [文档解析与本地 OCR](./prd/document-processing/document-extraction-and-local-ocr.md) |
| 消息通道 | [微信 ClawBot 通道 PRD](./prd/channels/wechat-clawbot-channel-project-prd.md) |
## Task 与 Job 文档
- [统一领域模型](./prd/task-and-job/task-and-job-model.md)
- [Task Center](./prd/task-and-job/task-center-prd.md)
- [Scheduled Task](./prd/task-and-job/scheduled-task-prd.md)
- [Goal Task](./prd/task-and-job/goal-task-prd.md)
- [Job 与 Subjob](./prd/task-and-job/job-and-subjob-prd.md)
## 跨功能文档
- [自动化平台架构](./architecture/automation-platform-architecture.md)
- [平台功能页签与模型下载源设计](./architecture/model-download-source-design.md)
- [本地文本向量模型与连接设计](./architecture/local-text-embedding-model-design.md)
- [全双工实时语音交互设计](./architecture/full-duplex-voice-design.md)
- [DeepSeek Harness Runtime 设计](./architecture/deepseek-harness-runtime-design.md)
- [跨平台助手产品设计](./design/cross-platform-assistant-product-design.md)
- [长期助手路线图](./roadmap/long-term-assistant-roadmap.md)
- [电脑控制实施状态](./status/computer-control-implementation-status.md)
- [知识检索评估](./quality/knowledge-retrieval-evaluation.md)
- [统一界面设计系统](../UI-DESIGN.md)
## 目录规则
1. PRD 放在 `docs/prd/<功能域>/`。
2. 跨功能技术总纲放在 `docs/architecture/`。
3. 产品级设计放在 `docs/design/`,路线图和实施状态分别放在 `roadmap`、`status`。
4. 测试方法、评估协议和质量报告放在 `docs/quality/`。
5. 一个概念只能有一份权威定义;其他文档链接到它,不复制另一套术语。
6. Task、Job、Run、Subagent 和 Conversation 的含义以
[Task 与 Job 统一领域模型](./prd/task-and-job/task-and-job-model.md) 为准。
@@ -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.
@@ -9,7 +9,7 @@
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 文档角色 | Task/Job、调度、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
| 领域模型 | [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md) |
| 领域模型 | [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md) |
## 1. 背景
@@ -37,11 +37,11 @@ GoodBuddy 当前已经具备若干长期助手能力,但它们仍是彼此分
## 2. 核心产品判断
### 2.1 智能心跳保持独立,未来分区记忆另行设计
### 2.1 智能心跳保持独立,分区记忆协同另行设计
当前智能心跳继续承担周期回顾、报告和建议,并支持 Global 或指定 Project 范围。它不是
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。未来分区记忆的
数据、状态、唤起和页面需要独立设计,不能从当前方向直接推导。
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。按分区回顾、候选生成
以及时间/内部事件唤起需要独立设计,不能从当前心跳行为直接推导,也不创建平行记忆类型。
- Scheduled Task 解决“何时在一个 Task 中执行新的 Job”。
- Goal Task 解决“围绕结果在同一 Task 中持续规划和推进”。
@@ -84,10 +84,10 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
初期自动学习只允许产生可审查候选,不允许自动修改:
- 工具权限和审批策略。
- 工具启用、分配与资源范围。
- Electron 安全边界。
- 项目根目录和数据访问范围。
- Runtime 当前用户执行权限与 Ask/Execute 边界。
- Runtime 当前用户执行权限与实际能力范围。
- 系统级提示词。
- 远程消息发送或其他外部副作用策略。
@@ -105,11 +105,11 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
### 3.2 产品目标
- 复用现有 Project、Conversation、Task、Run、Artifact、Approval 和 Notification 能力。
- 复用现有 Project、Conversation、Task、Run、Artifact 和 Notification 能力。
- 保持每个 Task 只关联一条 Conversation,同时允许一条 Conversation 承载多个 Task,不为
同一项工作建立第二份内容载体。
- 为所有后台工作提供统一的幂等、租约、恢复、取消、预算和审计语义。
- 保持 Ask 只读,Execute 继续经过现有能力和审批控制。
- 沿用统一执行与能力配置,不引入工作模式或一般工具审批。
- 保持本地优先,应用退出后不虚假承诺后台持续执行。
- 保证项目、会话、自动化和实验 Run 之间的记忆隔离。
- 先建立可观测和可评估能力,再允许任何形式的自动行为改变。
@@ -120,7 +120,7 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
- 将 GoodBuddy 变为需要常驻服务器、Redis 或云端控制面的多租户平台。
- 在应用退出后依靠未安装的系统服务继续运行任务。
- 默认允许无人值守高风险 Execute。
- 自动扩大无人值守任务的能力或资源范围。
- 让模型自行扩大工具、目录、知识库、记忆或网络访问范围。
- 允许多个实验 Run 并发修改同一个用户工作区。
- 记录键盘、持续录屏或静默监控其他应用。
@@ -197,13 +197,12 @@ Conversation
- Plan 版本。
- 项目和工作目录。
- Runtime 和模型配置引用。
- 工作模式。
- 提示和变量。
- 工具、Skills、MCP 和知识库范围。
- 可读、可写记忆分区。
- 监督策略和评估器版本。
- 预算和并发限制。
- 审批策略。
- 能力分配。
运行开始后的设置变化只影响下一次 Run。用户可以在 Task 执行记录中查看当次快照与最新
Plan 的差异,但 Run 不作为独立导航对象。
@@ -290,14 +289,17 @@ Runtime 边界完成,用户可见结果通过所属 Task 汇入关联 Conversa
当前实现以 Main 和 SQLite 中的 `conversation_queue_items` 作为每条 Conversation 的权威
输入队列,而不是在 Renderer 分别维护聊天草稿队列和 Scheduled Task 队列:
- 普通消息在发送时冻结 Runtime、工作模式、专家/团队、知识范围和附件上下文,再以
- 普通消息在发送时冻结 Runtime、专家/团队、知识范围和附件上下文,再以
`source=user` 入队;附件内容使用有界序列化保存,应用重启后仍可恢复。
- 到期或手动启动的 Scheduled Task 先建立 `schedule_run`,再以 `source=schedule` 进入同一
队列。Scheduler 不再绕过队列直接调用 Runtime。
队列,只派发任务要求和关联会话身份。执行时读取会话最新历史和配置,不保存任务上下文快照。
- Main 对每条 Conversation 只保留一个活动请求或 Renderer 派发保留位。默认按 FIFO 认领;
全局 Scheduled Task 执行仍受最多 4 项并发限制。
- 用户消息由 Main 派发给 Renderer,由 Renderer 建立用户消息和流式助手消息后调用
`agent.run`;Scheduled Task 由 Main 直接执行。两条路径共享同一 Conversation 活动锁。
Scheduled Task 不另设执行器或并发计数。
- 两种来源均由 Main 派发给 Renderer,建立普通用户消息和流式助手消息后调用 `agent.run`。
Renderer 加载会话及设置后才声明队列可派发。SSH 项目同样通过该入口选择远程 Runtime,
使用普通请求的上下文、工具范围、取消和恢复流程。
- `schedule_run.id` 作为普通请求 ID;普通请求终态同步该次计划运行与稳定产品 Task 的状态。
尚未接受的派发可释放重试;已经接受的运行在重启后按普通 Task 状态结算或恢复,不重新发送。
- 当前执行到达终态后再认领下一项。删除只移除尚未执行的项;“立即中断并插入”取消当前
请求并把所选项设为下一项,不重排其他项。
- 每条 Conversation 最多保留 20 个用户可提交的待执行项。启动时将未完成的派发恢复为
@@ -422,22 +424,21 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
```text
任务中心
├─ 需要关注
├─ 进行中
├─ 已暂停
├─ 暂停
└─ 已结束
└─ 打开任务自身
任务自身
├─ 消息时间线
├─ Run、步骤与审批活动
├─ Run、步骤与工具活动
├─ 监督、指标与证据
└─ 独立成果
智能心跳
├─ 成长概览
├─ 运行概览
├─ 待处理建议
├─ 心跳轨迹
├─ 报告与记录
└─ 心跳计划
└─ Global / 指定 Project
```
@@ -445,13 +446,13 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
Task 或实验对象;详细范围与交互契约见
[通用助手工作栏与执行空间 PRD](../prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
## 13. 安全与隐私
1. Ask 在 Runtime 边界保持只读,而不只是提示词要求只读。
2. Execute 继续通过现有审批、主机执行策略、工具和目录控制。
3. 无人值守只允许用户显式批准的能力集合;遇到未预授权动作时进入等待审批。
1. 正常请求按已注册工具、启用与分配配置执行,内部文本调用保持无工具。
2. 保留主机账号权限、参数与资源范围校验,不恢复一般工具审批策略。
3. 无人值守不获得未启用能力;需要结构化回答时按后台入口的既有支持范围处理。
4. Supervisor、Evaluator 和 Heartbeat 都把消息、工具输出、记忆和成果视为不可信数据。
5. 监督器不能读取隐藏推理,只能读取产品允许持久化和展示的事件。
6. 所有跨分区读取由 Main 根据绑定关系决定,Renderer 不能提交任意分区 ID。
@@ -467,7 +468,7 @@ Task 或实验对象;详细范围与交互契约见
- 触发来源和计划版本。
- 计划目标和当前 `goalStatus`。
- Runtime、工作模式和工作目录。
- Runtime 和工作目录。
- 实际读取的知识库与记忆分区。
- 实际调用的模型、Token、工具、耗时和成果大小。
- 当前预算和剩余预算。
@@ -507,7 +508,7 @@ experiment_runs
现有 `schedules`、`schedule_runs`、`heartbeat_configs`、`heartbeat_runs`、
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。Schedule 可渐进建立稳定 Task 与
Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心跳数据保持独立,不得
静默转成 `AutomationPlan` 或顶层 Task。未来分区记忆完成设计前,不新增迁移目标。
静默转成 `AutomationPlan` 或顶层 Task。分区记忆协同和唤起条件完成设计前,不新增迁移目标。
## 16. 分阶段实施
@@ -518,7 +519,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
- 明确 Task N:1 Conversation 关系、左侧行首展开按钮与 Task 子项图标,以及 Task Center
索引边界,不复制内容。
- 为现有 Scheduled Task 和专家执行建立按 Task 聚合的活动视图。
- 明确当前心跳保持独立,未来分区记忆尚待设计。
- 明确当前心跳保持独立,分区记忆协同与唤起条件尚待设计。
- 补充触发来源、运行版本、预算和读写范围展示。
### 阶段 1:调度与运行基础
@@ -539,7 +540,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
- 增加目标、成功标准、约束、预算和停止条件。
- 支持有界的观察、计划、行动、评估循环。
- 默认 Ask 或需要逐步审批的 Execute。
- 普通请求采用统一执行;专用内部分析采用无工具调用。
### 阶段 4:并行实验
@@ -555,29 +556,29 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
## 17. 相关文档
- [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md)
- [Task Center PRD](../prd/task-and-job/task-center-prd.md)
- [Scheduled Task PRD](../prd/task-and-job/scheduled-task-prd.md)
- [Job 与 Subjob PRD](../prd/task-and-job/job-and-subjob-prd.md)
- [智能心跳 PRD](../prd/smart-heartbeat/smart-heartbeat-prd.md)
- [并行实验工作台 PRD](../prd/experiments/parallel-experiments-prd.md)
- [会话监督 PRD](../prd/supervision/conversation-supervision-prd.md)
- [分区记忆 PRD](../prd/memory/partitioned-memory-prd.md)
- [持续学习与评估门 PRD](../prd/learning/continuous-learning-prd.md)
- [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md)
- [Task Center PRD](../features/task-and-job/task-center-prd.md)
- [Scheduled Task PRD](../features/task-and-job/scheduled-task-prd.md)
- [Job 与 Subjob PRD](../features/task-and-job/job-and-subjob-prd.md)
- [智能心跳 PRD](../features/smart-heartbeat/prd.md)
- [并行实验工作台 PRD](../features/parallel-experiments/prd.md)
- [会话监督 PRD](../features/conversation-supervision/prd.md)
- [分区记忆 PRD](../features/memory/prd.md)
- [持续学习与评估门 PRD](../features/continuous-learning/prd.md)
- [GoodBuddy 长期助手功能规划](../roadmap/long-term-assistant-roadmap.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
## 18. 总体验收标准
- [ ] 智能心跳保持独立,不作为 Task 类型;未来分区记忆尚未设计。
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
关联多个 Task。
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
- [ ] Scheduled Task 的重复触发和并行 Job 不创建新的顶层 Task。
- [ ] 每次自动执行的内部 Run 都记录触发原因、目标、范围、预算、状态和结果,并在所属
Task 下提供有界可观测信息。
- [ ] Ask 自动化无法调用写工具或产生外部副作用。
- [ ] Execute 自动化不能绕过现有审批、主机执行策略和能力控制。
- [ ] 专用内部文本调用不获得工具。
- [ ] 自动化不能绕过主机账号权限和能力范围。
- [ ] 会话监督默认只评论,不能替用户发言或批准工具。
- [ ] 并行 Run 的变量、会话、运行记忆、任务和成果相互隔离。
- [ ] 失败 Run 不参与最佳结果选择,全部失败不报告成功。
@@ -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.
@@ -0,0 +1,101 @@
# 本机专用推理架构与任务控制评估
## 记录范围与状态
评估日期:2026-09-18。本文记录 ASR、OCR、内置 embedding 的执行边界、任务控制缺口和后续优化依据,供语音输入、文档解析、知识检索与索引共同使用。
现状依据本次工作区源码核对,包含尚未提交的变更,不等同于已发布版本或实机验收结论。本次仅整理文档,未运行引擎性能测试、复现故障或实施迁移。下文“建议”是评估方向,“待办”均未在本次实施,也没有约定交付日期。
产品 Task、Job、Run 的定义沿用 [Task 与 Job 领域模型](../features/task-and-job/task-and-job-model.md)。本文的“推理任务”指一次引擎请求及其执行记录,不自动等同于产品 Task,不据此新增产品对象、持久化日志或全局调度器。本机推理管理的现有界面和接入清单见[应用导航技术设计](../features/application-tool-navigation/technical-design.md#111-本机推理任务与执行服务);本文负责跨功能架构评估及优化待办。
## 当前执行边界
| 能力 | 源码现状 | 所有权与释放 | 评估建议 |
| --- | --- | --- | --- |
| ASR | 录音结果就绪后提交完整 PCM;Main 创建 `node:worker_threads` Worker,以 sherpa-onnx WASM 执行离线识别;服务一次只接受一个请求 | 每请求创建识别器;超时或 abort 调用并等待 `worker.terminate()`;正常结果路径的清理确认存在缺口,见 TC-4 | 保留请求级 Worker,适合间歇语音输入;常驻化需流式需求或测量支持 |
| OCR | Main 排队后发往主窗口 Renderer;Renderer 复用 Web Worker,运行 `ppu-paddle-ocr/web` 与 `onnxruntime-web` WASM;文档串行处理,PDF 逐页处理 | Renderer 模块持有 Worker;活动请求取消或单页超时会终止 Worker;无活动和排队请求时空闲 60 秒释放 | 当前保留 Web Worker;独立于 UI 的持续任务可评估专用后台 Renderer 加 Worker |
| 内置 embedding | Main 按模型目录共享引用计数 Broker;transport 使用 `utilityProcess.fork`,Granite tokenizer 和 ONNX session 首次请求懒加载并复用 | 多使用方共享进程;最后一个 provider 释放时 shutdown;也支持显式启停,停止等待 child exit | 保留共享 utilityProcess;优先核对取消后的容量回收与加载失败重试 |
### ASR 的证据与边界
[App.tsx](../../src/renderer/src/App.tsx) 在 `recording.result` 完成后调用 `speech.transcribe`;[录音模块](../../src/renderer/src/speech-recognition.ts)负责采样与重采样。[SpeechTranscriptionService](../../src/main/speech/speech-transcription-service.ts) 的 `active.size` 拒绝并发识别,`runSherpaTranscription` 为每次请求创建 Worker,转移音频缓冲区,使用 `createOfflineRecognizer` 同步 decode。Worker 创建后的识别超时为 120 秒,不包含此前模型解析的等待时间。
[package.json](../../package.json) 固定 `sherpa-onnx` 为 1.13.4;本次同时核对已安装包的 `index.js`,其入口加载 `sherpa-onnx-wasm-nodejs.js`。因此这里记录的是 Node Worker 内的 WASM 路径。Node Worker 与 Main 共享 OS 进程,线程隔离不能视为独立进程故障边界。
Worker 先 `postMessage`,再在 `finally` 中调用 `stream.free()` 和 `recognizer.free()`。Main 收到结果后以 `finish(..., false)` 结算、移除监听和超时,不等待 Worker exit;随后 `active` 删除请求,推理记录转为终态。这证明结果完成与资源清理确认之间存在记账间隙,但尚未证明实际泄漏或清理耗时。
### OCR 的浏览器依赖
[OCR Worker](../../src/renderer/src/document-ocr-worker.ts) 显式选择 WASM provider、`canvas-native` 图像处理,当前 `ort.env.wasm.numThreads = 1`。PDF 路径使用 `pdfjs-dist`、`OffscreenCanvas` 和 `convertToBlob`。本次核对的依赖链为 `ppu-paddle-ocr/web/platform.web.js` 到 `ppu-ocv/canvas-web`;后者的 `platform/web.js` 使用 `createImageBitmap` 解码图片。包依赖以 [package-lock.json](../../package-lock.json) 为准,依赖升级后需重新核对此结论。
这些能力与浏览器执行环境有关。直接迁到 Node utilityProcess,需要另行验证图像解码、Canvas、PDF 渲染及打包资源路径,不能只替换进程创建 API 就宣称等价。
[DocumentOcrBroker](../../src/main/document-ocr-broker.ts) 最多保留 4 个 pending 请求,一次派发一个;总超时从派发时开始,不覆盖 Main 排队等待。其预算为 `min(10 分钟, max(单页预算, 60 秒 + 单页预算 × 页数))`。[Renderer bridge](../../src/renderer/src/document-ocr-bridge.ts) 另有串行 Promise 队列,Worker 就绪超时为 120 秒,就绪后按单页预算计时,并在进度消息到达时重置单页计时器。Main 的 60 秒启动预算与 Renderer 的 120 秒就绪等待未对齐,短文档冷启动时 Main 有机会先判超时。
[Main 初始化](../../src/main/index.ts) 将 Broker 绑定到 `mainWindow`;[Renderer 入口](../../src/renderer/src/main.tsx) 安装 bridge。Broker 派发前仅检查窗口销毁等条件,没有等待 bridge 就绪的握手,也没有在该派发链路中处理 bridge 就绪丢失。Renderer 内的 Worker `ready` 只确认模型初始化,不能证明 Main 发请求时 Renderer 已安装 IPC 监听。窗口重载、Renderer 崩溃及启动时监听尚未安装的影响应按 TC-3 复现。
来源请求可以取消 OCR;已派发请求取消时 Main 发送取消消息并立即结算,然后派发下一项,未等待 Renderer 清理确认。Renderer 对当前活动请求的取消会终止共享 Worker。管理页因此没有将该操作作为安全的单任务取消能力;手动释放也会拒绝活动或排队请求。
### Embedding 的共享执行与取消
[Main provider 工厂](../../src/main/index.ts) 按模型目录维护 `sharedEmbeddingBrokers` 及使用计数;[transport](../../src/main/knowledge/embedding-utility-transport.ts) 以 spawn 确认进程启动。[Granite 引擎](../../src/main/knowledge/granite-embedding-engine.ts) 缓存 `loadTask`,在首次请求时创建 tokenizer 和 WASM ONNX session。进程已启动不等于模型已加载;当前没有为模型加载完成提供独立服务状态。
[Broker](../../src/main/knowledge/embedding-inference-broker.ts) 默认请求超时为 120 秒,计时在 transport 就绪后建立 pending 时开始。调用方取消或超时会先返回错误,同时发送 cancel,保留 pending 和推理执行记录,等待结果、`CANCELLED` 或进程退出。保留容量避免把仍在计算的任务当作已释放。
[执行端](../../src/main/knowledge/embedding-inference-worker.ts) 收到 cancel 后设置 AbortSignal;Granite 在加载、输入构造和 `session.run` 前后检查取消,但没有将可抢占的取消句柄传入 `session.run`。若该调用一直不返回,取消检查和确认就无法完成。Broker 的 `cancelPending` 会清除原计时器,当前没有针对该 pending 的后续有界清理升级,因此持续挂起可一直占用在途容量。此为源码可达的条件风险,真实引擎挂起尚未复现。
`loadTask ??= this.loadFiles(...)` 也会缓存被拒绝的加载 Promise;同一引擎实例后续请求复用同一个失败,不能通过再次请求重新加载。重建进程会建立新引擎;是否增加实例内重试及其清理方式,按 TC-2 验证后确定。
## 架构建议与触发条件
ASR 继续按录音请求创建 Worker。对低频、间歇输入,每次释放模型能避免长期占用;本次没有冷启动与常驻成本数据,不能量化收益。只有连续流式识别成为真实需求,或高频请求测出重复加载已影响目标延迟,才评估常驻 utilityProcess。届时需比较加载成本、暖请求延迟、空闲工作集和取消时延,并重新设计音频流与识别器复用;单纯常驻不会把当前离线 API 变成流式识别。
OCR 保留 Web Worker。如果文档任务需要在主 UI 重载或不可用时继续执行,优先评估由 Main 持有的专用后台 Renderer 加 Worker,以保留浏览器图像/PDF 能力。该方案仍是候选,需要完成以下设计与实机验证后才可决定迁移:
- 保持现有 sandbox、context isolation、禁用 Node integration 的边界;通过专用 preload 暴露必要方法,并沿用可信 sender 与输入校验。当前 IPC 若限定主窗口 sender,需要明确新增宿主的受信身份及响应归属。
- 分别确认后台 Renderer 的 IPC 监听就绪与 Worker 模型就绪;定义重载、崩溃、启动失败及应用退出时 pending、队列和模型的处置,防止静默丢请求或自动重放。
- Main 持有后台宿主生命周期;管理 UI 关闭不销毁执行宿主,服务释放和应用退出才按约定清理。验证隐藏 Renderer 的调度、计时器和 Worker 执行,不根据窗口不可见推断运行不受影响。
- 用实际 OS PID、Electron 进程指标和崩溃影响验证隔离程度。单独的 BrowserWindow/WebContents 不能直接作为独立 OS 进程或独立服务资源统计的证据;若与其他 Renderer 共享 PID,必须继续报告共享范围。
Embedding 保留共享 utilityProcess。查询与索引共享模型和进程的现有方式具备复用基础,优化先处理取消回收和失败重试。单请求取消继续尽量只影响该请求;进程终止属于服务级操作,必须说明其他使用方的结果。不得把每次请求超时直接等同于可以无条件杀死共享进程。
## 任务控制覆盖与缺口
[共享契约](../../src/shared/local-inference-contracts.ts)将服务状态与任务状态分开;[LocalInferenceService](../../src/main/local-inference-service.ts) 在 Main 内存登记请求,保存活动项及最近 100 条终态记录。任务的 `running` 当前包含排队、模型加载与执行,未提供统一细分阶段。该登记器是观测和控制入口,各引擎仍负责实际队列与清理。
| 控制维度 | 当前覆盖 | 后续评估和验收要求 |
| --- | --- | --- |
| 状态 | 任务有 `running/cancelling/completed/cancelled/failed`;服务另有启动、运行、停止、未知和错误状态;ASR 模型可用时服务仍为 unknown | 区分调用方已返回、执行仍活动、资源清理已确认;进程 spawn、模型 ready、请求完成不能互相替代。先增加必要证据,不预建完整阶段状态机 |
| 所有权 | ASR 请求持有 Worker;OCR Renderer 持有共享 Worker;embedding 按目录共享 Broker 并引用计数 | 控制记录注明真实来源与宿主;缺失项目、会话关联时不猜测归属。产品 Task 的取消不能凭名称去停止整个推理服务 |
| 取消 | ASR terminate;OCR 来源取消会终止当前 Worker;embedding 协作取消并保留执行记录 | 验证取消与自然完成竞态、重复取消及清理结果。单请求取消与共享服务停止分别定义影响,不用 UI 隐藏代替取消 |
| 超时与容量 | ASR Worker 120 秒;OCR 两端预算与排队语义不同;embedding 默认请求 120 秒且保留取消后容量 | 分别说明排队、启动、推理、清理的计时起点和上限;调用方超时后仍在执行的工作需要有界处置,不得提前释放容量制造超额并发 |
| 停止与清理 | 服务操作按 serviceId 串行,期间拒绝新增请求;停止校验当前活动 taskId 确认清单;向量停止等待 exit,重启不重放请求 | 清理失败保持可观察的活动/错误信息;窗口或宿主丢失需使受影响请求得到明确结果;ASR 和 OCR 的结果/清理间隙分别核对 |
| 可观测性 | 有来源、状态、时间、可选模型和错误;向量有 PID 资源数据;管理 Modal 不显示任务历史 | 测量排队、加载、执行、取消请求到执行结束、清理确认的耗时与在途数;日志不记录音频、图片或向量原文,未知指标不补零 |
本文的覆盖要求用于后续问题复现和最小修复,不构成引入统一常驻服务框架、持久化执行副本或自动恢复协议的决定。范围为桌面本机引擎;远程 Agent、外部向量端点和未接入的 TTS 不因此获得同样控制能力。
## 资源观测口径
[LocalInferencePage](../../src/renderer/src/LocalInferencePage.tsx) 在启用、文档可见且 Modal 内容未被 hidden/inert 隔离时每 5 秒读取快照,跳过重叠请求,关闭时清理轮询。该刷新间隔不能用于证明短请求的峰值或清理完成时刻。
[向量资源采样器](../../src/main/inference-process-resources.ts) 按 transport 当前 PID 匹配 `app.getAppMetrics()` 的 Utility 条目,用累计 CPU 秒差除以单调时间间隔。100% 表示一个逻辑核心,可超过 100%;首次采样、PID/创建时间变化、间隔超过 15 秒或读数异常时重建基线。工作集从 KiB 转为字节,UI 显示 MiB。
向量工作集包含运行时、模型、临时缓冲区及并发使用方,不能当作模型权重大小或单任务内存。ASR Node Worker 和 OCR Web Worker 没有当前可独立归属的 OS 进程工作集;宿主进程总量不能直接填入某个服务行。未来 OCR 后台宿主也必须先核实 PID 与共享情况,再决定指标范围。显存当前未采集。
## 优化待办与验收
优先级表示建议处理顺序:P1 为执行正确性和回收问题,P2 为测量及有条件的架构选型。全部待办待开展;源码核对已完成,故障复现、修复和实机验收分别记录,不以文档完成替代实现完成。
| ID | 优先级与事项 | 依赖及验收条件 |
| --- | --- | --- |
| TC-1 | P1:embedding 取消/超时后的有界回收 | 先覆盖 `session.run` 延迟返回及不返回,观察 caller、pending、执行记录与容量。定义取消后的清理等待上限及升级条件,数值依据测量确定。普通取消不终止其他共享任务;确需服务级终止时明确受影响使用方及失败结果,等实际退出再释放容量,且不重放请求。模拟用于稳定触发条件,最终以真实引擎取消和停止路径补验 |
| TC-2 | P1:embedding 加载失败重试 | 覆盖首次读模型或建 session 失败、随后恢复依赖的场景;选择清除失败缓存或显式重建引擎的最小方案。验收后续请求能够重新加载,并发请求不重复加载,失败中已创建的资源被清理,销毁后不再复活;不能把一次成功创建进程当作重试通过 |
| TC-3 | P1:OCR 宿主就绪、丢失与超时预算 | 保留当前 Web Worker 即可先验证,无须等待迁移。覆盖监听安装前派发、主 Renderer 重载/崩溃、冷启动超过 Main 启动预算及排队取消。明确 bridge 就绪与 Worker ready 的区别;受影响请求有明确结果,队列能继续使用,没有静默丢失或重放;两端预算、起算点和取消后下一项派发与清理顺序一致 |
| TC-4 | P1:ASR 结果与清理记账 | 覆盖结果先到、延迟退出、清理异常以及取消/超时与结果竞争。选择延后请求结算或单独保留清理记录的最小方案;验收资源仍活动时不会被报告为已释放,也不提前放开实际单请求容量。正常完成及异常路径均验证 Worker 最终退出 |
| TC-5 | P2:统一测量记录与资源归属核验 | 可与 P1 复现并行,为 TC-6/TC-7 提供依据。记录冷/暖加载时间、端到端延迟、排队时长、取消至执行结束/资源回收时长、空闲与峰值工作集、CPU 时间和样本数;按 PID 对照 OS 工具,分别报告引擎、宿主与应用总量。重启后不沿用旧 PID 基线,不以 5 秒 UI 轮询代替专门测量 |
| TC-6 | P2、条件待定:OCR 后台 Renderer 加 Worker | 依赖明确的 UI 独立执行需求、TC-3 生命周期结论及 TC-5 数据。候选验证需包含图片、多页 PDF、UI 重载时执行、后台宿主崩溃、空闲释放、应用退出、preload/IPC sender 边界和各支持平台打包资源。证实任务连续性及实际 PID 隔离后,再记录采用或保留现状的结论 |
| TC-7 | P2、条件待定:ASR 常驻 utilityProcess | 依赖流式/高频场景成立,或 TC-5 证明重复加载达到需优化程度,并完成 TC-4。对比请求级 Worker 与候选的冷启动、暖请求、空闲内存和取消成本;若为流式,验证连续音频、结果提交和停止语义。没有测量或实际需求时继续保留请求级 Worker |
测量记录应包含 OS/架构、Electron 与依赖版本、模型版本、硬件、输入时长/页数/token 与批量大小、冷暖条件、请求频率和并发使用方。使用固定可重复输入,报告样本数、分布及异常,不填入未经测量的性能收益。后续验收覆盖受影响的 Windows、macOS、Linux 构建;尚未测的平台明确保留为待验。
本次证据为源码和已安装依赖入口检查。后续执行待办时,在所属功能进度文档记录命令、环境、真实路径结果及遗留项,并引用 TC 编号;若迁移条件未成立,记录保留现状的原因即可。
+125 -110
View File
@@ -10,10 +10,10 @@
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../prd/document-processing/document-extraction-and-local-ocr.md)、[本地文本向量模型与连接设计](./local-text-embedding-model-design.md)、[全双工实时语音交互设计](./full-duplex-voice-design.md) |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../features/document-processing/prd.md)、[本地文本向量模型与连接设计](../features/knowledge-base/local-embedding-technical-design.md)、[全双工实时语音交互设计](../features/full-duplex-voice/technical-design.md) |
本文定义“设置 → 平台功能”的二级页签结构,以及所有 GoodBuddy 托管本地模型共同使用的
“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
本文定义“设置 → 平台功能”的二级页签结构,以及位于“设置 → 模型连接”、所有 GoodBuddy
托管本地模型共同使用的“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
模型下载源是用户明确选择的供应链路径。当前来源不可用或没有所选模型时,GoodBuddy
必须明确失败并提供设置入口,**不能静默切换到另一个下载源。**
@@ -24,8 +24,9 @@
1. “平台功能”保留现有一级设置分类,在分类内部增加共享 `PageTabs`:
- **通用设置**
- **魔法笔记**
2. “通用设置”首个功能为“模型下载源”。
- **远程项目**
2. “模型下载源”位于“模型连接”页头,与模型类型页签同一行右对齐,在保存操作下方;
显示当前来源和次按钮,点击后在弹窗中选择并确认保存。
3. 首期提供两个互斥选项:
- `modelscope`,用户文案为 **ModelScope**,默认值。
- `hugging-face`,用户文案为 **Hugging Face**。
@@ -41,15 +42,15 @@
- 云端模型调用。
- LLM Provider、Runtime、Skills、MCP 或扩展下载。
- GoodBuddy 应用自身的“检查更新源”。
6. 每个模型文件继续固定 Revision、字节数和 SHA-256。两个来源只有提供完全相同的预期
文件时,才表示同一个模型工件。
6. 每个来源继续固定 Revision、字节数和 SHA-256。两个来源可以为同一模型 ID 提供字节
不同的工件,但必须分别声明并校验完整来源包,不能只登记一个未校验链接。
7. 一个模型在所选来源缺少任何必需文件时,整个模型在该来源标记“暂不可下载”。不能把
同一模型包的部分文件从 ModelScope 下载、部分文件从 Hugging Face 下载。
8. 下载任务启动时冻结来源。任务运行期间切换全局设置,不改变、重启或迁移该任务;新的
下载使用新来源。
9. 当前来源失败时,不自动重试另一个来源。错误可以提供“前往通用设置更换下载源”,由
用户明确选择。
10. 现有魔法笔记设置只移动到“魔法笔记”页签,不改变保存、启用或评论行为。
9. 当前来源失败时,不自动重试另一个来源。向量、语音和 OCR 的更换下载源入口直接打开
“模型连接”页的下载源弹窗,由用户明确选择并确认。
10. 魔法笔记设置由应用中心与笔记页共享,不在全局设置中重复提供入口。
---
@@ -59,10 +60,11 @@
“平台功能”现已使用两个二级页签:
- “通用设置”承载全局模型下载源。
- “魔法笔记”承载显示入口、AI 评论方式和 AI 评论形式。
- “通用设置”承载快捷唤起、会话与通知、工作目录设置。
- “远程项目”承载远程项目开关与 Agent 包管理。
页签复用共享 `PageTabs`,默认打开“通用设置”,不会把当前页签写入应用设置。
全局模型下载源由“模型连接”页头提供入口,布局和弹窗行为见第 7 节。
### 2.2 受管模型下载
@@ -72,7 +74,7 @@
两者均已接入全局模型下载源,并具备:
- Main 进程下载。
- 来源无关的固定文件名、字节数和 SHA-256。
- 固定文件名与角色,以及可按来源覆盖的字节数和 SHA-256。
- ModelScope 或 Hugging Face 的固定 Revision 与下载 Target。
- 随机暂存目录。
- 下载进度和取消。
@@ -99,7 +101,7 @@
- 切换来源时不影响已安装、已选择或正在使用的模型。
- 下载失败时知道实际使用了哪个来源。
- 所选来源没有模型时得到明确说明,并可以主动前往设置切换。
- 在“平台功能”中快速区分通用设置与魔法笔记。
- 在“模型连接”页头打开模型下载源弹窗,选择后确认保存。
### 3.2 产品目标
@@ -146,8 +148,8 @@
| 模型下载源 | 用户选择的 ModelScope 或 Hugging Face |
| 模型目录 | GoodBuddy 源码中经过验证的模型元数据集合 |
| 模型包 | 一个模型运行所需的全部必需文件和安装清单 |
| 下载目标 | 某个来源中一个固定模型文件的 URL 和仓库信息 |
| 规范工件 | 由文件名、字节数和 SHA-256 定义的来源无关文件 |
| 下载目标 | 某个来源中一个固定模型文件的 URL、仓库、Revision 和可选来源专用指纹 |
| 规范工件 | 目录用于旧安装、本地目录和 ZIP 兼容的默认文件指纹 |
| 来源覆盖 | 一个来源是否为模型包的全部必需文件提供下载目标 |
| 冻结来源 | 下载操作启动时记录且在操作期间不变化的来源 |
| 来源回退 | 一个来源失败后由应用自动请求另一个来源 |
@@ -163,13 +165,15 @@
```text
设置
├─ 模型连接
│ └─ 页头:模型类型页签同行右侧的当前来源与“模型下载源”按钮
│ └─ 模型下载源弹窗
└─ 平台功能
├─ 通用设置
│ └─ 模型下载源
└─ 魔法笔记
├─ 显示魔法笔记入口
├─ AI 评论方式
└─ AI 评论形式
│ └─ 快捷唤起、会话与通知、工作目录
└─ 远程项目
├─ 远程项目开关
└─ Agent 包管理
```
分类页只有一个一级标题“平台功能”。页签标题不重复渲染为第二个页面标题;每个面板内部
@@ -186,7 +190,7 @@
onChange={setActiveSection}
tabs={[
{ id: 'general', label: '通用设置' },
{ id: 'magic-notes', label: '魔法笔记' }
{ id: 'remote-projects', label: '远程项目' }
]}
value={activeSection}
variant="segmented"
@@ -214,7 +218,7 @@
未来新增页签时:
- 必须与“通用设置”和“魔法笔记”处于同级。
- 必须与“通用设置”和“远程项目”处于同级。
- 页签表示完整、独立的功能设置域。
- 通用设置只放跨功能的全局行为,不成为任意设置的杂物区。
- 页签超过可读数量时重新组织信息架构,不允许多行堆叠。
@@ -222,18 +226,20 @@
---
## 7. 通用设置界面
## 7. 模型下载源界面
### 7.1 布局
“模型连接”页头在保存操作下方,将模型类型页签与下载源控件放在同一行:页签居左,
“当前来源:ModelScope”和“模型下载源”次按钮整体右对齐。窄窗口保持同行,空间不足时
导航行可横向滚动。点击按钮打开弹窗;通用设置的工作目录卡片不再提供下载源控件。共享布局规则见
[UI 设计规范](../../UI-DESIGN.md#138-设置中心)。
```text
平台功能
管理通用平台行为与可选工作区能力
模型连接 [保存]
[文本] [向量] [重排] [语音] 当前来源:ModelScope [模型下载源]
[ 通用设置 ] [ 魔法笔记 ]
┌ 本地模型 ──────────────────────────────────────────────┐
│ 模型下载源 │
┌ 模型下载源(弹窗)───────────────────────────────── [×]┐
│ 选择 GoodBuddy 托管本地模型后续下载使用的平台。 │
│ 已安装模型和 ZIP 导入不受影响。 │
│ │
@@ -242,20 +248,19 @@
│ │
│ ○ Hugging Face │
│ 适合可以稳定访问 Hugging Face 的网络环境 │
│ │
│ 当前选择:ModelScope │
│ [取消] [确认] │
└────────────────────────────────────────────────────────┘
```
页面只保留一个视觉上最突出的任务。模型下载源保存属于即时设置,不增加与来源选项竞争的
大型“保存全部”按钮。
弹窗通过 Portal 挂载到 `document.body`,复用共享 Modal 样式、背景隔离和焦点管理。
模型连接页头的保存操作与下载源弹窗的“确认”各自保存对应设置。
### 7.2 控件
来源选项包含说明和未来可能的不可用原因,因此使用语义化 Radio Group,而不是原生
`select` 或 `SegmentedControl`:
- `fieldset` + `legend` 表达“模型下载源”。
- 弹窗内使用 `fieldset` + 持久的屏幕阅读器 `legend` 表达“模型下载源”。
- 每个选项使用 Radio 和整行可点击卡片。
- 当前项同时显示选中 Radio、强调边框和选中背景。
- 不能只用站点 Logo 或颜色表达选择。
@@ -284,12 +289,16 @@
### 7.4 保存交互
- 用户选择另一个 Radio 后立即调用应用设置更新。
- 保存期间禁用两个选项,保留最后确认值。
- 成功后更新当前选择,并通过应用通知提示:
- 打开弹窗时以已保存来源初始化草稿;选择 Radio 只修改草稿,不调用应用设置更新。
- 点击“确认”才保存;草稿与已保存来源相同时直接关闭,不重复写入。
- 取消、关闭按钮、Escape 或点击遮罩均丢弃草稿,已保存来源不变。
- 保存期间禁用选项、确认、取消和关闭,阻止重复提交及遮罩、Escape 关闭。
- 成功后更新页头当前来源并关闭弹窗,通过应用通知提示:
`模型下载源已切换为 Hugging Face。`
- 失败时恢复最后确认值,在 Radio Group 附近显示可重试错误。
- 保存失败时保留弹窗和草稿,已保存来源不变;就地显示错误,允许再次点击“确认”重试。
- 读取失败时显示错误和“重试”,禁用确认,不以默认值伪装为已保存来源。
- 同一个失败不再同时显示页面横幅和全局错误通知。
- 弹窗打开后聚焦关闭按钮,Tab 在弹窗内循环;关闭后焦点回到页头“模型下载源”按钮。
来源变化不触发:
@@ -299,39 +308,18 @@
- 当前模型切换。
- 活动下载取消。
### 7.5 活动下载说明
### 7.5 活动下载
来源选项下方持续说明:
> 正在进行的模型下载会继续使用启动时的来源;新的下载使用当前选择。
首期不为这段说明新增跨语音、OCR 和向量管理器的活动任务聚合 IPC。具体活动操作继续在
对应模型卡显示冻结来源、进度和取消入口。
不常驻展示活动下载说明。具体下载操作在对应模型卡显示进度和取消入口;需要显示来源时
必须使用任务启动时冻结的来源。切换全局来源不改变已经开始的操作。
---
## 8. 魔法笔记页签
## 8. 魔法笔记设置边界
现有魔法笔记卡完整移动到“魔法笔记”面板:
- 显示魔法笔记入口。
- AI 评论方式。
- AI 评论形式。
保持以下行为不变:
- 开关继续使用共享 Switch 和 `role="switch"`。
- 评论方式和形式继续使用 `SegmentedControl`。
- 每项继续即时保存。
- 保存失败保留最后确认设置。
- `onMagicNotesEnabledChange` 继续更新应用导航入口。
页签重构不能:
- 重置现有设置。
- 在切换页签时保存或改变值。
- 因魔法笔记入口关闭而隐藏“魔法笔记”设置页签。
- 把页签选择误当成启用开关。
全局设置不再提供魔法笔记页签或跳转入口。应用中心与笔记页继续共享同一表单和配置,
保留已有启用、待办数量显示、评论方式、评论形式及业务数据。共同设置的交互与同步规则见
[应用导航 UI 设计](../features/application-tool-navigation/ui-design.md)。
---
@@ -420,10 +408,11 @@ type StoredApplicationSettingsV7 = {
## 10. 模型目录契约
### 10.1 来源无关的规范工件
### 10.1 规范工件与来源专用指纹
当前每个文件把 URL、大小和 SHA-256 放在同一个 `download` 对象中。新契约把工件身份与
下载目标拆开:
每个文件保留一组默认大小和 SHA-256,用于兼容已有安装、本地目录和 ZIP。下载目标固定
来源、仓库和 Revision;当某个来源发布的是同一模型 ID 的另一套字节时,Target 可以同时
声明该来源自己的大小和 SHA-256:
```ts
type ModelArtifactTarget = {
@@ -431,6 +420,8 @@ type ModelArtifactTarget = {
repositoryUrl: string
revision: string
redirectHosts?: string[]
size?: number
sha256?: string
}
type ModelArtifactFile = {
@@ -450,14 +441,17 @@ type ManagedModelCatalogEntry = {
}
```
`size` 和 `sha256` 位于来源外层,表示两个来源必须提供同一规范工件。
Target 省略 `size` 和 `sha256` 时继承文件默认指纹;覆盖时两者必须同时出现。下载解析器
始终冻结一个来源,并使用该来源每个 Target 的有效指纹计算总大小和完成下载校验。
如果两个站点提供的文件不是完全相同的字节:
目录因此可以表达:
- 不能把它们放入同一文件的两个 Target。
- 不能为了“兼容”使用两个 SHA-256。
- 应创建不同模型工件版本、Variant 或不同模型 ID。
- 每个 Variant 分别经过运行时和质量验证。
- 两个来源提供字节完全一致的镜像;
- 两个来源提供文件名和运行角色相同、但大小或 SHA-256 不同的版本;
- 只有一个来源提供完整模型包。
来源差异不能削弱完整性校验。只提供 URL 而没有默认或来源专用指纹、只覆盖大小或
SHA-256 之一、使用可变 Revision,都会在目录解析时被拒绝。
Renderer 使用的目录快照不暴露下载 URL、重定向 Host 或文件存储地址,只返回来源可用性:
@@ -493,7 +487,7 @@ type ManagedModelSnapshot = {
1. 每个必需文件都有该来源的 Target。
2. 每个 Target 使用固定 Revision,不使用 `main`、`latest` 或可变 Tag。
3. URL、仓库 URL 和重定向 Host 通过 Schema 和来源策略校验。
4. 文件大小和 SHA-256 已独立验证。
4. 每个文件都有默认指纹或该来源专用指纹,并在下载时按解析后的大小和 SHA-256 校验。
只要缺少一个文件,该模型在该来源整体不可下载。
@@ -523,6 +517,7 @@ dictionary.yml ← ModelScope
- 模型 ID 唯一。
- 文件名和角色唯一。
- 每个来源 Target 的 URL 和 Revision 有效。
- 来源专用大小和 SHA-256 必须成对出现。
- 至少一个来源完整覆盖可下载模型。
- 非手动模型不得在两个来源都缺失。
- `repositoryUrl` 与下载 Target 属于同一声明来源。
@@ -539,15 +534,18 @@ dictionary.yml ← ModelScope
| PP-OCRv6 Tiny / Small / Medium | 可下载 | 可下载 |
| SenseVoiceSmall INT8 | 可下载 | 可下载 |
| Whisper Tiny | 可下载 | 可下载 |
| Paraformer 中英双语 / 中英日三语 | 暂不可下载 | 可下载 |
| Whisper Small / Medium | 暂不可下载 | 可下载 |
| Paraformer 中英双语 | 可下载 | 可下载 |
| Paraformer 中粤英三语 | 可下载 | 可下载 |
| Whisper Small / Medium | 可下载 | 可下载 |
OCR 模型包的检测模型与识别模型来自同一下载源中的不同上游仓库。因此每个 Target 都必须
符合所声明来源的 Host 策略,而模型卡的主仓库入口必须对应到该来源至少一个实际文件
Target。语音模型当前每个模型包的 Target 与主仓库入口保持一致。
未找到可独立验证且字节完全一致的 Target 时,目录保留部分覆盖,不通过替换模型、
改写摘要或请求另一个来源来伪造双来源支持。
当前六个语音模型均已登记 ModelScope 和 Hugging Face 固定 Target。Paraformer 中英双语
在两个来源使用不同的已验证文件指纹;其余语音模型当前使用字节一致的镜像。来源专用
版本不会触发自动回退,也不会允许一次安装混合两个来源。其 ModelScope 版本还必须通过
当前捆绑 `sherpa-onnx` 的模型加载和中英样例转写探测,不能仅凭下载 URL 可访问就入库。
---
@@ -641,12 +639,12 @@ resolveModelDownloadPackage(files, source)
### 12.1 来源状态
语音、OCR、向量和后续模型卡统一显示:
语音和 OCR 的当前模型详情各保留一个全局下载源标签。向量内置连接不重复显示模型连接页头中
已经明确的全局来源,也不显示固定本地安装路径;下载仍使用已保存的全局来源。
- 当前全局下载源。
- 当前模型在该来源是否可下载。
- 活动操作的冻结来源。
- 已安装模型的安装和校验状态。
所有模型管理界面都显示当前模型是否可下载、活动操作的进度与取消入口,以及已安装模型
的安装和校验状态。活动操作需要显示来源时,使用任务启动时冻结的来源,不能读取后来
切换的全局值。
未安装且来源可用:
@@ -659,10 +657,12 @@ resolveModelDownloadPackage(files, source)
```text
Hugging Face 暂不提供此模型的完整已验证文件。
[前往通用设置]
[模型下载源]
```
不能显示可点击“下载”后才告诉用户缺少某个文件。
向量、语音和 OCR 的“模型下载源”入口均导航到“模型连接”并直接打开同一弹窗。
跨分类导航沿用设置中心的未保存修改确认,用户取消离开时保留原表单。
### 12.2 已安装模型
@@ -671,6 +671,7 @@ Hugging Face 暂不提供此模型的完整已验证文件。
- 切换来源后继续可用。
- 不重新验证网络地址。
- 继续按安装 Manifest 中的大小和 SHA-256 验证。
- Manifest 必须完整匹配目录中的默认包或任一来源包,不能逐文件拼接不同来源版本。
- 不自动重新下载。
- 不改变当前模型选择。
@@ -693,6 +694,7 @@ type InstalledModelProvenance = {
ZIP 是来源无关的离线迁移方式:
- 导入始终按模型 ID、文件名、大小和 SHA-256 验证。
- 整个 ZIP 必须匹配默认包或某一个完整来源包,不接受逐文件跨来源组合。
- 当前下载源不参与导入兼容判断。
- 从 ModelScope 下载的模型可以在选择 Hugging Face 的设备上导入。
- 导出可以保留原安装来源作为审计元数据,但不能限制另一设备导入。
@@ -705,7 +707,7 @@ ZIP 是来源无关的离线迁移方式:
- `正在从 {{source}} 下载`
- `打开 {{source}} 模型仓库`
- `当前下载源暂不提供此模型`
- `前往通用设置更换下载源`
- `模型下载源`(打开下载源弹窗)
不再写死:
@@ -751,7 +753,7 @@ ZIP 是来源无关的离线迁移方式:
失败状态提供:
1. 重试当前来源。
2. 前往“平台功能 → 通用设置”。
2. 打开“模型连接”的模型下载源弹窗,选择并确认保存。
3. 取消。
用户切换来源后需要重新点击下载。不能在设置保存后自动恢复上一失败任务。
@@ -780,6 +782,9 @@ ModelScope 和 Hugging Face 都可能跳转到文件存储或 CDN。每个来源
- 不允许通配任意公网 Host。
- 最终内容仍按固定大小和 SHA-256 验证。
当前 ModelScope 大文件只允许跳转到已核验的
`cdn-lfs-cn-1.modelscope.cn`;小文件可以由 `modelscope.cn` 直接返回。
重定向到已声明 CDN 属于同一下载源内部传输,不构成下载源切换。UI 继续显示用户选择的
逻辑来源。
@@ -815,7 +820,7 @@ ModelScope 和 Hugging Face 都可能跳转到文件存储或 CDN。每个来源
| 设置 | 位置 | 作用 |
| --- | --- | --- |
| 模型下载源 | 平台功能 → 通用设置 | GoodBuddy 托管本地模型 |
| 模型下载源 | 模型连接页头 → 模型下载源弹窗 | GoodBuddy 托管本地模型 |
| 检查更新源 | 关于与更新 | GoodBuddy 版本检查和下载页 |
切换任一设置不得修改另一个设置。用户选择 ModelScope 不表示应用更新使用镜像节点;选择
@@ -837,7 +842,7 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
### 15.4 模型运行
模型下载源只决定获取规范工件的位置,不进入:
模型下载源只决定本次获取哪一个已登记来源包,不进入:
- 推理配置。
- 向量 Provider Fingerprint。
@@ -845,8 +850,9 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- OCR 模型选择。
- 模型质量或速度标签。
只要最终模型文件 SHA-256 相同,从 ModelScope 或 Hugging Face 安装后运行语义必须完全
一致。
安装后运行只依赖 Manifest 与磁盘文件匹配一个完整目录包,不依赖当前全局下载源。不同
来源可以具有不同 SHA-256;目录中的模型族、语言、质量、速度和许可证说明必须准确覆盖
所登记的来源版本。
---
@@ -854,9 +860,9 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
| 场景 | 行为 |
| --- | --- |
| 读取设置失败 | 显示平台功能阻塞错误,不猜测或写入来源 |
| 保存来源失败 | 保留上一个确认值,显示就地错误 |
| 当前来源无完整模型包 | 下载按钮禁用,显示前往通用设置 |
| 读取设置失败 | 页头显示读取失败,弹窗提供就地重试并禁用确认,不猜测或写入来源 |
| 保存来源失败 | 已保存来源不变,保留弹窗和草稿,显示就地错误并允许确认重试 |
| 当前来源无完整模型包 | 下载按钮禁用,提供直接打开模型连接页下载源弹窗的入口 |
| 当前来源网络不可用 | 当前下载失败或允许同来源重试,不请求另一来源 |
| 当前来源返回 404 | 显示来源缺少固定文件,不请求另一来源 |
| 重定向 Host 不受信任 | 立即失败并删除本次暂存 |
@@ -884,22 +890,27 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- 更高未知版本拒绝,不覆盖文件。
- 并发设置更新保持完整 JSON。
### 17.2 平台功能界面
### 17.2 设置界面
- 默认选中“通用设置”Tab。
- Tab 使用 `tablist`、`tab`、`tabpanel` 和 `aria-selected`。
- 方向键、Home、End 和焦点恢复正确。
- 切换到魔法笔记不改变任何设置。
- 切换平台功能页签不改变任何设置,全局设置不显示魔法笔记入口。
- 模型连接页头的当前来源与次按钮位于模型类型页签同一行右侧、保存操作下方;通用设置不再显示下载源控件。
- Radio Group 具有持久 Label。
- 选择来源时保存一次,保存期间防止重复提交。
- 保存失败恢复原值并保留可重试错误。
- 选择 Radio 只更新草稿,确认后保存一次;未改变来源时确认不写入。
- 取消、关闭、Escape 和遮罩关闭不改变已保存来源;保存期间禁止关闭及重复提交。
- 保存失败保留草稿并允许确认重试;读取失败提供重试且禁用确认。
- 弹窗使用 body Portal、焦点循环和关闭后焦点恢复。
- 向量、语音和 OCR 的更换下载源入口直接打开模型连接页弹窗,跨分类时保留离开保护。
- 窄窗口不隐藏页签或来源名称。
### 17.3 目录
对语音、OCR 和向量目录分别验证:
- 两个来源解析到相同文件名、大小和 SHA-256。
- 两个来源分别解析到自己的固定文件名、大小和 SHA-256。
- Target 未覆盖指纹时继承默认值;只覆盖大小或 SHA-256 之一时拒绝。
- 缺少任一 Target 时模型在该来源不可下载。
- 不允许一次任务混合来源。
- 可变 Revision 被拒绝。
@@ -916,7 +927,9 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- 设置中途变化不改变活动任务。
- 新任务使用最新已保存来源。
- Renderer 提交过期来源时零网络请求。
- 两个来源的安装 Manifest 文件摘要相同。
- 两个来源字节不同时,安装 Manifest 分别记录实际来源包摘要。
- 旧默认指纹和任一完整来源指纹都能通过安装、ZIP 和运行时校验。
- 逐文件命中不同来源的混合包被拒绝。
- 取消、关闭、错误和摘要不匹配不留下正式模型目录。
- 快照准备会清理名称同时匹配受管模型 ID 与安装 UUID 的陈旧
`.install-*` 目录,以及受管文件名对应的孤立 `.partial` 文件;已安装模型、
@@ -940,15 +953,15 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- 新增 `ModelDownloadSource` Schema 和默认值。
- 将应用设置版本升级并迁移旧版本。
- 在“平台功能”中增加 `PageTabs`。
- 增加“通用设置”与模型下载源 Radio Group。
- 把现有魔法笔记卡移动到“魔法笔记”面板。
- “通用设置”保留平台行为;模型下载源已移至模型连接页头,通过弹窗 Radio 草稿和确认保存。
- 魔法笔记卡已迁入应用共同设置,全局页签已移除,见第 8 节。
### 阶段 2:共享目录和来源解析(已完成)
- 定义来源无关的规范工件与分来源 Target。
- 定义默认规范工件与可覆盖指纹的分来源 Target。
- 实现共享 `ModelDownloadTargetResolver`。
- 为语音和 OCR 目录补充 ModelScope 与 Hugging Face 固定 Target。
- 静态验证两个来源的文件大小和 SHA-256。
- 静态验证每个来源解析后的文件大小和 SHA-256。
- 更正来源感知文案和仓库入口。
### 阶段 3:模型管理器(已完成)
@@ -970,19 +983,21 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
### 19.1 产品
- “平台功能”显示“通用设置”和“魔法笔记”两个可访问页签。
- “平台功能”显示“通用设置”和“远程项目”两个可访问页签。
- 默认打开“通用设置”。
- “通用设置”提供 ModelScope 和 Hugging Face,默认 ModelScope。
- “模型连接”页头在模型类型页签同行右侧、保存操作下方显示当前来源及“模型下载源”次按钮。
- 下载源弹窗提供 ModelScope 和 Hugging Face,默认 ModelScope;Radio 只编辑草稿,确认才保存,取消不改变已保存来源。
- 读取与保存失败均可就地重试;向量、语音和 OCR 的更换下载源入口直接打开该弹窗。
- 来源选择说明影响范围,并明确已安装模型、ZIP、Ollama 和应用更新不受影响。
- 切换来源不会下载、删除、切换或重新验证任何已安装模型。
- 当前来源没有模型时,在点击下载前即可看到不可用原因。
- 下载失败不会自动请求另一来源。
- 魔法笔记的现有设置和入口行为保持不变。
- 魔法笔记设置由应用中心与笔记页共享,全局设置不再提供笔记入口;已有配置值和业务数据保持不变。
### 19.2 数据与下载
- 旧应用设置完整迁移,并得到 ModelScope 默认值。
- 每个双来源模型的规范文件名、大小和 SHA-256 相同。
- 每个双来源模型的文件名和角色一致;大小和 SHA-256 可以按来源固定。
- 单次下载任务只使用一个冻结来源。
- 活动下载显示实际冻结来源。
- 已安装 Manifest 和 ZIP 兼容性不依赖来源。
+186
View File
@@ -0,0 +1,186 @@
# GoodBuddy 性能原则
## 文档信息
- **适用范围:** 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. 性能预算
“流畅”用下面的数字判断,不靠感觉。“标准机”指记录了硬件环境、不降速运行的参考机;Renderer 降速场景使用 `GB_PERF_CPU_THROTTLE=4`。该降速仅作用于 Renderer,不等于真实低配整机,也不模拟慢 Main 或磁盘。预算适用于单场景及规定的并发负载。
| 指标 | 标准机 | 低端机 |
| --- | --- | --- |
| 按键到下一帧,p95 | ≤ 16 ms | ≤ 50 ms |
| 点击到界面响应(切换会话、打开面板),p95 | ≤ 100 ms | ≤ 300 ms |
| 稳定交互期间(打字、滚动、流式输出)超过 50 ms 的长任务 | 0 | 尽量为 0,总时长不超过场景时长的 5% |
| 流式输出、滚动时的帧间隔,p95 | ≤ 17 ms | ≤ 33 ms |
| Main 事件循环单次阻塞 | ≤ 16 ms | — |
| Main 事件循环延迟,p99 | ≤ 30 ms | — |
| DOM 节点数 | 不随会话数、消息数增长 | 同左 |
当前 `npm run perf:check` 使用历史回归上限,部分数值宽于本表,也未覆盖全部并发入口。检查通过不能宣称本表达标。目标交付须在验收报告中分别给出规范预算和回归上限的结果;低端机降速目前只做记录,真实低配和新增自动检查的完成状态另行记录。
点击反馈、所需内容可用和后台工作完成分别计时。显示加载状态只能证明及时反馈;不能用它替代结果就绪。每个进程分别报告长任务/事件循环和排队耗时,禁止把多个独立探针相加为端到端延迟。
## 2. 原则
### P1 Main 只做调度
Main 负责窗口、生命周期、权限和消息转发。数据库查询、文档解析、哈希、向量计算、批量文件读写,都放到 worker 线程或 `utilityProcess` 里执行。
- 新代码不能在 Main 里使用 `DatabaseSync` 和同步文件 API。lint 已有白名单,名单只能减少。
- 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
- 目标交付后 Main 不再直接或通过 helper 打开桌面业务 SQLite;只读、失败回退、维护及退出均在检查范围内。当前迁移例外不是新增同步工作的许可。存储宿主可继续使用同步 SQLite,事务和计算仍需有界。
### P2 界面状态局部订阅
状态按领域放进外部 store,组件通过 selector 只订阅自己用到的那一部分。
- 不再往 `App.tsx` 里加 `useState`、`useEffect` 或领域逻辑。`App.tsx` 有行数上限,只能往下调。
- IPC 订阅放在 store 模块里,组件不直接调用 `window.goodbuddy.*.on*`。
- 一个按键、一个流式片段,只能让显示它的那一小块界面重新渲染。
- 传给 memo 组件的回调和 context 值,引用必须稳定。
**模块约定(便于维护,所有拆分都按这个结构做):**
| 层 | 文件命名 | 内容 | 不允许 |
| --- | --- | --- | --- |
| Store | `<domain>-store.ts` | `create<Domain>Store()` 返回 `getState / subscribe / 领域动作`;纯 TS,不依赖 React;可单测 | 访问 DOM、调用 React hook |
| 选择器 / Hook | `<domain>-selectors.ts` 或 `use-<domain>.ts` | 基于 `useSyncExternalStore` 的 selector,返回值引用稳定 | 在 selector 里新建对象却不做相等比较 |
| 同步层 | `<domain>-sync.ts` | 与 Main 的 IPC:首次加载、订阅推送、增量保存队列、失败重试、退出前刷盘 | 放在组件里;整份数据回写 |
| 视图 | `<Domain>*.tsx` | `memo` 组件,只从 selector 和稳定的动作读数据 | 接收 App 的大对象或内联回调 |
- `App.tsx` 只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。
- 每个 store 的单元测试和 store 文件放在一起(`<domain>-store.test.ts`),覆盖顺序、失败重试和边界情况。
- 领域数据库访问集中在 repository,目标架构中由存储宿主执行;Main 的 IPC handler 只做校验并等待领域操作。受管只读 Worker 与远端 Agent 按各自明确的连接归属执行,不能按源码目录把它们误判为 Main 访问。
- 多项目/多会话流式更新只通知相关领域与实体;后台子任务不得让全部项目视图重渲染。隐藏视图保留草稿、选择和必要执行订阅,停止无关绘制与全量重读,切回时按已有摘要及增量恢复。
### P3 高频数据走专门通道,按帧合并
流式文本、终端输出、进度、滚动这类高频数据,不进全局状态。
- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
- 高频路径上不能读取布局(`getBoundingClientRect`、`scrollHeight` 等),布局读取要集中到每帧一次。
- 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
- 显示合并与持久化分开。可以合并进度或替换过时刷新,不能静默丢弃消息、工具结果或需确认的事件;多生产者缓冲只有在用户设置、外部协议、数据安全或实测故障证据要求时才设置容量,否则将负载交给所属 Host。
### P4 数据只有一份来源,只传变化部分
- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
- 写入合并成批次和事务,不按每个事件单独开一个事务。
- 一个数据库有一个业务写入所有者,完整事务不跨 IPC 拆成单条 SQL。Main 不保存第二份可写业务状态;依赖写入的读取等待对应提交,远程 ACK 等待所需事件与 checkpoint 提交成功。
- 缓存必须说明归属、失效和释放条件,不按浏览过的项目/会话无限积累。查询返回摘要、分页或所需详情,保持完整信息可访问,不用截断历史满足预算。
- GoodBuddy 自身的中转数据优先使用有界内存;已有完整记录或文件时复用引用,避免重复落盘。预览、最终回答与工具全文分别核实,不能以“已保存消息”推断全文已保存。需要超出内存保留的内容直接使用所属存储的唯一正文,分页读取同一内容,避免临时全文再复制到正式存储。必要文件及清理范围遵守[输出与临时资源设计](./desktop-storage-direction.md#output-and-temporary-resource-ownership)。
### P5 工作量和可见内容成正比
- 长列表只渲染可见部分,包括消息、会话、笔记、文件树和知识库列表。
- 解析和渲染的结果要缓存,比如 Markdown 语法树。内容没变,就不重新计算。
- 增量处理:只解析变化的部分。没有把握时,退回到慢但正确的完整处理,并记录在测试里。
### P6 启动只做首屏需要的工作
- 首屏之前只做显示应用壳和恢复上次界面所需的工作。
- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
- 后台模块在第一次使用时才加载。
- 冷启动、首次进入功能和暖返回分别验收;预加载只使用有界空闲工作,不能为每个项目预建进程、连接或整份历史缓存。进程 spawn 不代表服务就绪,首次可用反馈与数据实际可用分开记录。
### P7 先测量,再修改
- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
- 必测多项目、多会话、多子任务及导入/检索/回顾发布重叠场景,负载和采样按[并发验收](../quality/concurrent-desktop-acceptance.md)。记录 offered load、实际吞吐、排队和排空、资源高水位;不得降低输入速率、串行化夹具或丢事件来取得流畅结果。
- 基准使用固定版本、数据和环境。性能测量与真实模型功能验收分开;受发布构建限制时遵守发布手册,不以性能原则授权额外生产打包。
### P8 正确性优先于速度
- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
- 取消请求、执行结算和容量释放不是同一时刻;调用方停止等待不能提前释放仍运行的工作。取消一个会话或子任务不得杀死仍服务其他任务的共享宿主。已提交事务如实报告成功,不伪装成回滚;未确认写入不盲目重放。
### P10 体验不倒退
性能优化必须保留数据、结果和用户交互语义,不能用体验换速度:
- **动画和过渡保持原样。** 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入 `scrollTop`/`scrollTo` 的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。
- **只渲染可见部分(P5)时,用户状态不能丢。** 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过 `<details>` 或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。
- **只渲染可见部分时,基本操作要和完整列表一致:** 跨行选择和复制、滚动位置不因上方插入或删除而跳动(关闭原生 `overflow-anchor` 时要自己做锚定)、快速拖动滚动条时没有空白帧、渲染结果缓存让重新挂载的行不出现“加载中”。
- **合并和批处理不能增加交互延迟。** 第一条数据立即处理(leading edge),只合并之后同一帧内的数据。打字回显、终端回显不能多等一帧。
- **低频同步不能让用户看到旧数据。** 复制、导出、保存这类读取全局状态的操作,要先把待同步的数据刷进去。
- **体验要有测试。** 涉及动画、滚动、焦点的改动,需要真实 Electron 测试(逐帧采样)或单元测试,并确认测试在旧代码上会失败。
### P9 用工具守住规则
- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
- 规则只能收紧,白名单和上限只能往下调。
- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
- 检查直接 SQL 导入及已知间接打开函数/构造点,按实际宿主区分测试、存储与远端代码。白名单和上限需要与已提交基线比较,不能只靠注释约定;全局同步回退同样要被发现。
- 普通 CI 与性能执行环境分别提供功能和预算证据;新增并发与首次进入场景纳入候选必需检查,缺少运行环境或结果时保留验收缺口,不由单测通过推断达标。
### P11 并发隔离与有界工作
同一会话的事件顺序和同一数据库的提交顺序必须保持;不同项目、会话、子任务的模型等待、重查询与清理不因此共用一把长时间全局锁。
- Main 全局控制队列不得等待模型、网络、原生启动或耗时清理。必要登记和状态提交可短暂串行,等待在所属操作范围内完成。
- 重查询复用有界 Worker;必要写入不排在整个导入解析或模型请求之后。长事务先减少事务内无关计算,再测写入排队,不把阻塞简单搬进唯一存储队列。
- 复用现有池、批处理和队列,记录在途数、排队数及实际资源使用。只有有明确依据时才设置容量;没有依据时将负载交给所属 Host,不因内部数字拒绝用户工作。有实测饥饿才引入最小调度调整,不预建通用优先级框架。
- 控制和取消不等待新的执行槽位;如果外部或用户设置要求排队,按该语义执行。禁止静默丢失、无限重启或自动扩增进程;不能把未经证明的队列上限包装成稳定性。
- `ReadonlyQueryReader` 的生产 storage 模式不传 `maxPending`,因此请求直接透传给 storage host;其等待队列只服务显式有界的调用方和测试,不构成第二个 storage owner。
- 每个异步响应携带已有项目/会话/任务/操作归属;迟到响应不得覆盖新选择或新操作。归属检查先于改变共享请求代次、缓存或队列状态。
### P12 架构完整性与 KISS
一次性交付按最终职责边界验收,不以全面 Promise 化或局部热点迁出代替。目标范围内正常、后台、失败、重试、取消和退出路径均使用同一权威实现,清理原直接入口和回退。
保持现有技术栈、数据库文件及业务合同,复用已有资源机制。不得顺带新增分布式事务、持久 RPC receipt、通用恢复框架、每项目独立服务或产品审批模式。同步内存缓存读取可以保留,不把纯函数和每个 getter 远程化。设计新增机制必须指出当前失败、现有机制的不足与对应验证,不以“将来可能并发”代替证据。
## 3. 评审清单
提交性能相关改动,或者会影响热点路径的功能改动时,逐项确认:
1. Main 里有没有新增同步数据库调用、同步文件 IO,或者耗时可能超过 16 ms 的计算?
2. 有没有往 `App.tsx` 里加状态?一次按键或一个流式片段,会让多大范围的界面重新渲染?
3. 高频路径上有没有读取布局,有没有每次事件都提交状态?
4. 有没有整份数据回写,或者“收到通知后重新查全量”?
5. 列表的 DOM 数量会不会随数据量增长?
6. 有没有在启动阶段新增同步初始化?
7. 是否附上了 `perf:app`、`perf:main` 或 `perf:knowledge` 的前后对比?
8. 顺序、取消、持久化和恢复,有没有测试覆盖?
9. 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
10. 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
11. 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
12. 多项目/多会话/多子任务同时运行时,取消或清理一个请求是否拖住其他任务?队列和缓存是否持续增长?
13. 存储提交、远程 ACK、调用方返回和资源释放的顺序是否有真实路径及故障注入验证?
14. 是否删除目标范围内的旧直接访问和同步回退?新增进程或调度机制是否复用现有设施并有实际必要性?
15. 请求或输出是否产生非必要临时文件、重复全文?是否核实完整内容与预览的区别,并验证分页、引用释放、必要文件的创建失败清理及升级后的残留回收?
## 4. 当前差距
截至 2026-10-05,以下状态结合[存储升级实施记录](../quality/storage-upgrade-implementation-2026-10-05.md)和[并发验收规范](../quality/concurrent-desktop-acceptance.md)核对。表中“已实现”只表示代码或定向测试已有证据,不表示 PERF-15、PERF-16 或 C01-C07 已完成。
| 原则 | 差距 |
| --- | --- |
| P1 | Assistant、Knowledge、文件和 Runtime 生产域已由 `DesktopStorageClient` 接入 utility process;剩余缺口是全量间接访问扫描、异常/维护/退出路径核验,以及 Main 阻塞和存储排队的并发测量。文档解析、分块和大文件哈希仍在各自 worker。 |
| P2 | `App.tsx` 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App |
| 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)约束当前候选的剩余验证,不能作为已有实现或实测结果;上述第一期历史状态也不替代当前验收。
+247
View File
@@ -0,0 +1,247 @@
# 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
recovery. `SubagentProgressStorage` caches event encoding state, not task statistics.
## Runtime Read Design
Execution duration is persisted on running-state transitions in Main. Snapshots
read small indexed timing rows; they require no historical evidence query, worker
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
parallel query demand. New consumers should reuse the same lifecycle and read-only
rules, without introducing a general query protocol before a second use case exists.
## Renderer Scheduling
Hidden task panels use React `Activity` to preserve state while scheduling hidden
render work below visible updates. Panel factories run inside that boundary.
Terminal and browser panels retain their existing mounted effects and sessions.
Duration snapshots refresh on Main change notifications while the task center is
open. The renderer ticks locally for open segments and fetches fresh snapshots on
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,497 @@
# GoodBuddy 设备共享与 ShareServer 联邦网络总体设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-09 |
| 适用范围 | GoodBuddy 桌面端、ShareServer、网关间联邦协议 |
| 桌面端设计 | [设备发现与共享](../features/device-sharing/README.md) |
| 服务端设计 | [ShareServer](../../../goodbuddy-shareserver/docs/features/share-server/README.md) |
## 1. 文档职责
本文是设备共享网络的跨端权威设计,负责定义拓扑、术语、身份、能力清单、授权计算、任务
路由、能力分发、协议边界和共同验收标准。桌面端的产品与界面行为由“设备发现与共享”功能
文档负责,独立 Web 服务由 ShareServer 功能文档负责。
本文描述完整目标系统,不以交付阶段删减领域模型,也不把尚未实现的设计表述为当前能力。
## 2. 核心决策
### 2.1 两个独立产品面
1. **GoodBuddy Desktop** 在设置中提供“设备发现与共享”,默认使用局域网发现与直连,也可
由用户额外连接一个或多个 ShareServer。
2. **ShareServer** 是独立部署、独立升级的 Web 服务,提供组织、设备、能力、权限、更新、
联邦、中继、Office 编辑和审计管理。它不运行在 Electron Main 中,也不由桌面端代为启动。
### 2.2 能力优先,来源可见
用户日常选择的是能力,不是网络连接。设备、ShareServer 和组织作为能力来源与诊断入口。
任何调用都必须持续显示提供者、执行位置和数据去向,不能为了统一体验隐藏跨设备或跨组织
边界。
### 2.3 局域网是默认连接方式,网关是显式附加连接
- 桌面端没有网关配置时,局域网发现、配对、调用和本机共享仍可完整工作。
- 添加、删除或断开 ShareServer 不改变局域网配对关系。
- ShareServer 不接管本机局域网发现,也不能静默把局域网设备注册到组织。
- 同一桌面端可以连接多个 ShareServer;能力来源和授权按连接分别计算。
### 2.4 发现、被发现和开放能力是三个独立动作
- **发现设备**:浏览同一局域网中主动广播的 GoodBuddy 服务。
- **允许被发现**:广播本设备存在及配对入口。
- **开放能力**:向指定设备、用户组或组织发布可查看、调用或拉取的能力。
开启其中一项不能推导另外两项已开启。新安装默认不开放能力。
### 2.5 ShareServer 优先作为控制面
ShareServer 负责身份、目录、策略、任务路由、更新和审计。数据面优先在两个已认证端点之间
直接建立;网络条件不允许时,可按组织策略使用 ShareServer 中继。是否允许中继、正文是否
端到端加密、是否允许跨组织,由本次任务冻结的策略共同决定。
### 2.6 Office 编辑是 ShareServer 托管服务
ShareServer 可以把 ONLYOFFICE Docs、工作副本存储、保存回调和编辑会话作为一个可选部署
能力提供给 Desktop。Desktop 只连接用户已明确配置的 ShareServer,不直接配置 Document
Server,也不在本机启动服务。Office 文档内容会由 ShareServer 和编辑引擎处理,因此不能沿用
“控制面默认不读取正文”的数据面假设;创建编辑会话前必须明确显示服务端、数据去向和保留期。
编辑后的本地或 SSH 源文件仍由 Desktop 或远程 GoodBuddy Agent 持有和原子回写。ShareServer
是工作副本及服务端保存检查点的权威,不是源文件最终写入成功的权威。完整设计见
[Office 协同编辑](../features/office-document-editing/README.md)。
## 3. 术语
| 术语 | 定义 |
| --- | --- |
| `Device` | 安装 GoodBuddy 并拥有稳定设备身份的终端 |
| `Peer` | 与当前设备存在局域网配对关系的另一台设备 |
| `ShareServer` | 提供组织控制面和可选数据中继的独立服务 |
| `Organization` | ShareServer 中隔离成员、设备、能力与策略的管理范围 |
| `GatewayConnection` | 某台设备与一个 ShareServer 上特定组织身份的连接 |
| `Capability` | 可被查看、调用或分发的稳定能力标识 |
| `CapabilityProvider` | 实际持有并执行某项能力的设备或服务节点 |
| `Publication` | 能力提供者向某一范围发布的可见性和使用声明 |
| `Grant` | 主体对能力执行某类动作的明确授权 |
| `Task` | 一次远程能力调用及其输入、执行、输出和终态 |
| `Package` | 可下载到本机安装的签名能力工件 |
| `FederationTrust` | 两个组织网关之间经管理员确认的双边信任关系 |
| `DocumentEditSession` | ShareServer 托管的单文档短期编辑会话、工作副本和保存检查点 |
“网关”是 ShareServer 的网络角色;产品名称为 `ShareServer`,独立仓库为
`goodbuddy-shareserver`。
## 4. 系统拓扑
```text
局域网:
GoodBuddy Desktop A <-- mDNS 发现 + 配对后 TLS 直连 --> GoodBuddy Desktop B
单组织:
GoodBuddy Desktop A -- 出站连接 --> ShareServer <-- 出站连接 -- Provider Device B
Office 编辑:
GoodBuddy Desktop --> ShareServer --> ONLYOFFICE Docs
└-------> 临时工作副本与保存检查点
跨组织:
Desktop A --> ShareServer A <== 联邦信任 ==> ShareServer B <-- Provider Device B
```
局域网和网关连接共享能力、授权和任务的领域语义,但不共享发现方式:
| 维度 | 局域网 | ShareServer |
| --- | --- | --- |
| 发现来源 | mDNS/DNS-SD 主动广播 | 已配置 URL 或邀请链接 |
| 初始身份 | 双端验证码确认设备指纹 | 用户登录、设备注册和组织成员身份 |
| 能力目录 | 已配对设备直接返回 | 服务端按组织策略过滤 |
| 连接方向 | 配对后直连 | 设备主动建立出站连接 |
| 管理范围 | 单个设备间授权 | 用户组、组织和联邦授权 |
| 审计权威 | 两端各自本地记录 | 组织网关记录元数据,两端保留本地记录 |
## 5. 组件边界
### 5.1 GoodBuddy Desktop
- 浏览和广播局域网服务。
- 创建、确认和撤销点对点配对。
- 连接 ShareServer,保存脱敏连接信息和本机受保护凭据。
- 发布用户明确选择的本机能力。
- 展示经当前身份过滤的能力目录。
- 在调用前冻结提供者、输入范围、授权和路由。
- 执行本机提供的任务,或消费远端任务结果。
- 校验并安装用户明确选择的签名能力包。
- 保存本机活动记录和可撤销授权。
### 5.2 ShareServer
- 认证用户、设备、服务节点和其他 ShareServer。
- 管理组织、成员、用户组和设备目录。
- 接收能力发布并生成权限过滤后的目录。
- 计算组织强制策略,签发短期任务授权。
- 路由任务、维护在线状态,并在允许时中继数据。
- 管理签名包、发布通道、更新策略和撤销。
- 可选管理 Office 编辑会话、ONLYOFFICE 接入、工作副本、保存回调、保留和清理。
- 建立双边联邦关系并限制跨组织可见性与调用。
- 保存有界审计元数据、用量和管理操作。
### 5.3 能力提供者
- 只执行已发布且本次授权允许的能力。
- 在执行边界重新验证任务令牌、输入 schema、大小、期限和调用方。
- 使用自身 Runtime、账号、工作区和数据权限。
- 返回有界进度、结果、成果或确定终态。
- 不因收到任务令牌而获得其他能力或任意系统接口。
## 6. 身份与信任
### 6.1 设备身份
每次 GoodBuddy 安装生成稳定设备密钥和自签设备证书,私钥保存在系统安全存储中。对外只
使用随机设备 ID、公钥指纹、用户设置的设备名称和必要协议版本。操作系统用户名、绝对路径、
软件资产和能力详情不进入未认证发现广播。
重装或主动重置设备身份会产生新设备。旧局域网配对和 ShareServer 设备注册不会自动迁移。
### 6.2 局域网配对
1. 发起端选择一台主动广播的设备。
2. 双方建立临时加密通道并计算共同短验证码。
3. 两端同时显示设备名、系统类型、公钥指纹和验证码。
4. 双方用户确认后保存对端公钥及配对 ID。
5. 后续使用双方设备证书认证;任一端可以撤销配对。
IP 地址不作为设备身份,也不单独构成授权依据。
### 6.3 ShareServer 注册
用户通过浏览器/OIDC 或服务端支持的登录方式认证后,为当前设备申请一次性注册挑战。设备
使用自身私钥签名挑战,ShareServer 建立 `user + organization + device` 绑定并签发可轮换的
设备凭据。设备凭据不等同于用户 Web 会话,不可用于管理控制台登录。
### 6.4 联邦身份
两个 ShareServer 由双方管理员交换组织标识、网关公钥、公开端点和指纹,并分别确认信任。
联邦关系只有在双方状态均为 `active` 时生效。单方暂停或撤销后立即停止新目录交换和新任务,
已执行任务保留审计终态。
## 7. 能力模型
### 7.1 能力清单
```json
{
"schemaVersion": "1.0",
"capabilityId": "document.convert.docx",
"name": "文档转 Word",
"version": "1.4.0",
"modes": ["remote", "package"],
"inputSchema": {},
"outputSchema": {},
"permissions": ["receive_text", "receive_files", "return_files"],
"limits": {
"maxInputBytes": 10485760,
"maxOutputBytes": 52428800,
"maxDurationSeconds": 900,
"maxConcurrentTasks": 2
},
"dataPolicy": {
"retention": "task_lifetime",
"networkAccess": false,
"delegation": "none"
}
}
```
清单只描述能力事实,不授予任何主体访问权。`inputSchema` 和 `outputSchema` 使用受限 JSON
Schema 子集;实现必须限制 schema 深度、属性数、枚举数和总字节,不能把清单当作可执行代码。
### 7.2 发布声明
`Publication` 将一个能力版本发布到以下一种范围:
- 指定局域网 Peer。
- 所有已配对 Peer。
- ShareServer 内指定用户、用户组或设备组。
- 指定联邦组织中的用户组。
发布声明同时定义允许动作:`discover`、`inspect`、`invoke`、`download`。`inspect` 允许查看
详细清单,`discover` 只允许看到名称和可用状态。
### 7.3 可调用与可分发
- `remote`:代码与受控数据留在提供方,调用方只发送输入并接收结果。
- `package`:签名工件下载到调用方,经校验和用户/组织策略批准后安装。
同一能力可以同时支持两种模式,但两种模式分别授权。远程调用授权不能推导下载权,下载权
也不能推导安装后自动启用。
## 8. 授权模型
### 8.1 授权输入
一次动作的最终决定取以下约束交集:
```text
调用方用户权限
∩ 调用方设备状态
∩ 调用方组织强制策略
∩ 发布范围
∩ 提供方授权
∩ 提供方组织强制策略
∩ 能力输入与数据策略
∩ 联邦关系限制(跨组织时)
= allow | require_approval | deny
```
`deny` 优先于 `require_approval`,`require_approval` 优先于 `allow`。客户端偏好只能在组织强制
策略允许的范围内收紧权限,不能放宽权限。
### 8.2 Grant
Grant 至少绑定:
- 主体:用户、用户组、设备、设备组或外部组织。
- 能力 ID 和允许的版本范围。
- 动作:查看、调用、下载、安装或发布。
- 输入:文本、文件、数据分类、单项和累计大小。
- 提供者和执行位置限制。
- 次数、并发、预算、时间段和有效期。
- 是否需要每次批准。
- 是否允许委托及明确的下游组织。
“始终允许”只创建与当前能力、提供者、输入类型和范围匹配的 Grant,不创建整台设备的任意
访问权。
### 8.3 转委托
默认 `delegation=none`。允许委托时,任务授权中必须冻结完整执行链和最大跳数。任一新组织、
新能力或新数据接收方加入执行链,都需要重新计算授权,不能沿用原任务令牌。
## 9. 任务协议
### 9.1 任务快照
创建任务时冻结:
- 调用方用户、设备和组织。
- 能力 ID、版本和提供者。
- 输入 schema 版本、数据类型、数据分类和字节数。
- 路由:局域网直连、网关直连、网关中继或联邦路径。
- 授权决定、批准记录和策略版本。
- 委托链、预算、期限和幂等键。
设置或策略后续变化不追改已接受任务;紧急撤销可以阻止尚未开始的任务,并按策略取消活动
任务。
### 9.2 状态
```text
created -> awaiting_approval -> queued -> connecting -> running
running -> completed | failed | cancelled | outcome_unknown
created/awaiting_approval/queued/connecting -> rejected | expired | cancelled
```
- `completed` 只在提供者提交完整终态且调用方确认接收后成立。
- 网络中断后无法确认副作用结果时使用 `outcome_unknown`,不得自动重放。
- 重试创建新 attempt,并复用仅在能力声明支持时才有效的幂等键。
- 用户取消必须传播到实际提供者;未确认取消时不能声称任务已停止。
### 9.3 数据传输
- 小型 JSON 输入使用任务通道传输。
- 文件使用内容 SHA-256、声明大小和一次性任务对象令牌传输。
- 接收端边读边校验大小和摘要,拒绝路径、符号链接和调用方文件名决定的落盘位置。
- 任务正文、文件和输出不进入普通网关审计日志。
- 中继模式下是否端到端加密由策略快照明确记录;界面必须显示网关能否读取正文。
## 10. 能力包与更新
每个 Package 必须提供不可变版本、目标平台和架构、内容大小、SHA-256、发布者签名、权限
声明、依赖和兼容范围。安装流程统一为:
```text
取得签名目录 -> 选择不可变目标 -> 下载暂存 -> 校验大小/摘要/签名
-> 展示权限差异 -> 批准 -> 安装到 GoodBuddy-owned 目录 -> 验证 -> 启用
```
要求:
- 下载开始后冻结来源,不在失败时静默切换 ShareServer 或镜像。
- 包不得包含凭据;配置只携带非敏感结构和密钥引用。
- 新版本扩大文件、进程、网络、数据或委托权限时,旧 Grant 不自动继承。
- 紧急撤销阻止新安装和新调用;已安装包的停用或删除遵循组织策略并向用户说明。
- 复用现有 GoodBuddy 签名工件原则,但不同用途使用独立签名域。
## 11. 协议分层
### 11.1 局域网发现层
- DNS-SD 服务类型:`_goodbuddy-share._tcp.local`。
- 广播仅包含协议 major、随机实例 ID、端口和配对状态。
- 设备名称、证书和详细版本通过临时加密握手取得。
- 不扫描任意端口,不枚举未主动广播的机器。
### 11.2 ShareServer 控制 API
使用 HTTPS 提供登录、设备注册、目录、管理和审计 API;使用 WebSocket 建立设备出站控制
通道,承载心跳、任务声明、取消和短期数据通道协商。普通管理 API 与设备通道使用不同凭据
和受众。
### 11.3 数据通道
数据通道可以是配对 TLS 直连、ShareServer WebSocket 中继或经网关协商的端到端加密流。
协议只保证有界帧、顺序、取消和终态,不自行实现另一套拥塞控制。
### 11.4 版本协商
- 每层独立声明 `major.minor`。
- major 不同拒绝连接;minor 使用双方共同支持的最低能力集合。
- 未识别字段在声明允许扩展的位置忽略,安全与授权枚举中的未知值一律拒绝。
- 联邦只交换双方共同支持的能力和策略表达式,不降低任一方策略。
### 11.5 Office 编辑协议
Office 编辑使用独立的 HTTPS 会话、文件和事件 API,不复用普通远程能力 Task 表达长生命周期
编辑器。Desktop 通过设备凭据创建会话和传输源文件;编辑器使用一次性启动授权;ONLYOFFICE
使用独立短期 JWT 获取工作副本并提交保存回调。三类凭据使用不同 audience,不能互换。
文档页签可以长期存在,但启动 URL、文件 URL、callback token 和编辑 Cookie 必须短期、单用途
并可撤销。跨端会话、保存和冲突规则见
[Office 协同编辑技术设计](../features/office-document-editing/technical-design.md)。
## 12. 数据与审计
### 12.1 最小审计事件
- 操作者、调用设备和组织。
- 动作、能力、版本和提供者。
- 路由与是否跨组织。
- 输入数据类别、分类、文件数和字节数。
- 授权结果、策略 ID 和批准者。
- 任务状态、耗时、输出大小和资源用量。
- 包下载、安装、更新、撤销和发布变更。
默认不保存提示正文、文件内容、模型输出、密钥、Cookie、认证头或完整私有路径。需要内容
审计的组织必须单独定义保留期限和访问角色,并在调用前向用户显示。
### 12.2 数据权威
- 桌面端是本地配对、本机共享偏好和本地活动记录的权威。
- ShareServer 是组织成员、设备注册、组织策略、发布目录和组织审计的权威。
- ShareServer 是 Office 工作副本、服务端保存检查点、编辑会话和保留清理的权威;Desktop 或
远程 GoodBuddy Agent 是源文件是否成功回写的权威。
- 能力提供者是运行中任务及结果是否产生的权威。
- 联邦两端各自保存本组织审计,不复制对方完整成员目录或内部策略。
## 13. 安全边界
ShareServer 可以部署到公网,因此公网连接、外部组织和服务端多租户是本设计中的实际信任
边界。共同要求如下:
1. 所有远端清单、名称、schema、错误、文件和模型输出都作为不可信数据处理。
2. Renderer 不接触设备私钥、网关令牌、任意 socket 或原始文件传输接口。
3. Main/Provider 在执行边界重新验证任务,不信任 Renderer 或 ShareServer 代替本机授权。
4. ShareServer 按组织隔离查询,搜索和计数接口不能泄露无权查看的设备或能力。
5. 设备只建立主动配置的网关出站连接,不接受 ShareServer 发起任意系统调用。
6. 局域网发现不等于信任,组织成员身份也不等于所有能力授权。
7. 远程能力不能扩大本机 Workspace、Runtime、Skill 或 MCP 既有能力范围,沿用统一执行规则。
8. 密钥只进入对应安全存储,诊断和审计使用固定字段与脱敏错误码。
9. 限制握手、清单、帧、文件、并发、速率、任务时长和审计查询大小。
10. 撤销设备、用户、Grant、Publication、Package 或 FederationTrust 后停止签发新令牌。
11. Office 编辑器使用固定 origin 和隔离会话,不获得 Electron、文件系统、Shell、Runtime 或
普通 ShareServer 管理 API;Document Server 默认禁止任意出站网络和未批准插件。
12. Office 文件正文由服务端实际处理,组织策略必须定义允许格式、大小、保留和内容访问角色,
Desktop 在上传前显示这些事实。
## 14. 建议领域实体
```text
DeviceIdentity
PeerPairing
GatewayConnection
Organization
Membership
Group
RegisteredDevice
Capability
CapabilityVersion
Publication
Grant
Policy
Approval
Task
TaskAttempt
ArtifactTransfer
Package
PackageRelease
FederationTrust
AuditEvent
Revocation
DocumentEditSession
DocumentCheckpoint
```
共享协议只定义跨端 DTO 和状态,不规定桌面 SQLite 与 ShareServer 数据库使用相同表结构。
## 15. 仓库边界
当前仓库边界(跨仓库文档链接约定两个仓库克隆到同一级目录):
```text
goodbuddy/
docs/architecture/share-network-architecture.md
docs/features/device-sharing/
src/ # GoodBuddy Desktop
goodbuddy-shareserver/
docs/features/share-server/
server/ # 独立 Hono Web 服务
shared/ # 当前原型 fixture,不是跨端协议
src/ # React 管理前端
```
ShareServer 源码和服务端文档已迁入独立仓库:
`ssh://git@git.digiman.live:11022/root/goodbuddy-shareserver.git`。
当前服务是交互原型,尚未提供真实 Desktop 集成。共享网络总体设计仍由本文统一维护。
ShareServer 不反向依赖桌面端 `src/shared`,构建和运行不要求桌面仓库存在。
真实跨端协议实现时双方依赖独立 `share-protocol` 包,具体仓库归属和发布方式届时确定,
不提前创建空包。协议包不得包含数据库模型、Electron API、服务实现或凭据。
## 16. 共同验收标准
- [ ] 不配置 ShareServer 时,两台桌面端可通过局域网发现、双端配对、查看获准能力并调用。
- [ ] 关闭“允许被发现”不会关闭主动发现;关闭主动发现不会撤销既有配对。
- [ ] 本机没有明确 Publication 时,任何 Peer 或组织成员都不能查看或调用本机能力。
- [ ] 桌面端可以连接多个 ShareServer,任一连接故障不影响局域网和其他连接。
- [ ] ShareServer 只展示当前用户、设备和组织获准查看的目录。
- [ ] 每次远程调用都能说明提供者、执行位置、发送数据、路由、授权依据和委托链。
- [ ] 网络中断且副作用结果不确定时进入 `outcome_unknown`,不会自动重放。
- [ ] 能力包经大小、SHA-256、签名、兼容和权限差异校验后才能安装。
- [ ] 跨组织调用同时满足两端策略;联邦撤销后不能创建新任务。
- [ ] 审计默认只保存必要元数据,不保存正文、文件、密钥或认证信息。
- [ ] ShareServer 可独立部署和升级,不由 Electron 启动,也不影响桌面端离线使用。
- [ ] 启用 Office 服务后,Desktop 可在多实例文档页签中编辑 DOCX、XLSX 和 PPTX;保存回调、
源文件冲突或网络故障均不会破坏原文件。
- [ ] Office 会话的启动、文件和 callback 凭据不可互换,过期或撤销后不能重用;临时工作副本
按策略清理,默认审计不保存文档正文或 AI 选区内容。
## 17. 相关设计
- [设备发现与共享 PRD](../features/device-sharing/prd.md)
- [设备发现与共享功能逻辑](../features/device-sharing/logic-design.md)
- [设备发现与共享 UI](../features/device-sharing/ui-design.md)
- [设备发现与共享技术设计](../features/device-sharing/technical-design.md)
- [ShareServer PRD](../../../goodbuddy-shareserver/docs/features/share-server/prd.md)
- [ShareServer 功能逻辑](../../../goodbuddy-shareserver/docs/features/share-server/logic-design.md)
- [ShareServer 管理界面](../../../goodbuddy-shareserver/docs/features/share-server/ui-design.md)
- [ShareServer 技术设计](../../../goodbuddy-shareserver/docs/features/share-server/technical-design.md)
- [Office 协同编辑](../features/office-document-editing/README.md)
- [SSH 远程主机与 Agent](../features/remote-host/README.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
File diff suppressed because it is too large Load Diff
@@ -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 就宣称功能完成 |
| 更新一个权威文档,其他文档链接它 | 在多个文档复制同一预算、状态机、所有权表并分别维护 |
+83
View File
@@ -0,0 +1,83 @@
# 数据库迁移与大数据升级指南
本文约束已发布用户数据的数据库升级。具体字段、存储表示和功能行为由对应功能文档负责;
本文不建立通用迁移框架、回滚引擎或独立状态日志。
## 先确认问题和数据边界
1. 先只读统计文件大小、WAL、页数、空闲页、表与字段的字节数,不先删除或压缩数据库。
2. 区分有效内容、重复存储、索引和已释放但未回收的空间。空闲页少时,单独
`VACUUM` 不能消除重复记录。
3. 查清实际写入、读取、删除、重放与恢复入口。修改 Runtime 相关持久化时,同时检查
本机和 GoodBuddy Agent 的桌面路径,不假设一个入口覆盖另一个入口。
4. 明确用户必须保留的信息、稳定 ID、顺序、时间、引用与远程去重字段。
分支内未发布、可安全重建的内部记录不需要兼容机制。
## 实现原则
- 先修新增数据路径,再修历史数据。历史转换与新写入尽量使用同一套转换函数。
- 普通 schema 变更使用既有 `PRAGMA user_version` 和 SQLite 事务;
已升级 schema 不允许旧客户端强行打开,不通过回写版本号支持降级。
- 将结构升级、旧内容转换和空间回收分别判断。结构版本落后不代表需要整理历史;
普通更新产生的空闲页也不是旧数据证据。只有明确的旧内容转换才允许检查空闲页并
执行 `VACUUM`,后续新增结构迁移不自动继承全库回收。启动页按实际操作显示状态。
- 同一次启动的重试可在内存中保留已确认的转换/回收决定;退出后重新根据数据库
schema 和未转换内容判断,不为仅剩空闲页增加持久化标记或重复启动回收。
- 大数据转换不得同步阻塞 Electron Main 或 Renderer。放到工作线程,在业务连接
打开前处理,并使用应用的共享字体、主题与页面组件显示进度和可执行的失败提示。
- 根据实测设置有界读取批次与缓存,批次提交后再报告进度。保持 SQLite 事务与
checkpoint,不靠关闭同步落盘、取消约束或一次性读入整库提速。
- 从已提交数据直接重试和还原即可完成的迁移,不增加 durable snapshot、journal、
receipt 或多阶段恢复状态。退出及失败至少回滚未提交批次,保留已完成部分。
- 差量编码必须能精确还原原始事件。更新内容、删除或重排都要覆盖;涉及远程来源时,
去重、冲突判断、消息投影和恢复序号不得失效。
- 释放数据库页和归还文件系统空间是不同步骤。先减少实际存储内容,再执行需要的
空间回收;保留足够临时空间,磁盘不足时明确失败并允许重试。
- SQLite 同库写入是串行的。是否增加读取/转换并行,应由实际读取、解析、写入、
提交与 checkpoint 的耗时决定,不把增加写线程当作默认优化。
## 验证与备份
### 合成回归
覆盖正常转换、无变化记录、重复打开、内容修订、失败事务和退出续跑。数据库大小问题
必须同时验证新增写入的增长量、旧记录可还原性以及文件实际缩小,而不只检查 schema。
对象比较必须覆盖真实生产形状和 JSON 往返,尤其是可选字段值为 `undefined` 时与字段缺省
的差异。再次修复已发布迁移产生的数据时,提高目标 schema,并覆盖已升级用户的入口;
不能只处理尚未迁移的旧库。
仅修改升级调度、不改变存储格式时不增加 schema。回归必须覆盖已完成历史转换的库
在后续结构升级时仍有空闲页,确认不重新转换或 `VACUUM`,并验证真实 Worker、
同次启动重试和下一次启动的区别。
### 真实旧库
1. 关闭占用源数据库的应用,确认 WAL 状态。没有 WAL 时可复制已关闭的数据库;
有活动 WAL 时使用 SQLite 一致性备份机制,不只复制主文件。
2. 备份到空间足够的位置,记录字节数并校验 SHA-256。备份和测试副本分离,
不直接在唯一原件或唯一备份上试跑。
3. 使用当前源码或用户要求的 portable 正常启动入口执行升级,不用手工 SQL 代替
产品升级过程。退出一次后重新启动,验证已提交数据可以继续处理。
4. 比较升级前后消息、任务、成果等用户表的内容;对转换事件逐条还原后比较原始内容、
ID、顺序、时间和去重来源,不只比较行数。日志只能包含统计和断言结果,不记录正文。
5. 运行数据库完整性检查,记录主文件和 WAL 的实际字节数、回收比例、耗时、平台、
源 schema 与目标 schema。确认原件和备份未被改写。
6. 运行 `npm test`、`npm run typecheck`、`npm run lint`;生产构建变化按开发验证规则
检查,发布准备则遵循[发布手册](./release-runbook.md),不重复本地打包。
7. 用真实模型模拟正常用户任务,允许主代理与子代理按需调用工具、修改独立测试项目、
运行测试和处理失败;只读纯文字模型调用不能证明工具事件增长正常。记录实际提供商
请求数与统计范围,不把 Runtime 用量上报数当成全部 HTTP 请求数。
## 发布与维护
发布说明应告知升级范围、可能耗时、退出重试、额外磁盘空间和降级边界。不得把一个测试库
的回收比例当作所有用户都能获得的保证。功能文档记录具体转换规则,进度文档记录实际
证据和未完成项;未经验证的阶段不能标为完成。
### 已有实例
- Desktop 0.13.1:子任务累计过程重复保存导致数据库膨胀,转换到 schema 34 的差量事件
表示,并回收空间。具体规则见
[执行记录存储与升级回收](../features/assistant-workbar/execution-history-storage.md),
数据保留与真实旧库验证见[工作栏进度](../features/assistant-workbar/progress.md)。
- Desktop 0.13.2:schema 35 同时整理旧快照和 schema 34 的重复工具块,工具字段按
JSON 可保存的值比较,不因 `undefined` 被省略而误判变化。
+252
View File
@@ -0,0 +1,252 @@
# GoodBuddy 龙芯(LoongArch)预览版构建
本文档是 GoodBuddy 龙芯预览版的权威构建与功能状态说明。当前目标是
Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支持矩阵。
## 当前状态
当前流程可以在 AMD64 Linux 主机上交叉构建一个独立的
`goodbuddy-loongarch-preview` DEB,并校验包内 Electron、node-pty 和 Koffi
均为 LoongArch ELF。
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
当前使用 Electron 44.5.1,因此该预览包只用于移植和真机验证,不能作为正式
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
已完成的实际构建结果:
| 项目 | 结果 |
| --- | --- |
| GoodBuddy 源版本 | `0.11.0` |
| 预览版本 | `0.11.0-loong64-preview.1` |
| Debian 架构 | `loong64` |
| Electron | 社区版 `42.3.0` |
| DEB 包名 | `goodbuddy-loongarch-preview` |
| 构建产物 | `GoodBuddy-0.11.0-linux-loong64-preview.deb` |
| 已验证构建时间(UTC) | `20260825T102937Z` |
| 已验证产物大小 | `187053208` bytes |
| 已验证产物 SHA-256 | `461ade49f10a20ef7b28163077a2ecd6afd2c5b1b0befc673c6d174fd58497b3` |
| DEB 解包、架构和动态库解析 | 已通过 |
| QEMU Electron 初始化 | 共享库解析通过;Chromium 多线程初始化在 QEMU 下异常退出 |
| 龙芯真机启动 | 尚未验证 |
版本号和产物名会跟随当前 `package.json` 版本变化,上表只记录首次成功构建。
## 功能列表
“已打包”只表示所需代码或组件进入 DEB,不等同于已在龙芯桌面完成运行验证。
| 功能 | 预览包状态 | 当前验证 |
| --- | --- | --- |
| GoodBuddy Electron 桌面主体 | 已打包 | Electron 可执行文件为 LoongArch ELF;尚未完成真机 GUI 启动 |
| React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 |
| 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 |
| Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 |
| 本地终端能力 | 已打包 | 输入锁文件固定的 node-pty(当前为 1.2.0-beta.15)已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 |
| Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF |
| 内置 Skills | 已打包 | 静态资源已进入 DEB |
| GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch |
| Remote Runtime | **不包含** | 当前没有 LoongArch Remote Runtime 产物 |
| OpenCode Runtime | **不包含** | OpenCode 所需 Bun Runtime 当前不支持 LoongArch |
| 原生 Canvas 图像解码 | **不包含** | npm 当前没有 `@napi-rs/canvas-linux-loong64-gnu` 包 |
| Koffi musl binding | **不包含** | Koffi 3.1.4 只发布了 `linux_loong64` glibc binding |
| 正式自动更新和发布索引 | **不支持** | 预览包不加入正式 CI、安装包矩阵或更新索引 |
在真机验证完成前,不应把“已打包”功能描述为正式可用。尤其是窗口显示、GPU、
音频、系统托盘、桌面集成、PTY 交互和模型调用都必须在真实 LoongArch 桌面上
重新验证。
## 与正式 GoodBuddy 隔离
预览版有意使用单独身份,避免覆盖正式安装:
- Debian 包名:`goodbuddy-loongarch-preview`
- 产品名:`GoodBuddy LoongArch Preview`
- 可执行文件:`goodbuddy-loongarch-preview`
- 安装目录:`/opt/GoodBuddy-LoongArch-Preview`
- Electron 应用身份与正式 GoodBuddy 不同,因此使用单独的用户数据目录
该流程不会修改 `.github/workflows/packages.yml`,也不会把 `loong64` 加入
正式 release manifest 或下载索引。
## 构建前提
### 本地源码主机
- 已完成 `npm ci`
- Node.js 和 npm 版本满足仓库要求
- 可以成功执行 `npm run build:bundle`
### AMD64 交叉构建主机
- Linux AMD64
- Docker
- 建议至少 30 GiB 内存和 50 GiB 可用磁盘
- 可访问 Ubuntu、npm、Electron headers 和社区 Electron Release
### Electron LoongArch shell
从
[`darkyzhou/electron-loong64`](https://github.com/darkyzhou/electron-loong64)
的 Releases 下载:
```text
electron-v42.3.0-linux-loong64.zip
```
构建脚本固定校验以下 SHA-256,摘要不匹配时必须停止:
```text
92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e
```
这是社区产物,不是 Electron 官方发布的 Linux LoongArch 二进制。
## 第一步:生成架构无关输入
在 GoodBuddy 仓库根目录执行:
```sh
npm ci
npm run build:bundle
node build/loongarch-cross/prepare-input.cjs
```
默认输出:
```text
dist/loongarch-cross/goodbuddy-loongarch-cross-input.tgz
```
输入归档包含生产 `out`、锁文件、Skills、Continue Runtime、npm Runtime、
图标和发布元数据。它不包含 Agent、Remote Runtime 或 OpenCode 二进制。
可通过 `--output` 指定其他输出位置:
```sh
node build/loongarch-cross/prepare-input.cjs \
--output /path/to/goodbuddy-loongarch-cross-input.tgz
```
## 第二步:构建交叉工具链镜像
将 `build/loongarch-cross`、输入归档和 Electron zip 放到 AMD64 Docker
主机,然后执行:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
build/loongarch-cross
```
工具链镜像包括:
- `loongarch64-linux-gnu-gcc` 和 G++
- LoongArch glibc 开发 sysroot
- QEMU user mode
- CMake 和 Ninja
- Rust `loongarch64-unknown-linux-gnu` target
- Node.js、npm 和上游 node-gyp
可先运行基础探针:
```sh
mkdir -p build/loongarch-cross/results
docker run --rm \
--volume "$PWD/build/loongarch-cross/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
探针必须通过 C、C++、CMake、Rust、NAPI-RS、QEMU、DEB、Koffi 和
node-pty 检查。Canvas 和 Koffi musl 显示为 unavailable 是当前已知限制,
不是 Debian 桌面主体 DEB 的构建失败。
## 第三步:生成预览 DEB
假设交叉构建主机上的文件布局如下:
```text
input/goodbuddy-loongarch-cross-input.tgz
electron/electron-v42.3.0-linux-loong64.zip
preview-output/
```
运行:
```sh
docker run --rm \
--entrypoint /usr/local/bin/goodbuddy-loongarch-preview-deb \
--volume "$PWD/input:/input:ro" \
--volume "$PWD/electron:/electron:ro" \
--volume "$PWD/preview-output:/preview-output" \
goodbuddy-loongarch-cross:ubuntu-26.04 \
/input/goodbuddy-loongarch-cross-input.tgz \
/electron/electron-v42.3.0-linux-loong64.zip \
/preview-output
```
每次构建创建一个 UTC 时间戳目录,包含:
```text
GoodBuddy-<version>-linux-loong64-preview.deb
preview-manifest.json
SHA256SUMS
```
脚本会执行以下强制检查:
1. 校验 Electron zip 的固定 SHA-256。
2. 删除 npm 错误解析出的宿主架构可选原生包。
3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。
4. 读取输入 lockfile 固定的 node-pty 版本,并使用上游 node-gyp 11.4.2
和 Electron 42.3.0 headers 交叉编译。
5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。
6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。
7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。
Ubuntu 发行版自带的 node-gyp 会在 Linux addon 链接阶段无条件添加
`-lnode`,不适合此 Electron 交叉编译;不要用它替换脚本固定的上游
node-gyp。
## 验证边界
### 已由交叉构建验证
- C、C++、CMake 和 Rust 生成 LoongArch ELF
- C、C++ 和 Rust 程序通过 QEMU user mode 执行
- 最小 NAPI-RS addon 交叉编译
- node-pty 针对 Electron headers 交叉编译
- Koffi LoongArch glibc binding 架构
- Electron、node-pty、Koffi 和 DEB `loong64` 元数据
- DEB 完整解包
- Debian Forky rootfs 中 Electron 全部共享库解析;该检查发现并补充了
`libpulse0` DEB 依赖
### 仍需真实龙芯桌面验证
1. 安装 DEB 并从桌面入口启动。
2. 验证窗口、渲染、GPU 回退、输入法、系统托盘和通知。
3. 配置模型并执行一次真实的直接模型对话。
4. 验证 Continue Runtime 启动、取消和退出清理。
5. 打开本地终端,验证 node-pty 输入、输出、窗口缩放和进程回收。
6. 验证 Koffi 使用路径。
7. 验证音频、文件选择器、剪贴板和外部链接。
8. 确认 Agent、Remote Runtime、OpenCode 和 Canvas 相关入口不会被误认为
预览版已支持。
QEMU 可以补充动态链接器和缺库检查,但不能替代上述 GUI、PTY、GPU、音频和
桌面集成真机测试。当前 Electron 在 QEMU user mode 下可以完成全部共享库
解析;继续进入 Chromium 多线程初始化后,QEMU 出现 target `SIGSEGV`,因此
不能用该结果判断真机 Electron 是否可以启动。
## 已知依赖问题
- npm 的 `--cpu=loong64 --os=linux` 仍可能安装宿主 x64 的 Koffi 可选包。
打包脚本会删除所有该作用域的已解析原生包,并按 lockfile integrity 手工
安装 LoongArch 包。
- `@napi-rs/canvas` 没有发布 LoongArch 原生包。基础 NAPI-RS 探针通过只说明
Rust addon 工具链可用,不代表 Skia/Canvas 已完成移植。
- Koffi 没有 LoongArch musl binding,因此当前不能满足 GoodBuddy Agent 的
双 glibc/musl 产物契约。
- 预览 Electron 比正式 GoodBuddy 低一个主版本。正式支持必须先获得与当前
Electron 基线一致、可重复构建并完成安全维护的 LoongArch shell。
@@ -0,0 +1,137 @@
# Desktop 0.13.12 and Agent 0.13.3 Preparation
Prepared on 2026-09-23 on `main`. This record describes a local candidate, not a
published release. Desktop notes are in `resources/release-notes.json`; Agent
notes are in `resources/agent-release-notes/0.13.3.md`. Both are bilingual drafts
awaiting approval with the exact candidate commit before tagging.
## Scope
- Desktop baseline: `v0.13.11`, including all nine subsequent commits through
`83a48a4` and the reviewed working-tree changes.
- Agent baseline: `agent-v0.13.2`. The Agent changes reclaim idle Runtimes and
drain displaced Agents; the Desktop Continue path restores conversation
history when reopening a Runtime.
- Desktop package and both root lockfile versions are `0.13.12`.
`agent-runtime-lock.json` is `0.13.3`. Agent package tooling derives the minimum
Desktop version from `package.json`. Node, OpenCode, and Continue locks are
unchanged at `24.19.0`, `1.18.29`, and `1.5.47`.
- Both feature matrices were reviewed and updated for versions, Supervisor
default-off behavior, implemented reviews/graphs/activity/incremental input,
remaining supervision work, and release acceptance limits.
## Local Validation
`npm run release:notes:verify`, `npm run typecheck`, and `npm run lint` passed
after the final source changes. Focused runs cover 35 distinct test files and
690 passing tests; 403 tests were excluded by the IPC/App name filter.
| Group | Files | Passing Tests |
| --- | ---: | ---: |
| Supervisor, settings, bridge, and release-note suites passing in the initial run | 14 | 157 |
| Database, migration, activity I/O, note storage, menu, and sidebar suites after fixes | 6 | 209 |
| IPC/App filtered by `supervision\|Supervisor\|heartbeat\|application enable\|graph navigation` | 2 | 16 |
| Agent CLI, daemon, Runtime backend, Desktop ACP, transport, and connection manager | 7 | 207 |
| Usage, duration, workspace, Markdown, and Supervisor Electron layout | 6 | 101 |
The initial focused run had 25 failures: 22 synthetic legacy database fixtures
retained schema-46 objects, two menu assertions assumed Supervisor was enabled,
and one browser close retry left its earlier error visible. Fixtures now remove
the new objects before simulating older versions, menu tests follow the saved
enabled preference, and a successful browser close clears its scoped error.
All affected suites passed on rerun. Production migration behavior was not
changed to accommodate synthetic fixtures.
The Electron layout test uses production components with isolated fixtures,
including 1440, 1024, and 390 pixel widths and light/dark themes. It is not a
packaged-app probe or a real-user database acceptance test.
No real model requests were made during this preparation. Existing development
evidence records eight requests on the shared Linux x64 Host for remote lifecycle
checks and three for incremental review. The remote evidence does not repeat
Continue real-Host validation, the complete signed installation flow, or the
native architecture matrix. See
[remote lifecycle evidence](../features/remote-host/technical-design.md#生命周期验证2026-09-23)
and [incremental review evidence](../features/conversation-supervision/progress.md#2026-09-23-自动增量验证).
## Pending Release Work
The user requested no further full local suite. Full main-branch CI validation
and its production build remain required before tagging; native Desktop and
Agent packaging and publication verification remain pending. No local production
build or package was run. No push, tag, remote synchronization, or publication
was performed during preparation. Approval must identify the exact candidate
commit and both bilingual note files before either release tag is created.
## Approved Candidate CI Attempt
The user approved `bf8ece05902509411454b27b742ebfec930005ad` and both bilingual
note files. On 2026-09-23 that exact commit was fast-forwarded to `main` on
`origin` and `github`; both remote refs were verified. GitHub SSH stalled, so
the GitHub push used HTTPS with the existing GitHub CLI keyring credentials.
No persistent Git configuration was changed. GitHub reported the account's
existing pull-request-rule bypass for this requested direct push.
- [Desktop main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362694)
failed in the full suite: 4 files failed, 418 passed, 13 skipped; 10 tests
failed, 4,997 passed, 43 skipped. Type checks, lint, and the production build
did not execute after the test failure.
- [Agent main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362799)
passed source validation and native Linux x64, Linux arm64, and Darwin arm64
deterministic compound-package verification with ephemeral test signatures.
- Neither release tag was created or pushed. No Desktop or Agent publication
workflow was started, and no production release assets were verified.
The follow-up changes are limited to tests. Two IPC tests clear the startup
settings-read count before measuring interactive request lookups. The settings
failure UI test now expects Supervisor to remain disabled. OpenCode isolation
tests prepare their real configuration dependencies in a dedicated temporary
directory instead of relying on a prior local package preparation; connection
failures now include their diagnostic detail. The large activity-list and
multi-page navigation and external knowledge binding tests have explicit
15-second budgets; the binding test also waits up to five seconds for its
resulting heading.
Its snapshot fixture now selects by library ID, and its instance list remains
available during polling instead of assuming a fixed number of asynchronous
reads. Both mocks reset between tests; the binding test waits for the
selected-library read.
These budgets address timeouts observed in the full CI run without removing
assertions or changing product behavior.
The approved source and bilingual notes remain unchanged. These test changes
need a revised candidate commit and approval before another release attempt;
the failed commit must not be tagged. No local production build, package,
external model request, or LoongArch build was performed.
Follow-up validation: the seven selected IPC/UI regression cases and all seven
OpenCode isolation cases passed locally. Type checks and lint passed. Full
remote CI and the production build still require a new approved commit; these
focused results do not satisfy that release gate.
## Linux Executable Path Follow-up
On 2026-09-23, local `main` was fast-forwarded from `163e245` to
`073aa45a30efe99fab7a92f8aee2a36087db9c2a` after inspecting the four intervening
commits. They update only the community QR codes in the two README files.
The user authorized retaining these changes, fixing CI, pushing both remotes,
and publishing Desktop `v0.13.12` and Agent `agent-v0.13.3` after successful
candidate CI without another approval round. No LoongArch preview is requested.
[Desktop CI 35854792610](https://github.com/mesalogo/goodbuddy/actions/runs/35854792610)
reported 5,004 passing tests, three failures, and 43 skipped tests. All three
failures were in the real OpenCode isolation test. It constructed
`opencode-ai/bin/opencode` on Linux, but OpenCode 1.18.29 installs its npm
executable as `bin/opencode.exe` on every platform. The test now reuses
`resolveBundledRuntimePaths`, the existing production resolver, instead of
constructing a platform-specific filename. Production Runtime code and both
release-note files are unchanged.
Local follow-up validation passed all nine tests in
`opencode-selection-isolation.test.ts` and `bundled-runtimes.test.ts`, including
the existing Linux development-path assertion, plus full type checks and lint.
Release-note and package/lockfile version validation passed. The isolation
tests made two requests to their local mock endpoint and zero real model
requests. No full local suite, production build, package, or real-Host check
was run for this test-only fix; it changes neither deployed Agent code nor
the Desktop-to-Agent production path. Successful Linux main-branch CI and
its production build remain required before tagging.
@@ -0,0 +1,37 @@
# Desktop 0.13.13 Preparation
Prepared on 2026-09-23 from `b16fc90`, relative to published Desktop `v0.13.12`.
This is a Desktop-only release; Agent remains at published `0.13.3`.
No LoongArch preview is requested.
## Scope and isolation
- `bf8ff7f` reduces repeated activity-history reads during streaming saves.
- `b16fc90` restores compact supervision feedback controls and target names.
- The candidate is prepared in a separate worktree. The user's uncommitted
supervision timeout tests in the original worktree are explicitly excluded.
- Desktop and both root lockfile versions are `0.13.13`. Both feature matrices
and bilingual release notes were reviewed against this range.
- Database schema and Agent/Runtime contracts are unchanged. The activity
comparison cache belongs to Desktop persistence; it does not modify the
deployed Agent, model bridge, or remote Runtime lifecycle.
## Validation
- Six focused files passed 192 tests: activity-history I/O, AssistantDatabase,
storage upgrades, sidebar interactions, SupervisionCard, and the Supervisor
Electron layout fixture.
- Re-running `tests/supervisor-layout.electron.test.ts` with
`GOODBUDDY_SUPERVISOR_SIDEBAR=1` passed its additional sidebar scenario:
480, 300, and 200 pixel widths in both themes, no horizontal overflow,
compact 34px buttons, visible keyboard focus, and no raw target ID in body text.
- `npm run release:notes:verify`, `npm run typecheck`, and `npm run lint` passed.
- Ten consecutive activity saves performed one full-history scan. WAL and SQL
counters are not installed-application physical disk throughput measurements.
- No real model requests, local full-suite rerun, production build, packaging,
or packaged-app probe were performed. UI validation uses production
components with isolated fixtures, not a real user database.
The candidate must pass complete main-branch CI and its production build before
an unused annotated `v0.13.13` tag is pushed to both remotes. Native six-platform
packaging and public GitHub/OSS verification remain subsequent release steps.
@@ -0,0 +1,64 @@
# Desktop 0.13.14 / Agent 0.13.4 发布准备
后续状态:Desktop 标签流程 `36165805814` 在测试阶段失败,未进入构建或发布;
Agent 流程 `36165805884` 成功且 0.13.4 已公开。两个历史标签均保留,
桌面恢复版见 [0.13.15 准备记录](./release-preparation-0.13.15.md)。
以下为原候选准备时的范围与证据,不代表其桌面发布成功。
## 范围与审批
- 用户确认从 0.13.13 升至 0.13.14,现有七个测试修改及三个验证辅助文件一起提交。
- 不构建实验性 LoongArch 预览,不运行本地生产构建、打包或安装包启动探针。
- 准备起点为 `ff746bdc1add6a12227ccb61a7794f654aaaa8ba`。
- 最近可达稳定标签为 `v0.13.12`。已公开发布的 `v0.13.13` 指向
`7756231`,其发布准备提交不在当前 main 祖先链上;两项产品修复已包含,
0.13.13 的包内发布说明也已保留。需同时核对两个范围,不重复宣传已发布修复,
不移动标签或重写历史。
- 用户随后确认 Agent 也末位加一,目标为 0.13.4。已批准联合发布的双语说明,
以及验证通过后一起提交本地候选;最终候选 SHA 仍须另行确认。
- Agent 范围为 `agent-v0.13.3..HEAD`,生产改动来自 `7adae97`。
- 用户批准提交并推送两个远端、等待 CI;标签仍待精确提交确认。
- Fetch 确认两个远端 main 均为 `7756231b3bdfa148f1b2702e4ff3fc3bd0cb057e`。
用户批准以普通合并保留双方历史,保留本次版本和已批准说明,
补回历史发布文档;不强推、不改动已有标签。
- 本地候选提交为 `d23af99`。普通合并解决后,仅新增或修改文档;
产品源码、测试、依赖、版本文件及两份已批准说明与该候选完全一致。
合并后 `v0.13.13` 恢复为可达发布基线,先前祖先链差异仅记录准备起点状态。
## 交付与数据边界
- 本轮联合准备 Desktop 0.13.14 和独立 Agent 0.13.4,不覆盖历史 Agent 包。
- 桌面包含远程结果未知后显式发送新消息的恢复修复;Agent 完成确认、
空闲回收失败处理和旧安装清理随 0.13.4 交付。
- Agent 发布流程读取本次 package.json 作为最低 Desktop 版本,因此需先升级
Desktop 至 0.13.14,再下载并更新 Host。Node、OpenCode、Continue 锁定版本不变。
- 两个候选分别使用 `v0.13.14`、`agent-v0.13.4`,Agent 不替代 Desktop Latest。
- 数据库从 schema 46 升至 47。升级前需完整备份用户数据,包括数据库和
相邻 notes/ 目录;旧客户端不能打开升级后的数据库。
- 已检查中英文功能矩阵,更新当前版本、监督回顾、待办能力及独立 Agent
交付边界。其他平台和功能的既有未验收事项继续保留。
## 验证
- 首次 `npm test`:5,125 项通过、74 项跳过、2 项失败。失败均为版本已更新而
说明尚未获批写入导致的当前版本缺失,未修改断言规避。
- 联合说明写入后的 `npm test`:435 个文件通过、10 个文件跳过;
**5,127 项通过、74 项跳过,0 失败**,耗时 685.49 秒。
- `npm run typecheck`、`npm run lint`:首轮与联合候选复验均通过。
- `npm run release:notes:verify`:联合说明写入后通过;Desktop 三处版本一致,
当前说明仅一项,Agent lock 与独立双语标题一致。
- 合并后复验:发布说明校验、9 项发布说明测试、完整 typecheck、lint、
`git diff --check` 通过。因合并仅恢复文档,未重复运行完整测试和 Host 探针。
- 当前源码 Linux x64 完成/历史恢复探针通过:两轮 Ask 均完成,Runtime 重建后
保留会话历史,每轮结束及最终退出时所属进程数均为 0。
- 隔离旧安装清理探针通过:真实 CLI 回收所属孤儿进程、删除对应旧 payload,
保留存活旧 daemon、活动工作、历史及 current/candidate,重复清理无重复删除。
- 本轮新增真实模型调用 **2 次**,模拟模型调用 0 次;清理探针模型调用 0 次。
- 两个探针从当前源码重新生成临时测试脚本,仅操作共享 Linux x64 Host 的独立
`/root/tmp/gb-lifecycle-*` 与 `/root/tmp/gb-upgrade-*` 测试目录,不替换共享安装。
探针沿用历史测试身份版本,不是正式签名 0.13.4 包安装验收,也不代替
Linux arm64、macOS arm64 原生 CI 或完整 Host 更新界面验证。
- 复现命令沿用[远程 Host 技术设计](../features/remote-host/technical-design.md)
中 lifecycle-host 和 upgrade-cleanup 脚本;本轮生命周期参数为
`driver --completion-only`,使用真实模型而非 `--fake-model`。
- 候选主分支 CI、生产构建、六平台原生打包及公开发布验证:尚未执行。
@@ -0,0 +1,40 @@
# Desktop 0.13.15 恢复版准备
## 发布范围
- 以已发布的 `v0.13.13` 为用户更新基线,完整承接未发布 `v0.13.14` 的产品变更。
历史准备及 Host 验证见 [0.13.14 记录](./release-preparation-0.13.14.md)。
- 0.13.14 标签流程 `36165805814` 在测试阶段失败,生产构建、打包与发布均未执行;
GitHub Release API 返回 404。不可变标签保留,包内条目改为 0.13.15,
不保留重复的未发布说明。
- Agent 0.13.4 流程 `36165805884` 成功,三个包已公开,最低 Desktop 要求仍为
0.13.14。恢复版满足该最低要求;不重发 Agent、不更改其签名目录或标签。
- 用户批准 0.13.15 标题及升级提示调整,其余双语正文沿用已批准内容;
要求先推送候选,等 CI 全量验证与生产构建成功后再打标签。
- 不构建 LoongArch,不运行本地生产构建、打包或安装包启动探针。
- 中英文功能矩阵已复核,同步恢复版本和 Agent 已发布状态,保留其他未验收边界。
## CI 修复
`cd27163` 仅改测试和质量规范,不改变产品、Agent、数据库或外部契约:
- 原生能力清单测试控制第二次响应,等待重试已开始再展开控件,并分别校验响应前后状态。
- 浏览器关闭测试在同一个 `waitFor` 中检查两次调用、页签移除和错误清除。
- 笔记布局测试显式扫描 fixture 依赖,使用可见的隔离 Electron 窗口执行原生输入,
保留 sandbox/contextIsolation,明确检查超时、信号和退出码后才读取结果。
对应 CI 证据:标签流程有 1 项失败(原生能力按钮未找到);
main 流程 `36164698647` 有 2 项失败(关闭错误残留、布局测试约 90 秒后缺少结果文件)。
两次失败点不同,不能仅凭本地通过认定 CI 已修复。
## 验证
- 本地三个定向用例通过,Electron 布局约 3.46 秒,全部几何和交互断言保留。
- 修复提交完整 typecheck、lint 通过。
- 本轮没有重复本地全量测试,以新候选 Linux CI 的完整测试结果为准;
先前 5,127 项本地通过仅为历史记录。
- 无新真实模型调用,未改变 Agent 或 Runtime 生产源码,无需重复 Host 开发探针。
- 恢复候选的说明校验、9 项发布说明测试和 `git diff --check` 通过;
三处桌面版本一致。逐项比较确认只替换版本及已批准升级提示,
0.13.14 无重复条目,Agent lock 仍为 0.13.4。
- 主分支 CI、生产构建和标签发布结果待后续核对。
@@ -0,0 +1,43 @@
# Desktop 0.13.16 发布准备
日期:2026-09-26。当前为候选,不代表已发布。
## 范围与批准
- 用户确认 Desktop 0.13.16、Agent 保持 0.13.4、不构建 LoongArch 预览。
- 基线为 `325b702934ba0f0c076f27e926a897267262ab55`,用户明确批准纳入新提交的
Supervisor 界面修改。相对已发布 `v0.13.15` 包含:
- `e6ac073`:监督回顾持续分批处理,保留主动暂停、单次模型超时和并发。
- `4c7193e`:结构升级不自动回收正常空闲页,准确显示操作状态及保留同启动重试。
- `325b702`:监督回顾空态、运行状态与活动阶段布局修复。
- 用户已批准中英文三项问题修复及两项升级提示,写入 `resources/release-notes.json`;
已发布版本说明保留。最终候选 SHA 需另行确认,主分支 CI 通过后才创建新标签。
- `FEATURES.md` 和 `FEATURES.zh-CN.md` 已核对并更新当前版本、持续回顾、
结构升级及界面行为,删除过时的执行预算暂停描述。
## 数据与 Agent 边界
相对 0.13.15 不提高 schema,仍为 47。不新增持久化迁移标记,不改写用户数据库。
旧格式仍执行必要转换;普通结构升级不因空闲页执行全库 VACUUM。
监督回顾仍只读、无工具,模型调用可能收费;取消固定时长暂停后可连续处理更多批次。
本轮没有 Agent、Runtime、模型桥、部署锁或远程协议修改;本机与远程历史共用桌面升级
实现。监督通过桌面 Ask 编排,不改变 Host 运行路径,不要求 Agent 更新或新 Host 部署。
本轮实际模型请求数为 0。
## 验证
在独立工作树准备,避免其他工作修改影响候选,复用相同锁定依赖,未复制本机私有环境文件。
候选第一轮全量发现 `HeartbeatCenter.test.tsx` 的两种语言仍要求已移除的重复说明存在;
改为断言说明不存在,保留范围、操作、标题及页面内容校验,专项 **42 项通过**。
最终候选完整测试:**5,128 通过、84 跳过、1 失败**。唯一失败为未修改的
`mermaid-viewer.electron.test.ts` 子进程退出码 null;首次单独复跑被工具的 150 秒
上限中断,随后按用例原有时间窗口复跑 **1 项通过**,覆盖九种显示场景和十次实际 PNG
导出。未修改该用例、提高其自身超时或弱化断言,独立复跑不替代全量结果。
`npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 均通过。
先前迁移定向 47 项通过;最终开发期全量为 5,137 通过、74 跳过、两项 Electron
失败,失败用例独立复跑两项通过。该记录不是本候选的全量或 CI 通过证明。
未运行本地生产构建、打包或安装包启动。主分支 CI、六目标原生打包和公开发布核验待完成。
@@ -0,0 +1,238 @@
# Desktop 0.15.1 与 Agent 0.15.0 发布准备及恢复
日期:2026-09-27 至 2026-09-28。原 Desktop 0.15.0 标签构建失败,
用户已选择 Desktop 0.15.1 与 Agent 0.15.0 恢复发布;验证通过不代表已发布。
## 范围与批准
- 用户要求 Desktop 与 GoodBuddy Agent 同步升级至 `0.15.0`,明确不构建 LoongArch。
- 源码基线:`0f8bf405db7ca21a85cc70009ad083eb837e1e03`。
- Desktop 差异基线为已发布的 `v0.13.16`,Agent 为 `agent-v0.13.4`。
- `v0.15.0` 已创建并推送,保持不可变;恢复使用 `v0.15.1`,
`agent-v0.15.0` 尚未创建或推送。
- 用户已批准两份完整双语说明,正式正文仅维护在
[Desktop 发布资源](../../resources/release-notes.json) 的 `0.15.1` 条目与
[Agent 发布资源](../../resources/agent-release-notes/0.15.0.md)。
已逐条核对写入文本与批准草案一致,本记录不再复制正文。
- `FEATURES.md` 与 `FEATURES.zh-CN.md` 已核对本轮能力、限制和候选版本。
- 用户要求两张未跟踪宣传截图保留原位、不提交,打标签前另行处理;
不移动、删除、暂存或更改忽略规则。
- 2026-09-28 用户更新授权:当前工作区的宣传文档与素材可以一起提交、推送,
后续候选由代理自行审核确认,不再逐次等待用户批准,继续完成双产品正式发布。
三张新增候选图已读取检查,保留原始草稿属性,不冒充正式发布截图或对外媒体发文。
发布说明正文保持不变,`releasedAt` 更新为实际准备发布日期 2026-09-28。
## 本地验证
- `npm run typecheck`:通过。
- `npm run lint`:通过。
- `npm run release:notes:verify`:通过。
- 发布说明相关三个文件定向测试:16 项通过。
- 首轮 `npm test`:5,313 通过、79 跳过、4 失败,耗时 697.61 秒。
三项失败来自当时尚未批准写入的 0.15.0 Desktop/Agent 发布说明;
另一项为 `magic-notes-layout.electron.test.ts` 的布尔断言。
该布局用例单独复验 1 项通过,未修改源码或放宽断言。
单独通过不替代全量结果。
- 正式说明写入后的最终 `npm test`:**5,317 通过、79 跳过、0 失败**,
453 个测试文件通过、11 个跳过,耗时 645.49 秒。随后再次执行完整
`npm run typecheck` 与 `npm run lint`,均通过。
- 最终验证期间出现并行宣传文档编辑及新增 `storygraph.png`,均不属于本次
发布准备改动,不暂存或覆盖。打标签前仍须处理工作区洁净条件。
- 修改文档的相对文件链接、版本一致性和 `git diff --check` 通过。
- 不运行本地生产构建、打包或安装包启动。准确候选提交须经用户批准,
然后双远端推送、主分支 CI、生产构建、原生发布与公开核验分别进行。
## Agent 当前源码实机验证
共享 Linux x64 Host 的 LAN/VPN SSH 端口均可达,实际通过 LAN 与既有固定
Host Key、加密凭据连接。重新捆绑当前源码的 Agent CLI、daemon、model bridge
与 Desktop 适配器,仅上传独立 `/root/tmp/gb-lifecycle-*` 测试目录,复用已安装且
版本锁未变的 Node/OpenCode/Continue,不替换原安装,不构建正式包。
最终 OpenCode、Continue 各通过一次 Ask 和一次 Execute,无工具请求,
验证回答、后续历史、空闲 Runtime 回收及测试停止后所属进程为 0。
模型请求时读取所属进程的固定遥测字段,确认 `DO_NOT_TRACK=1`、
`OTEL_SDK_DISABLED=true` 及三个 exporter 均为 `none`,不输出整个环境。
这些结果补齐 `eb53c82` 的 Agent/model bridge 真实链路验证,
但不证明后台没有其他网络流量,也不替代三目标正式包验收。
首轮 Continue 探针沿用了 OpenCode 原生历史持久化假设,未像 Desktop 一样传入
`request.history`,第二轮历史断言失败;当前 Continue 冷启动源码明确要求传入
保存历史。修正测试输入及会话 ID 断言后通过,未修改产品实现。
有完整计数的实机运行共 **8 次**真实模型 HTTP 请求:首次 OpenCode 2 次、
首次 Continue 2 次、最终 Continue 2 次、最终 OpenCode 2 次。
此前一次直接启动 Electron 的探针未捕获子进程输出,不能确认是否实际发出请求,
该次上限 2 次,不纳入通过证据;因此不能把 8 次写成本轮精确总数,总量为 8–10 次。
后续均使用等待退出并记录脱敏输出的启动器;本轮探针 Electron 进程已退出。
本地诊断证据在临时 `opencode/release-015-host/`,不提交脚本、日志或凭据。
## 首次候选 CI 与测试修正
用户批准的首次候选 `5a6ba6c14f5c58f3da5e9afe281b18e0cbdb1ae8`
已推送至 `origin/main` 和 `github/main`,未创建两个发布标签。
- [Agent CI 36329271991](https://github.com/mesalogo/goodbuddy/actions/runs/36329271991)
源码验证及 Linux x64、Linux arm64、Darwin arm64 原生构建全部通过。
- [Desktop CI 36329271967](https://github.com/mesalogo/goodbuddy/actions/runs/36329271967)
为 5,342 通过、51 跳过、3 失败。两个 App 长历史场景超过默认 5 秒单用例时限;
会话采集 Electron 场景等待 Escape 返回笔记列表超时。生产构建未运行。
- 测试修正仅让原生键盘辅助函数显示窗口、获取焦点并等待 `document.hasFocus()`,
然后发送真实输入;两个长历史场景采用仓库既有的 15 秒单用例时限。
不修改产品实现、不跳过 Linux、不关闭沙箱、不删除或放宽数据和键盘断言。
- Windows 聚焦复验三个失败场景通过,其中真实 Electron 覆盖完整 502 条历史、
214px 窄栏键盘操作、浅深主题确认按钮、真实 IPC/SQLite 和重开保留。
修正后首次全量复验被中断,不计为通过;随后完整重跑为 **5,317 通过、
79 跳过、0 失败**,453 个文件通过、11 个跳过,耗时 646.48 秒。
类型检查、lint 和发布说明校验均通过,新候选 Linux CI 仍需独立通过。
- 本次只改测试和验证文档,不改变 Agent、远程 Runtime 或模型桥;
沿用上方当前源码 Host 验证,不新增真实模型请求。
- 新提交必须重新取得准确 SHA 批准,原发布说明正文保持不变。
## Linux 实机复现与最终同步修正
2026-09-28:第二次批准候选 `12594961fe1e814e89f55de1fbfe245310505115`
的 [Desktop CI 36331853137](https://github.com/mesalogo/goodbuddy/actions/runs/36331853137)
为 5,343 通过、51 跳过、2 失败。两个 App 长历史场景已通过;
Escape 返回列表仍失败,活动历史迁移这次超过默认 5 秒时限,生产构建仍未执行。
按用户要求直接在共享 Linux x64 Host 测试准确候选源码。使用独立磁盘测试目录、
锁定 Node 24.19.0 和 `npm ci`,补齐 Xvfb 与匹配的 X server 系统依赖;
没有创建新账号,使用既有非 root 账号运行测试,保留 Electron 沙箱及其标准 helper 权限。
不替换既有 Agent,不修改其安装或工作区,不执行生产构建或打包。
原测试在 Host 上稳定复现:缩窄窗口会按产品既有行为收起工作栏,隐藏面板仍有非零宽度。
诊断显示 Escape 实际发给 `aria-expanded=false` 的工作栏开关,而不是笔记输入框。
因此仅获取窗口焦点不够;测试改为等待窄视口及响应式渲染完成,按真实按钮打开工作栏,
确认展开状态和目标输入框焦点后再发送键盘输入。临时诊断代码已移除。
迁移测试在 Host 上以原默认时限通过,保留 10,000 条记录、完整迁移、WAL 字节比、
扫描次数与无变化写入断言;仅为这一个大数据场景采用已有的 15 秒单用例时限,
不改数据库实现或全局时限。
- 最终 Linux Host 完整 `npm test`:**5,345 通过、51 跳过、0 失败**,
449 个文件通过、15 个跳过,耗时 751.09 秒。
- Windows 聚焦回归:3 项通过,包含真实 Electron 和迁移。
- Windows 完整类型检查、lint、发布说明校验:全部通过。
- Linux Electron 实测覆盖 502 条历史、218px 窄栏、Escape/Tab/Enter、
浅深主题确认按钮、真实 IPC/SQLite 与重开保留;Windows 对应窄栏为约 214px。
- 本阶段不新增真实模型请求,不改变产品代码或双语说明。
新候选仍须重新批准 SHA 并通过自己的主分支 CI/生产构建,两个标签仍未创建。
## 后续 CI 焦点前提与确定性取消验证
`b8b6007` 的 [CI 36335076841](https://github.com/mesalogo/goodbuddy/actions/runs/36335076841)
两次执行均为 5,344 通过、51 跳过、1 失败,失败点改为 `new title focus`,
生产构建没有运行。该次失败未在 Host 的 10 次带诊断复验中重现,其中 5 次限制单 CPU;
移除临时诊断后再测也通过。这些独立结果不替代 CI。
测试原先直到键盘阶段才显示窗口,前面的自动聚焦检查仍在隐藏窗口执行。
现让测试窗口从开始即显示并确认焦点,保留标题自动聚焦原断言;
超时增加实际焦点、标题禁用/可见/隔离状态和保存状态,避免只报告阶段名称。
可见窗口版本在 Host 连续五次聚焦复验通过。
随后 Host 全量出现一个独立的 Continue 取消测试竞争:
`setTimeout(0)` 没有保证取消发生在子进程启动前,空 mock 已被调用。
用例改在既有启动环境回调处精确取消,仍验证回调执行、子进程未启动和临时配置删除,
不更改 Continue 产品实现或取消机制。
- 最终 Host 完整测试:**5,345 通过、51 跳过、0 失败**,449 个文件通过、
15 个跳过,耗时 743.34 秒。
- Windows 可见窗口 Electron 回归及确定性取消回归通过。
- 完整类型检查、lint 通过;双语说明正文不变,日期调整后另行验证发布资源。
- 上述均为测试或验证文档调整,不新增真实模型调用,不改变 Agent 发布输入。
正式发布仍要求当前候选自己的 CI/生产构建通过。
## 完整分支快照重新核对
`d9e1e42` 的 [Desktop CI 36340682594](https://github.com/mesalogo/goodbuddy/actions/runs/36340682594)
已通过原生会话采集焦点测试,但为 5,367 通过、51 跳过、4 失败。
核对祖先后确认准备期间另有四个并行提交进入分支:
`ee1bf8a` 输入区控件样式、`8e60736` 流式会话稳定排序、
`f35198d` 桌面通知开关、`d47f967` 监督结果继续讨论。
它们已随用户授权的当前分支推送;前述 Host 的 5,345 项通过使用较早源码快照,
不能作为包含这四项的新候选验证结果。
本轮已重新核对四个提交的实现和文档,补充两份功能矩阵与双语发布说明:
通知开关默认开启且不影响应用内提示;流式更新不改变排序;
监督结果可直接编辑问题并继续讨论,界面不再提供知识实体写回控件。
Agent 产品源码及 Runtime 锁未因这四项改变,Agent
[CI 36340682607](https://github.com/mesalogo/goodbuddy/actions/runs/36340682607)
源码校验和三个原生构建全部通过。
补齐通知默认值的契约 fixture;Obsidian 多次真实子进程调用使用单用例 15 秒时限,
避免前项超时后的在途调用污染下一项取消断言;笔记来源用例按面板查询控件,
并等前次来源操作释放禁用状态后再发起下一次。
这些修正不改变产品实现。Windows 聚焦回归 19 项通过,类型、lint 和说明校验通过。
从 Git tree `1758ff58a6ff33fcf7ddaa3d35dd2fcc57cd1265` 导出完整快照,
覆盖 Host 测试源码,而非只同步早期快照中的少数文件。
该完整快照 `npm test` 为 **5,371 通过、51 跳过、0 失败**,
451 个文件通过、15 个跳过,耗时 751.42 秒,包含全部四个并行提交。
最终提交前再次核对源码、测试、依赖和发布资源与该已验证 tree 一致,
仅追加本段验证结果;完整候选仍需自己的主分支 CI 与生产构建通过。
### 完整候选后的焦点同步修正
`5cdffee` 的 Agent [CI 36343184917](https://github.com/mesalogo/goodbuddy/actions/runs/36343184917)
三个原生构建和源码校验通过;桌面
[CI 36343184916](https://github.com/mesalogo/goodbuddy/actions/runs/36343184916)
为 5,369 通过、51 跳过、2 失败,生产构建未执行。
会话采集的焦点等待可被 `body` 后代文字误满足,菜单随后被真正的消息聚焦关闭;
另一个原生 Runtime 控件用例在启动完成前打开失焦关闭的菜单。
本轮收紧消息焦点的元素断言,并等待项目能力请求及启动渲染后再打开 Runtime 选项。
不重试菜单、不跳过场景、不改产品实现。
完整冻结 tree `2ddc0da69b6bcb42d8272c44dd11e3e6904d0438` 在 Host 全量
**5,371 通过、51 跳过、0 失败**,耗时 748.21 秒。
Windows 原生 Runtime 聚焦用例、类型检查和 lint 通过。
提交仅追加本段记录;仍须等待修正候选的主分支 CI 和生产构建。
`706726b` 的桌面 [CI 36345672365](https://github.com/mesalogo/goodbuddy/actions/runs/36345672365)
已通过上述采集与 Runtime 用例,但另一个外部知识库测试失败:
5,370 通过、51 跳过、1 失败,生产构建未执行。
该 fixture 只在查询新库 ID 时返回包含已创建库的快照,普通轮询会返回创建前数据;
同时错误地要求最后一次请求必须属于新库,忽略后台轮询。
改为模拟已持久化的库列表,等待新库请求发生,保留原库与新增库选中、被排除库未选中的
全部界面断言,不修改产品代码。Windows 完整 App 300 项通过,类型、lint 和说明校验通过。
冻结 tree `42a92450ad7d9ebc50167c7946bff53cff2889e6` 的 Linux 全量为
**5,371 通过、51 跳过、0 失败**,耗时 748.67 秒。
## Desktop 0.15.1 恢复
`65a21390debe0dd04871417a363a9d87a29339e6` 的主分支
[CI 36347785076](https://github.com/mesalogo/goodbuddy/actions/runs/36347785076)
验证与生产构建全部通过后,两个远端创建了不可变 `v0.15.0`。
其正式 [CI 36348938269](https://github.com/mesalogo/goodbuddy/actions/runs/36348938269)
两个 macOS 目标在 `codesign --verify --deep --strict` 失败,
报 `invalid destination for symbolic link in bundle`;GitHub Release 未发布。
Host 隔离 fixture 经实际 DS 资源准备和 electron-builder 文件复制,
复现相对 npm CLI 链接变成原构建目录的绝对链接。不是签名凭据或测试超时问题。
用户明确选择 **Desktop 0.15.1 + Agent 0.15.0** 恢复,不移动或复用旧标签。
修正仅让复制保留相对链接;新增回归移除原安装与暂存目录后验证最终链接仍可读。
该路径不用于 Agent,远程 Runtime 与 Agent 源码未改,不增加真实模型调用。
未发布 Desktop 0.15.0 的双语条目迁移到 0.15.1,保留全部功能说明;
Agent 版本不变,其包元数据从新版 Desktop manifest 派生最低版本 0.15.1,
两份说明和功能矩阵同步这个升级顺序。
原标签最终 Windows/Linux 四个目标成功,两个 macOS 失败,发布任务跳过,
公开 Latest 仍为 `v0.13.16`。
Windows 打包相关测试 45 通过、1 个 POSIX 链接用例跳过,类型、lint、
发布说明校验通过。冻结 tree `99d3271174a68922fbf0a1a4cfbadbb85a6067c8`
在 Linux Host 全量 **5,372 通过、51 跳过、0 失败**,耗时 745.88 秒;
新增符号链接回归实际执行通过。提交仅追加验证记录,恢复候选 CI 与 macOS 签名待验证。
## 数据、兼容与验收边界
Desktop schema 48 新增魔法笔记会话来源元数据。升级前应备份完整用户数据,
包括数据库及相邻 `notes/` 目录;旧客户端不能打开升级后的数据库。
本次发布准备不修改数据库迁移实现,也不直接改写用户数据库。
Agent 发布工作流以候选 `package.json` 设置最低 Desktop 版本,因此
Agent 0.15.0 包要求 Desktop 0.15.1;Node、OpenCode、Continue 锁不变。
Agent 与 Desktop 独立发布,Agent 不替代 Desktop Latest。
原生客户端的远程 coordinator、托管标准 Node 交付整合、Continue/OpenCode
Ask 内置 MCP 映射及完整跨平台安装包验收仍有缺口,见
[原生客户端进度](../features/assistant-workbar/progress.md#2026-09-27-local-native-client-validation)。
本次说明明确标为本地预览,不将这些未完成部分宣称为已提供能力。
@@ -0,0 +1,83 @@
# Desktop 0.15.10 发布准备
日期:2026-10-04。基线 `v0.15.9`,冻结已提交源码
`74efbcdc1703e6893764dbb2c48e3d1c47b21fce`,35 个提交。
仅纳入已提交实现和发布准备,保留并排除工作区中的导航、文档处理等并行修改。
标准六平台 Desktop,不含 LoongArch;Agent 保持已发布的 0.15.3。
## 范围与兼容性
- 统一项目与会话菜单、当前/全部范围、活动及十个最近会话、悬停预览。
- 监督者独立文本模型选择及默认开启的会话 Story Graph 开关。
- 活动差量保存、索引分页与完整汇总,schema 57 保留历史并迁移排序;
schema 58 增加启动恢复候选消息索引。升级前关闭应用并备份完整数据库
和相邻 notes 目录,旧客户端回退须恢复升级前备份。
- 文档解析 Worker、依赖延迟加载、本地工具环境后台准备、Composer 和
路由等渲染边界、笔记/文件树/知识文档窗口化、滚动和焦点保留。
- 终端 Main 合并与 xterm 帧批次,远程事件 16ms 定时 flush;
checkpoint 前落库、事务顺序和回放去重保留。
- 旧 Runtime 缓存按明确版本和布局命名清理,跳过符号链接;
保留当前布局、运行记录、会话和配置。工作栏间距及操作修复。
- 高频本地写入临时切换 NORMAL 并恢复原 PRAGMA;嵌套事务由外层决定。
保留应用崩溃原子性,但断电/系统崩溃可能丢失最近部分本地写入。
远程确认、通道投递和任务生命周期继续 FULL,后台 PASSIVE checkpoint
不代表已经验证破坏性断电恢复。
- Agent 守护进程只有测试变更,Continue host-v7 仅提取常量,协议和部署
行为未变,因此不发布新 Agent。Electron/Node/OpenCode/Continue 锁不变。
不承诺通用提速比例。窗口化未挂载行不支持页内查找,快速滚动可显示占位;
只读 Worker 失效可同步回退。开发报告中的 stable controller generation resume
及 Runtime 未到终态错误仍是调查项,本轮不声称修复。
## 本轮真实 Host 验证
中断前已针对 `e89db96` 源码使用固定 Host 身份与既有凭据存储,
在隔离测试目录经生产 Agent、模型桥、远程 ACP、RemoteEventBatcher
和 SQLite 持久化入口执行。后续 `74efbcd` 仅修改工作栏 CSS 间距,
不改变已验证的远程实现。OpenCode 探针显式使用已有 Continue 模型配置,
不是产品静默回退;未构建生产安装包。
**4 次真实模型请求,4 次 HTTP 200**:
| Runtime / 模式 | 批次 | 事件 | checkpoint |
| --- | ---: | ---: | ---: |
| OpenCode Ask | 3 | 4 | 2 |
| OpenCode Execute | 22 | 42 | 21 |
| Continue Ask | 3 | 4 | 2 |
| Continue Execute | 21 | 40 | 20 |
checkpoint 前落库、完整已保存流再次写入返回已存在、Runtime 重建后历史恢复,
本轮拥有进程清零。生产 SSH 连接池与终端会话另输出 1000 行 Unicode,
13 个输出事件;CRLF 归一化后内容完全一致、序号递增、输出先于退出。
最初终端探针选错 Host 路由,发生在模型请求前,真实调用数为零;
随后复用成功固定的路由完成。临时 Electron 缓存警告非致命,不算产品修复。
这些证据不是完整 Renderer、已安装 Agent 升级或全部断线恢复矩阵,
也没有验证所有消息投影恢复分支。开发阶段其他模型调用不计入本轮四次。
## 发布门槛
按用户持续授权,直接提交、推送并跟进 CI 和发布,不逐步询问。
不运行本地测试套件、类型检查、lint 或生产构建;完整验证和构建由精确候选
main CI 执行,通过后才创建新的不可变 Desktop 标签。双语说明与 FEATURES
同时更新;Agent 不打新标签。开发文档记载的未通过测试不能视为已解决,
由此次精确候选 CI 决定,失败须定位修复。
最终需核验六平台打包、macOS 签名与公证、GitHub Latest 和 20 项资产、
北京 OSS 索引一致性与网站 12 种下载选择。不下载或启动安装包,
不将候选或推送成功称为发布完成。
### 首轮候选 CI
`273a7fa` 的 Desktop CI `37174433765`:5953 通过、59 跳过、3 失败,
生产构建未执行;Agent CI `37174433777` 通过。
监督设置测试需要先进入“回顾整理”页签再操作其可见保存按钮,
Electron 布局测试更新为真实标题“模型与运行限制”,截图锚点限定可见设置面板。
保留保存值、再次打开恢复、布局及溢出断言。
1200 条活动历史测试会实际显示至 1000 行,专用超时从 30 秒改为 60 秒;
不减少数据、不移除分页、清空总数或离页裁剪断言,等待下一次 CI 验证。
`362fe50` 的 CI `37175597089`:上述三个测试均通过,5955 通过、59 跳过;
另一个工作区 KeepAlive 测试在默认一秒内未找到魔法笔记标题。
仅将该轮换测试的异步标题等待设为五秒,保留聊天 DOM 身份、旧工作区淘汰
及缓存数量断言。CI 观察命令曾出现 GitHub API EOF,最终失败以工作流结果为准。
@@ -0,0 +1,93 @@
# Desktop 0.15.11 / Agent 0.15.4 发布准备
日期:2026-10-04。基线为已发布的 `v0.15.10`,冻结源码
`ea3d607fdd16fe24bc4be6a172884ef4d14ef244`。中断后按用户最新要求纳入
全部已提交改动,开始准备时工作区干净;不纳入随后出现的未提交工作。
标准六平台 Desktop、三平台 Agent,不含 LoongArch。
## 范围与升级边界
- 无模式执行:删除 Ask/Execute、安全设置分类及通用 toolApproval,
普通请求使用启用能力和执行账号权限,内部文本任务保持无工具。
旧 Ask 会话不再有产品只读隔离,必须在发布说明中明确。
- schema 59 增加状态计时和会话汇总索引,不扫描旧历史补算时长;
schema 60 按已知 JSON 路径分批清理工作模式,并删除三列。
保留正文、图片、稳定引用、远程去重身份;损坏元数据可见失败并保留已提交批次。
Runtime 设置 22 清理顶层 toolApproval,保持加密凭据字节,不递归删除同名用户字段。
- 升级前关闭应用,备份完整数据目录(数据库、相邻 notes 与设置),
大库可能耗时,回退须恢复升级前备份。0.15.10 的 NORMAL/FULL 边界不变。
- 远程 runtime/acp 能力从 5 升到 6,新增工作区 importFile 动作及时间字段;
Continue helper 采用原生 agent。Agent 必须升级 0.15.4,最低 Desktop 0.15.11;
wire major/minor 保持 2.0,Node/OpenCode/Continue 锁不变。
更新前处理活动任务,不声称覆盖旧操作跨版本恢复。
- 工作区分块导入二进制与空文件,同名不覆盖,失败项独立反馈;按名称和时间排序。
每目录最多读取 500 条,排序只覆盖已读条目,无传输中取消和百分比。
移除成果页不删除数据库成果,不自动把会话附件或图片复制到项目目录。
- 图片仅消息引用也可发现,生成关闭时仍可保存;远程专家保持父请求执行空间。
- 项目菜单默认全部项目、归档恢复和明确进入,新本地项目必选目录、
远程目录符号链接、OCR 提供方与 PPTX 图片位置诊断、完整输入缓存率。
- 回顾来源初始化和续跑移到 Worker;不宣称最后发布大事务已移出 Main。
历史选择与布局统一,真实区间按主故事着色,空档中性、无归属事件保留;
消息密度来自当前消息/小时桶,非历史快照,不补造故事归属。
## 证据与限制
审阅开发记录和临时原始验证说明:
- `docs/features/unified-execution/progress.md` 与临时
`opencode/unified-execution-host/validation.md`:13 次真实文本请求。
OpenCode/Continue 无模式、原生工具、detach、controller 恢复、重放、
取消和续发检查;Continue 首次原生工具尝试暴露 readonly helper,
改为 agent 后工具及取消场景通过,每轮所属残留进程为零。
- 临时 `opencode/expert-image-validation.md`:12 次真实文本请求,
包含两次预算不足的准备尝试各 2 次及最终两个 Runtime 各 4 次。
Host 保存、冲突、显式覆盖及替换 PNG 字节一致,桌面保存次数为零。
这些是已有开发证据,累计 25 次文本请求、0 次真实图片生成;
本次发布准备未新增提供方请求。
- 工作区 Host 探针:当前源码 Desktop 访问层、协议方法和真实文件系统,
700001 字节二进制及空文件、同名冲突、时间与归属通过,模型调用零。
不等于已安装签名 Agent 完整 attach/传输矩阵。
- 图片保存 Host 探针验证 SQLite 重开和仅 artifactIds 引用图片的分块保存;
专家验证使用测试 PNG,不是提供方图片生成。
- 监督图谱定向测试、真实只读备份和 Electron 几何验证见监督进度;
真实样本是点事件,不冒充真实持续区间着色证据。新增样式不影响 Agent。
- schema 60 合成迁移及真实 Worker 夹具通过,但用户生产旧库完整迁移取样、
外部委派服务兼容性和跨版本旧远程操作恢复尚未覆盖。
各性能数值仅限原报告路径、数据和机器,不作为普遍速度承诺。
## 已有 CI 与准备状态
`e205e909` 的 Desktop CI `37202987733`:6032 通过、56 跳过、3 失败;
Agent CI `37202987727` 通过。失败为监督旧 inline-error 断言、
窄栏 Escape 后状态未就绪、文件导入默认五秒超时及其清理竞态。
这些定向修正已经包含在 `ea3d607f`:改查通知回调的语气和消息,
等侧栏 inert 后再改变尺寸,导入多块真实文件测试专用 15 秒。
不跳过场景,不减少数据,不删除保存、冲突、键盘或布局断言。
最新开发验证不是本次精确候选全量 CI,不记为全绿。
按用户要求,不跑本地测试套件、类型检查、lint 或生产构建;
只校验发布元数据后推送,由 main CI 验证和构建。
通过后创建新不可变双版本标签,跟进原生打包和独立发布;
不下载、哈希或启动 Desktop 安装包,不发布宣传文章。
双语 FEATURES 与发布说明同步权限、迁移、配套升级和验收边界。
### 精确候选首轮 CI
`245e296a`:Desktop `37211139053` 为 6038 通过、56 跳过、2 失败,
Agent `37211139043` 通过;生产构建未执行。
App 旧测试仍假定回顾表单默认展开,改为点击“新回顾”后保留原按钮断言。
覆盖层堆栈通过仅编译测试 driver(不运行应用或生产构建)定位到窄转宽后的
侧栏按钮,先前 Escape 等待已通过;补齐宽高与 CSS 过渡结束后再原生点击。
保留 hit-test、侧栏 inert 和后续归档/弹层操作断言,等待下一轮 CI 验证。
`29af4090` 的 CI `37212673749`:上轮两项通过,6039 通过、56 跳过,
仅工作区导入 Electron 探针失败(Renderer 脚本执行异常)。
探针取消选择后按旧名称查询按钮并直接读 disabled,导入中的临时名称会使
查询为空;改为等待匹配按钮存在且可用,点击也使用同一就绪条件,并在脚本
异常中保留执行表达式和原始 cause。字节核对、同名保护、取消和旧图片保留
断言均保留;此为探针时序修正,不宣称修复产品导入故障。
`4e72aa04` 的 CI `37213768876`:导入及此前修正项通过,6039 通过、
56 跳过,唯一失败为活动分页等价测试默认五秒超时。该测试四个种子、
三种筛选分别渲染新旧组件共 24 次;仅将此项设为 15 秒,完整快照、
分组、数量、标题、状态和清空计数对比不变。
@@ -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,48 @@
# Desktop 0.15.2 与 Agent 0.15.0 发布准备
日期:2026-09-28。用户要求包含当前本地工作、推送双远端,等待候选 CI 和生产构建通过后打标签;
确认同时发布 Agent 0.15.0,不构建 LoongArch。沿用此前自行审核候选和双语说明的授权。
## 发布范围
- Desktop 基线为已发布的 `v0.15.1`(`4e5239256d42c856bb1420d87c4a338f17867b80`)。
[发布 CI 36368878429](https://github.com/mesalogo/goodbuddy/actions/runs/36368878429)
六个原生目标和 GitHub/OSS 发布全部成功,0.15.1 的历史说明保留。
- 新源码截止 `66f77b83c2b6ebe0018a6c5c2bc72e1b4c1e43b1`:
renderer 退出诊断、笔记采集界面、双语网站及 Story Graph 只读工具与完整讨论草稿。
- Agent 0.15.0 尚未发布,基线仍为 `agent-v0.13.4`。
既有遥测和 Continue 更新检查修复保留,补充远程 Story Graph 工具;
最低 Desktop 版本由当前 manifest 派生为 0.15.2。三个原生目标和 Runtime 锁不变。
- 当前未提交的功能矩阵、主机/云环境设计和宣传文稿一并审核纳入。
云平台适配与主机入口迁移明确为待实现规划,不进入发布功能说明。
宣传稿保持草稿和原核查截止点,不代表已对外发布。
- 正式双语正文仅维护在 `resources/release-notes.json` 的 0.15.2 条目和
`resources/agent-release-notes/0.15.0.md`。两份 FEATURES 已对照实现与规划核对。
## 验证边界
Story Graph 已有开发期证据见
[监督者验证进度](../features/conversation-supervision/progress.md#2026-09-28-story-graph-只读工具):
真实 Linux Host 上的 OpenCode/Continue Ask、Execute 调用与禁用检查通过;
推理为本机确定性响应,10 次请求含 2 次标题生成,外部付费请求 0 次。
不将这些证据称为真实模型建议质量验证,也不宣称历史重建、跨范围事实合并或大库性能已验收。
本轮类型、lint、双语说明及网站静态校验通过。按当前源码重建隔离 Agent 探针,
在共享 Linux x64 Host 经固定身份完成 OpenCode/Continue Ask、Execute、空闲进程回收、
冷启动历史恢复和遥测关闭环境检查;每个 Runtime 2 次真实模型请求,共 **4 次**,
没有重试或假模型调用,结束时归属进程均为 0。该补充检查不等于真实模型图谱建议质量验收。
准备期间另有 `0cdf042` 宣传稿提交,已核对只有三份文档变化,产品源码仍截止 `66f77b8`,
一并纳入完整候选。用户随后中断本地全量步骤,明确要求直接推送等待 CI;
本地全量不记为通过,由精确候选的主分支 CI 执行完整测试和生产构建,通过后才打标签。
不运行本地生产构建、安装包打包或安装包启动;使用既有隔离 Host 目录,不更改生产 Agent。
schema 保持 48;从旧版升级应备份完整数据库和相邻 `notes/`,不能用旧客户端打开升级后的库。
## 首次候选 CI
`76e781a` 已同步双远端。Agent [CI 36379713606](https://github.com/mesalogo/goodbuddy/actions/runs/36379713606)
通过;Desktop [CI 36379713607](https://github.com/mesalogo/goodbuddy/actions/runs/36379713607)
5,389 通过、51 跳过、1 失败,生产构建未执行,未打标签。
失败的设置测试用固定五个服务器名称过滤按钮,却用新增 Story Graph 后的六项目录断言数量。
同步把旧模拟列表与按钮身份断言改为复用正式目录,保留数量和原交互断言,并逐项验证目录按钮存在。
最终完整 SettingsPanel 文件、类型、lint 和发布说明检查通过;全量继续交由新提交 CI。
@@ -0,0 +1,36 @@
# Desktop 0.15.3 发布准备
日期:2026-09-28。用户确认沿用双远端推送、精确提交 CI 和生产构建通过后自动打标签的流程;
本次仅发布 Desktop 0.15.3,已发布 Agent 0.15.0 保持不变,不构建 LoongArch。
## 范围
- 已发布基线:`v0.15.2`,提交 `47ba54f47db4f22f69375310925eedab020b3cf0`。
- 本轮已核对 15 个提交,截止 `86347af0b1cc8726c1166b3f9fa3d7f12a0cc5e3`。
包含手动压缩、请求级工具授权、取消/失败状态、聊天/笔记重复工作、
侧栏与工作栏布局、打包依赖精简。起始工作区干净。
- Office 同会话实时编辑仅更新需求与验收设计,不计入交付能力。
- `package.json`、lockfile 两处 Desktop 版本统一为 0.15.3。
已发布 0.15.2 与 Agent 0.15.0 的正文保持不变;新双语说明单独追加。
- 两份 FEATURES 对照实现更新手动压缩、状态反馈、搜索、箭头方向及发布状态。
不用局部 Markdown 基准宣称整体渲染性能提升,不提前承诺跨平台安装包缩小比例。
## Agent 与验证边界
`src/agent-daemon`、`src/main/remote-agent`、Agent 和 Runtime 锁在本轮未变。
授权修复属于 Desktop 的 `KnowledgeMcpGateway`:请求有效期间不再按固定十分钟撤销,
取消和清理继续撤销;不修改远程工具通道或部署包,不需要重发 Agent 0.15.0。
手动直连压缩仅用于本地文本会话,UI 与 IPC 均排除 SSH/远程直连入口。
开发期证据分别见[直连模型进度](../features/direct-model-agent/progress.md)、
[导航进度](../features/application-tool-navigation/progress.md)和
[性能基准](../roadmap/product-performance-experience-improvement-plan.md)。
开发期全量曾报告侧栏和笔记 Electron 用例失败,不把这些历史结果计为本候选通过。
本轮 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
网关、Obsidian 授权、桌面资源与 DS 打包、手动压缩真实 Electron 生产链路共 6 文件:
**85 通过、1 跳过**。跳过的是 Windows 不适用的 POSIX 符号链接用例。
压缩测试使用本机 HTTP fixture,不冒充外部模型调用;本轮没有新增付费模型请求。
沿用用户确认的直接推送等待 CI 流程,完整测试及生产构建以精确候选 CI 为发布门槛。
不进行本地生产构建、打包或安装包启动。
schema 保持 48;从更早版本升级前备份完整数据库及相邻 `notes/` 目录。
@@ -0,0 +1,55 @@
# Desktop 0.15.4 与 Agent 0.15.1 发布准备
日期:2026-09-29。用户确认两个补丁版本,提交并推送两个远端;
候选 CI 和生产构建成功后自动打标签发布,不构建 LoongArch。
## 范围与说明
- Desktop 已发布基线为 `v0.15.3`(`3c9229ac7a57071fc955adb1095c499ffdfd006a`)。
本轮源码截止 `9c88ca20f3c62ca29b9aca179dc191b460db37d8`,开始时工作区干净。
- 三个提交包含系统任务 Token 统计、Runtime/模型分层和宣传稿归档。
正式功能说明以实现为准;用途模型和旧全局 provider 移除仍为规划。
- Agent 基线为已发布 `agent-v0.15.0`。本轮 Continue helper 识别首次历史封装、
去除保存消息中的工作模式前缀,因此发布新 Agent 0.15.1,不覆盖旧产物。
Node 24.19.0、OpenCode 1.18.29、Continue 1.5.47 和协议锁保持不变。
- 正文仅维护在 Desktop 0.15.4 JSON 条目及 Agent 0.15.1 Markdown;
已发布版本正文保留。FEATURES 中修正旧的简化选择描述,并同步用量、版本和限制。
## 数据与兼容
schema 49 在既有 SQLite 事务中迁移项目与会话选择,不引入额外恢复系统。
已读迁移指南并核对合成 schema 48 数据迁移回归;本轮不改写用户原库。
关闭应用后备份完整数据库及相邻 `notes/`,旧客户端不能打开升级后的库,
降级需要恢复完整升级前备份。远程先升级 Desktop 0.15.4,再更新 Agent 0.15.1。
统计无法补算历史未记录的调用或缺失的供应商 usage。
## 验证
- `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
- 10 个聚焦文件、221 项通过:包含数据库迁移/用量分类、选择解析、选择器、
Activity UI、后台用量记录和 Continue helper。
- 当前源码隔离 Agent 在共享 Linux x64 Host 验证 OpenCode 与 Continue Ask、Execute、
空闲进程回收及冷启动历史恢复。成功场景每个 Runtime 两次真实模型请求。
OpenCode 原模型连接前三次尝试未完成,第三次脱敏诊断明确为上游 `ECONNRESET`;
随后测试显式使用 Continue 已配置且成功的模型连接,未修改用户设置或产品回退规则。
本轮共 7 次上游请求分发尝试,4 次成功、3 次未完成;不推断失败尝试的计费情况。
所有探针结束时归属进程为 0,用户设置只在临时加密副本中读取,测试副本已清理。
- 既有开发记录缺少完整 UI 目测及全仓通过结果,保留边界,不将局部通过冒充全量。
完整测试与生产构建交由精确提交 CI;不执行本地生产构建、打包或安装包启动。
## 首轮候选 CI
`ed47e2c` 已同步两个远端。Agent `36580699297` 三平台验证通过。
Desktop `36580699255` 为 5,472 通过、52 跳过、1 失败,生产构建未执行,
尚未创建任何本轮发布标签。问答测试在 Runtime 就绪前点击仍禁用的发送按钮,
没有产生首个请求;按现有异步同步机制等待按钮可用,保留全部问答与重载断言。
该修正仅影响测试和发布记录,运行时源码、锁文件及双语发布说明不变。
修正后完整 `App.test.tsx` 314 项通过,类型检查与 lint 再次通过。
## 发布前界面修正
用户要求移除会话 Runtime 按钮图标与名称间的圆点,并确认继续提交、推送和发布。
删除专用覆盖标记 class 及其伪元素样式,保留菜单内来源提示和恢复默认操作,
同步双语说明及选择设计。完整 App 与样式回归共 351 项通过,类型检查和 lint 通过。
该改动仅在 Renderer,不影响 Agent 或桌面至 Agent 协议,无需新增真实模型请求。
此前中断的测试不计为通过;本条结果来自重新执行。最终新提交仍须等待独立 CI。
@@ -0,0 +1,74 @@
# Desktop 0.15.5 发布准备
日期:2026-09-30。用户授权按新增改动确定版本、提交推送两个远端,
CI 与生产构建通过后自动打标签发布;本轮不构建 LoongArch。
## 范围
已发布基线 `v0.15.4`:`a662b16f5e076f67788457653d29c0d2fbacdab0`。
初始工作区干净,产品源码截止 `2d8e58e995c4ac2e3d885b028e588d49d06f05cf`。
四个提交分别为取消轮次历史、执行清单浮层、智能心跳增量回顾与监督建议、
笔记保存后退出编辑。双语正文维护在 `resources/release-notes.json`;
已发布正文保留,两个 FEATURES 同步当前能力和剩余边界。
Agent 保持 0.15.1,不发布新标签或修改其历史说明。Agent daemon、协议实现和
Runtime 锁没有变化;共享合同新增的是桌面监督配置/API,历史构建在 Renderer/Main
进行,远程继续通过现有 history 字段传输。因桌面至 Agent 历史输入改变,
仍对当前源码完成真实 Host 取消后续聊验证。
## 数据与行为边界
schema 50 在既有事务中增加心跳介入方式、建议状态和建议表;旧报告中仍 proposed
的记忆迁为 pending 约定,不确认或删除原记录。已读迁移指南并核对 49→50 回归;
没有读取或升级用户原库,也不声明大规模真实旧库性能已验证。
升级前关闭应用,备份完整数据库及相邻 `notes/`;降级需恢复完整升级前备份。
手动回顾默认增量,显式重新整理不推进或重置共享进度。建议以本次结果中的依据为准,
有候选才进行额外模型调用;项目与全局分别维护进度,会话级实时介入仍未实现。
本地 OpenCode 的取消会话标记不等同于 Agent 实现;远程使用共享桌面历史输入与
原有取消/会话恢复路径,不声称复用了本地 OpenCode 类。
## 验证记录
- 类型检查和 lint 通过。
- 首轮定向 34 文件:1092 通过、1 跳过、1 失败。会话合并用例只等待 Runtime
mock 调用便触发新会话,单独复跑通过;补充等待已发送消息可见,保留两次同批
新建请求和最终会话数量断言。最终 34 文件全部通过,1093 项通过、1 项既有条件跳过;
类型检查、lint、双语发布说明及差异检查再次通过。
- 当前源码在共享 Linux x64 Host:OpenCode、Continue 各在 Ask 输出后主动取消,
用生产 `buildRuntimeHistory` 生成中断历史,再以 Execute 发送无工具的新问题。
两条路径均只回答新问题,未续写原列表;归属进程最终为 0。
本轮恰好 4 次真实上游请求,全部 HTTP 200,无重试或假模型;用户设置只读取
临时加密副本,未修改正式 Agent 安装和工作目录。
- 智能心跳开发记录中的 4 次 DeepSeek 请求是该功能开发证据,不计入本轮新增调用。
参见[实施记录](../features/conversation-supervision/progress.md)。
- 不执行本地生产构建、打包或安装包启动。全仓验证和生产构建以最终提交的 CI
为准,标签只能在该门槛通过后创建。
## 中断后扩展候选
用户新增四个提交并授权再次推送继续发布;源码范围扩至
`6edfc0046b83150f2027167352000f161fb0dd0a`,恢复时工作区干净。
新增 OpenCode 终端依赖缓存、短 MCP 名称与并行占用、工作区固定工具栏、
笔记双栏布局及手动标签。说明和功能矩阵已同步,目标 schema 改为 51。
标签支持多选 AND 筛选、重命名/合并/删除及桌面笔记工具字段,非自动分类;
Agent daemon 和协议不变,Agent 仍为 0.15.1。
上一候选 `925d410` 的 Agent CI 通过,Desktop `36664224641` 为 5481 通过、
55 跳过、3 失败,生产构建未执行,未创建标签。两个笔记分析用例的旧编辑按钮
假设已由新提交修正;新源码真实 Electron 复验已通过。监督布局仍查找旧记忆建议
标题及已移除的心跳运行审计,修正为监督建议区域和两条建议断言、审计不重复展示,
保留页签、宽度、历史报告和导航检查。
扩展候选复验:15 文件、502 项通过,包含上述三个失败场景、笔记标签与布局、
工作区滚动布局、schema 51 数据库、MCP 网关/名称及原生终端缓存测试;
类型检查、lint、双语说明校验和差异检查通过。该轮测试使用隔离 fixture,
没有新增付费模型调用;先前 4 次真实 Host 请求仅对应取消后续聊范围,
不作为新增标签工具的完整远程 UI 实测声明。
`89fc100` 的 Agent CI 通过;Desktop `36707706849` 为 5507 通过、55 跳过、
2 失败,前述三个失败场景均已通过。笔记滚动在请求完成回调中早于 DOM 提交,
以立即执行动画帧的回归稳定复现后,改在布局 Effect 中定位已挂载记录。
手动压缩独立复跑通过,夹具同步当前分层选择格式并等待目标会话历史显示;
尚不将独立通过视为 Linux 全仓通过。修正后完整 App/笔记组件及真实 Electron
导航/压缩共 450 项通过,类型检查和 lint 通过,无新增外部模型调用。
@@ -0,0 +1,70 @@
# Desktop 0.15.6 发布准备
日期:2026-10-01。用户授权提交当前未提交改动并继续双远端推送与发布,
如 Agent 实际受影响可升补丁版本;另明确本轮仅标准六平台,不含 LoongArch。
## 候选范围
基线 `v0.15.5` 为 `3be55348a215f4343326b1950a69529247cbae6a`,
六个新增提交截止 `9cc5dc124464a67ed93014c53cbc93f96764caa2`。
初始未提交改动仅 `application-center.css` 一行选择器:
侧栏底部仅对第二个直接子元素文字 span 禁止伸缩,避免影响其他 span。
已核对并纳入本轮,未包含凭据、日志或临时产物。
新增内容包括磨砂外观、输入/缓存会话切换渲染优化、直接图片预览、设置与笔记布局、
本地 MCP 恢复及不确定名称退役、诊断和执行统计索引。
双语说明和功能矩阵按实现更新,不将已有开发记录中的全量失败记为成功。
Agent 保持 0.15.1:相对其发布标签,daemon 与锁文件无变化。
本地 OpenCode 的 HTTP 注册恢复属于 `OpenCodeRuntime`,远端使用
`RuntimeAcpBackend`/`AgentImageToolMcp`,不经过这条 HTTP 注册路径。
没有据此声称远程恢复得到同样改进,也没有新发布 Agent 包。
## 数据与验证
已读数据库迁移指南。schema 52 在事务内增加任务事件部分索引,不转换消息正文,
不执行 VACUUM;建立索引的耗时受历史规模影响,本轮未测真实大库升级。
升级前关闭应用并备份数据库和相邻 `notes/`,降级需要完整升级前备份。
- 当前源码 17 个定向文件、886 项通过,包含真实 OpenCode + HTTP 网关合成服务的
MCP 故障恢复和晚到初始化回归,数据库/统计、诊断、设置、App、图片附件和真实
Electron 磨砂、笔记布局、工作栏设置布局。
- `npm run typecheck`、`npm run lint` 通过;本轮新增付费模型调用 0 次。
- 完整测试和生产构建等待精确候选 CI,不运行本地生产构建、打包或安装包探针。
## 首轮 CI
候选 `cc451f6` 已含用户未提交样式修正并同步两个远端。Agent `36743915140`
通过,Desktop `36743915207` 为 5573 通过、57 跳过、1 失败,生产构建未执行。
磨砂测试切换主题后立即读取样式,窗口按钮 color 正在过渡,导致深色关闭态与
先前基线不同。测试改为等待当前 CSS Transition 完成,保留完整样式、几何、
拖拽、对比度及原生交互断言,不修改产品外观或全局超时。
修正后完整磨砂 Electron 回归、类型检查及 lint 通过;双语说明与产品源码不变。
`8201c9c` 的 CI `36746918608` 中磨砂回归通过,唯一失败为 161 条历史的多次
热切换 App 用例超过默认 5 秒(5573 通过、57 跳过)。只将该用例预算设为 15 秒,
保留消息数量、DOM 复用、零额外渲染、草稿、附件、展开状态和滚动位置的全部断言;
不修改全局超时或将运行时性能要求替换为等待通过。
调整后完整 App 319 项、类型检查及 lint 通过。
## 再次扩展候选
用户要求将当前未提交改动一起提交。新增 `0a02a17`(新建笔记入口)和
`a3b41bf`(设备共享元数据),并纳入五份应用导航文档修改。
设备共享需另行运行 ShareServer,首期面向本地未认证服务,仅注册设备及管理元数据;
不执行远程能力,不传输或检索知识正文,设备 ID 不构成所有权认证。
Agent 实现和锁不变,仍不升版。
扩展源码类型检查和 lint 已通过。用户随后明确要求停止本地测试、直接推送等 CI,
进行中的定向测试已中断,不计为通过,也不沿用旧候选的全量结果。
最终发布取决于包含新增提交和文档的精确候选 CI 与生产构建。
## 扩展候选焦点回归
`2c5300d` 的 Desktop CI `36753498640`:5580 通过、58 跳过、1 失败,
生产构建未执行;Agent CI `36753498643` 通过。
笔记导航用例在打开并取消带 `autoFocus` 标题输入框的新建表单后,
才断言此前打开详情时的返回按钮焦点,混合了两个不同交互阶段。
将原焦点断言移至打开新建表单之前,并断言新建标题输入框取得焦点;
保留返回列表、滚动位置、搜索内容和原卡片焦点断言,不改产品代码或延长超时。
按用户要求不运行本地测试,由新候选 CI 验证。
@@ -0,0 +1,53 @@
# Desktop 0.15.7 发布准备
日期:2026-10-01。基线为 `v0.15.6`,源码候选 `c8f327f`。
检查时工作区干净,用户要求纳入的先前改动已由该提交包含,共 75 文件。
本轮 Desktop 升至 0.15.7,Agent 保持 0.15.1,仅标准六平台,不含 LoongArch。
## 范围
- 流式 Markdown 分段复用、会话行与派生值缓存。默认 16ms 节奏不变。
- 运行记录进入时读取、可见时 15 秒刷新及页头手动刷新,不是逐事件实时视图。
- 本地自定义 MCP 发现与动态目录故障按服务隔离,取消和撤销仍生效。
- 模型连接中的受管模型下载源弹窗,确认保存、取消不变及相关入口统一。
- 设备共享默认关闭的测试入口,显式启用,关闭不撤销服务端记录。
网关修改只涉及本地自定义 MCP 发现方法;远程 Agent 的独立 MCP 实现与
Story Graph 绑定不经过这些方法,Agent 源码、锁及包未变。边界与开发验证见
[工作栏进度](../features/assistant-workbar/progress.md)。
数据库仍为 schema 52,无新增迁移。
## 证据与发布门槛
本轮遵照用户要求,不运行本地测试、类型检查、lint 或生产构建,由新候选 CI 验证。
已有开发会话记录不等于本候选全仓通过;网关会话的历史全仓失败也不掩盖。
流式输入测量见 [组件测量记录](../../tests/support/streaming-input-results.md),
不据此声称完整 App 或所有机器的相同比例提速。本轮外部模型调用为 0。
中英文 FEATURES 已核对设备共享默认值、下载源位置及实现边界,并补充本轮性能和活动行为。
发布说明保留 schema 52 备份提醒,不修改历史已发布条目。
双远端推送精确候选后,必须通过主分支全量验证与生产构建,并确认候选及双语说明后,
才能创建不可变 `v0.15.7` 标签。
## 首轮 CI
`227507d` 的 Desktop CI `36872224138`:5613 通过、59 跳过、2 失败,
生产构建未执行;Agent CI `36872224140` 通过。
英文设置布局 fixture 在 640×420 窗口将控件居中到包含固定页头的整个滚动区,
点击检测被较高的英文页头遮挡。测试共用滚动辅助函数改为扣除实际页头高度的
可见区域,仍检查命中、溢出、文字宽度和原生键盘交互,不改产品布局。
旧 outbox fixture 先建当前数据库,再降为 schema 23 并执行完整迁移,
超过默认 5 秒;仅该用例设 15 秒,保留终态、尝试次数及附件清理断言。
修复不修改发布说明或运行时代码,按用户要求继续交新候选 CI 验证。
## 第二轮 CI
`7cb74cc` 的 CI `36874700588`:首轮两项失败已通过,但仍有 5613 通过、
59 跳过、2 失败,生产构建未执行。检查提交链确认先前提交期间并行加入了
`5e77118`,移除文字输入框占位提示的问候语,App 单元测试已同步为两行,
Electron 侧栏探针却仍要求三行,导致断言失败。探针同步为两行并检查首行键盘提示,
保留平台快捷键、输入、清空、焦点、布局和搜索检查。
Markdown 用例逐一比较超过 1500 个流式前缀,超过默认 5 秒;
仅该用例改为 15 秒,保留全部 DOM 等价、前缀数及多段覆盖断言。
用户明确授权连续修复、双远端推送和 CI 通过后的标签发布,不再中途确认。
本轮仍不运行本地测试,以上修复须由下一次精确候选 CI 验证。
@@ -0,0 +1,57 @@
# Desktop 0.15.8 / Agent 0.15.2 发布准备
日期:2026-10-02。Desktop 基线 `v0.15.7`,Agent 基线 `agent-v0.15.1`;
已提交源码固定为 `b1129cedec5e4890d82fcddc186b13467f466a6f`。
仅纳入四个已提交功能修正及本次必要发布准备,不提交已有 Story Graph、
会话监督设计或宣传文章等未提交文件。
## 范围
- `dd29f30`:保存会话图片到本机或 Host、设置中列出图像工具。
- `f4d8fe7`:新记录取消、编辑焦点返回、笔记正文使用可用宽度。
- `2e5c348`:缺失内置浏览器设置时默认启用,保留已保存配置。
- `b1129ce`:缺失磨砂外观字段时默认开启,保留已保存配置。
- 数据库仍为 schema 52;Desktop 标准六平台,无 LoongArch。
- Agent 保存路径涉及守护进程、工具声明与桌面图像二进制回复,升至 0.15.2,
最低 Desktop 0.15.8;协议主版本及 Runtime 锁版本不变。
## 当前源码共享 Host 验证
共享 Linux x64 Host 的现有 SSH 固定身份可达。在独立 `/root/tmp` 目录运行
当前源码 AgentImageToolMcp,通过真实 SSH 传递工具消息,与 Desktop 的
MainImageToolSession、ImageGenerationService、真实 SQLite 会话协作。
图像为本轮生成的合法随机 PNG,196992 字节,超过单个 128 KiB 分块。
保存后的 Host 文件 SHA-256 与 Desktop 原始内容相同;默认覆盖拒绝、明确覆盖成功、
其他会话图片 ID 拒绝、Ask 写入拒绝且无文件落盘。共 8 个回复帧,
最大 174892 字节,低于 256 KiB 上限。外部模型调用 0 次。
这是生产保存服务与 Agent MCP 的真实 Host 工具层验证,不冒充完整 App、
模型自动选工具、安装包或三个 Host 架构验证。使用 SSH 流作为该探针传输;
未改变用户工作区、产品配置或 Host 已安装 Agent。
临时探针最初因打包 Zod 初始化顺序、混入仅 Desktop 依赖而无法启动,
修正探针依赖边界后验证通过;这些失败不计为产品模型调用或产品修复。
探针和包不进入仓库。用户要求不跑本地测试套件,精确候选由 CI
执行全量验证和生产构建后,才允许打 Desktop 和 Agent 不可变标签。
## 发布核对
中英文 FEATURES、Desktop 与 Agent 双语说明必须与上述实现边界一致。
只暂存必要发布文件,保留工作区用户文件;双远端核验候选和标签一致。
公开验证包括六平台 Desktop、三平台 Agent 签名目录、GitHub/OSS 同步及 Latest 分离。
## 首轮 CI 修正
`20317c2`:Agent CI `36960575058` 成功;Desktop `36960575157` 为
5620 通过、59 跳过、4 失败,构建未执行。
Obsidian 专项 IPC fixture 未安装浏览器服务,显式关闭无关浏览器能力;
MCP 列表断言纳入新增图像卡片的数量和名称;覆盖层原生点击先将控件居中,
避免磨砂顶栏覆盖 `nearest` 保留在顶部的控件,并补充命中遮挡诊断。
保留所有业务、覆盖层和原生交互断言,不改变产品默认值、关闭沙箱或跳过测试。
这些修正由新候选 CI 验证。
`5368e0e` 的 CI `36962139369` 中上述四项通过,余下新记录取消焦点用例失败,
5623 通过、59 跳过、1 失败。测试用 `fireEvent.click` 点击取消不会像原生点击
那样移动焦点,确认框记住的仍是先前的返回总览按钮。用例先等打开详情的焦点完成,
明确聚焦取消按钮后再点击,并等继续编辑实际聚焦编辑器后进行下一步;
保留放弃草稿后回到记录与未保存新记录的断言,不改产品焦点规则或延长超时。
@@ -0,0 +1,56 @@
# Desktop 0.15.9 / Agent 0.15.3 发布准备
日期:2026-10-03。Desktop 基线 `v0.15.8`,源码
`50481deaa1f7d17184500bb6f5abb3e1a0c8bbd5`,37 个提交、163 个文件。
检查时工作区干净,范围仅为已提交实现与必要发布准备。
标准六平台 Desktop、三平台 Agent,不含 LoongArch。
## 范围与边界
- 监督共享时间线、故事归属与人工调整、经验抽取、故事回顾与心跳建议;
平铺与时间螺旋、紧凑图谱控件及 WebGL 生命周期修复。
- schema 53~56:合并既有进度,增加故事、经验及建议类型,保留历史结果。
旧数据库副本验证和模型质量抽查见[监督进度](../features/conversation-supervision/progress.md),
不将 schema 53 的单库耗时当作本轮完整迁移保证。
- 会话外部存储、侧栏和消息窗口化、Markdown 缓存、只读 Worker、活动差量写入、
远程事件 checkpoint 批次持久化;保留丢失焦点、滚动、去重与提交顺序检查。
- Execute 配置工具不再重复弹原生确认,Ask 只读边界不变。
- Electron 44.5.1;Node、OpenCode、Continue 锁不变。
- Agent 虽无守护进程文件变更,但打包的 Story Graph 工具 schema 增加 story/experience,
旧 Agent 不支持这些输入,因此升级 0.15.3,最低 Desktop 0.15.9。
性能范围及暂缓项见[性能计划](../roadmap/product-performance-experience-improvement-plan.md)。
不把组件或特定机器的指标作为普遍提速保证;向量仍可全表扫描,Worker 失效会同步回退,
窗口化的未挂载行不支持页内查找,快速滚动可能短暂显示占位。
故事、经验、建议与输出重试有模型费用,自动结果须人工核对。
## 本轮真实 Host 验证
当前源码临时打包到隔离测试目录,不构建生产安装包。使用现有固定 Host 身份及
凭据存储,通过生产 Agent Attach、模型桥及远程 ACP 路径执行 OpenCode 与 Continue
各一次 Ask、一次 Execute。OpenCode 探针显式选择既有 Continue 模型配置,
不修改产品设置,也不是产品静默降级。
共 **4 次真实提供方请求,4 次 HTTP 200**,无模拟推理、无自动请求重试。
验证 Runtime 释放后重建、历史恢复和本轮进程清零。
每次消费真实远程语义流,通过生产 `RemoteEventBatcher` 与
`AssistantDatabase.appendRemoteTaskEventsOnce` 在 checkpoint 前落库;
对完整已提交事件再次写入均返回已存在。四次请求分别记录:
| Runtime / 模式 | 批次 | 事件 | checkpoint |
| --- | ---: | ---: | ---: |
| OpenCode Ask | 3 | 4 | 2 |
| OpenCode Execute | 21 | 40 | 20 |
| Continue Ask | 3 | 4 | 2 |
| Continue Execute | 23 | 44 | 22 |
验证没有启动完整 Renderer,也不声称覆盖会话消息投影的所有恢复分支。
Story Graph 新对象与来源语义由本轮源码测试及开发记录覆盖,以上四次请求不调用图谱工具。
开发文档中的模型调用不计入本轮 4 次。临时探针的两次编译钩子定位失败均发生在请求之前。
## 发布门槛
按用户要求,不执行本地测试套件、lint、类型检查或生产打包;
精确候选推送双远端后由 CI 做全量验证和生产构建,再打不可变双版本标签。
中英文 FEATURES 需同步共享进度、已实现故事经验、成本和窗口化限制。
公开核验 Desktop Latest、标准产物、Agent 签名目录、两个源及网站,保留任何并行未提交工作。
+339
View File
@@ -0,0 +1,339 @@
# GoodBuddy Release Runbook
Read this runbook only when preparing, publishing, recovering, or verifying a
desktop or GoodBuddy Agent release.
This is an operator and CI procedure. It does not authorize adding application
persistence, snapshots, journals, rollback engines, recovery protocols, or
compatibility layers to automate release operations. Determine release recovery
from the authoritative GitHub, OSS, workflow, and Git state, then use the
smallest existing idempotent rerun or a new immutable version as specified
below.
## Optional PR Article
Product publicity is an optional companion to a release, not a release blocker
or a replacement for the approved bilingual release notes. When requested,
follow the [PR article workflow](../pr-articles/README.md) and choose either the
[featured article](../pr-articles/README.md#重点稿模板) or
[regular update](../pr-articles/README.md#常规与综合稿模板) template based on user value,
not version size or commit count.
Prepare the draft and real screenshots against the candidate commit using the
[isolated screenshot procedure](../pr-articles/README.md#截图执行步骤). This does
not waive the build, packaging, or launch restrictions below. Keep missing
materials in the article's review record without delaying the software release.
Before external publication, verify the final version, download links, upgrade
notes, and screenshot accuracy. Preparing materials does not authorize posting
to a public channel; follow the user's explicit publication instruction.
## Release Packaging
- Unless the user explicitly requests it, never run a local production build,
desktop package, or launch probe as part of release preparation. In
particular, do not run `npm run build`, `npm run build:bundle`,
`npm run portable`, `npm run release:package`, direct `electron-builder`
packaging, or launch a locally packaged app before publishing a release.
Local tests, type checks, and lint plus the main-branch and native tag GitHub
Actions jobs are the release authorities.
- After a release is published, confirm only that the expected Windows assets
exist in the public release metadata. Do not locally rebuild or launch the
Windows package, download or hash published installers, attach browser
automation to a packaged app, or perform exhaustive per-asset network probes
unless the user explicitly asks for that additional validation.
- `.github/workflows/packages.yml` is the canonical cross-platform packaging
workflow. It validates and builds `out` once, then packages on six native
runners: Windows, macOS, and Linux, each for x64 and arm64.
- Linux source validation configures the installed Electron `chrome-sandbox`
helper with root ownership and mode `4755`, rehydrates the Electron
distribution without cache when the helper is missing, then runs the full
test suite under Xvfb. Keep Electron sandboxing enabled; do not skip UI tests
or add `--no-sandbox` to work around runner setup failures.
- The experimental Linux LoongArch (`loong64`) preview is built separately
through `build/loongarch-cross` by following
`docs/development/loongarch-preview-build.md`. It is not part of
`.github/workflows/packages.yml`, the six-runner GitHub Actions matrix,
standard release manifests, GitHub Release asset expectations, or the
production update index. Do not add it to those paths unless the user
explicitly requests a future production-support migration.
- Run the unified packager with
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
GitHub update readers and all older mirror readers require ZIP and cannot
validate this new matrix; affected users must upgrade manually from the
download page. The user approved this immediate format change. Current
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
- Final Desktop package verification compares every bundled OpenCode
configuration file with the prepared dependency tree and rejects external
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
imports the packaged OpenCode plugin and starts an isolated packaged
UtilityProcess before publishing; source-only tests do not replace it.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
- All desktop build paths, including local `portable`, package the Agent and
remote Runtime version locks plus the public signing-key registry without
embedding installable Linux Agent or remote Runtime payloads. Managed SSH is
optional: users explicitly download a compatible compound `.gbagent` package
from Settings or import one offline before GoodBuddy can install or update a
Host.
- Each independently published Linux x64, Linux arm64, or macOS arm64 `.gbagent` contains the
Agent daemon, pinned Node, and the desktop-maintained compatible OpenCode
and Continue Runtimes. Build all three targets on native GitHub Actions runners from one
immutable `agent-v<agentVersion>` tag.
- Keep the GoodBuddy Agent source, shared protocol/contracts, runtime lock,
bundle tooling, and tests in this repository so a desktop commit identifies
the exact Agent source it expects. Do not create a separate Agent repository
or long-lived Agent release branch unless the Agent becomes an independently
released product serving multiple clients.
- `.github/workflows/agents.yml` is the branch and pull-request build
verification workflow for GoodBuddy Agent. It must build Linux x64 and
arm64 on native runners from the checked-out commit, acquire the locked
official Node archive and locked OpenCode and Continue npm archives, verify every locked
digest, use only an ephemeral in-memory test signing identity, and verify the
resulting compound package and deterministic archive. It must not read
production signing secrets, publish installable release artifacts, or modify
the checked-in public key registry. The Darwin arm64 target uses `macos-15`
and the same production locks. Native CI checks do not replace real Host
installation, Attach, mode-free tool execution, and lifecycle validation during development.
- `.github/workflows/agent-release.yml` is the only production compound Agent
publication path. It requires an annotated immutable Agent tag, the protected
`agent-signing` Environment, native x64/arm64 builds, one production
GoodBuddy signing identity for every layer of the compound package and
catalog, a non-Latest GitHub Agent Release, and synchronized immutable
Beijing OSS objects plus the final signed latest-catalog pointer.
- `.github/workflows/packages.yml` and `release:package` build only desktop
products. Missing Agent packages, remote Runtime inputs, or GoodBuddy signing
credentials must never block an ordinary desktop package or release.
- Keep electron-builder invocations on `--publish never`. Main-branch builds
run validation and build the production bundle without running the native
package matrix. Manual builds upload 30-day GitHub Actions artifacts.
Version-tag builds verify and aggregate packages before publishing GitHub
Release assets. The macOS jobs sign, notarize, and verify packages when all
five Apple credentials are configured. With no Apple credentials they must
use the explicit `--unsigned` path, warn that Gatekeeper may block the
packages, and still complete; a partial credential set must fail rather than
silently downgrade.
- Keep `ELECTRON_CACHE` and `ELECTRON_BUILDER_CACHE` under
`${{ runner.temp }}` in step-level workflow contexts. A cache beneath the
repository inherits the root `"type": "module"` and breaks electron-builder's
CommonJS macOS icon tool.
- Tag builds must use `v${package.version}`. The workflow also supports manual
dispatch and main-branch changes to release tooling.
### Agent Release Process
- Agent releases use annotated `agent-v${agent-runtime-lock.agentVersion}` tags
and are separate from desktop `v${package.version}` releases. Confirm the
exact Agent release commit and tag with the user before creating or pushing
either.
- Store approved bilingual Agent release notes in
`resources/agent-release-notes/<agentVersion>.md`. The Agent publication job
reads that exact file instead of generating a generic release description;
keep it aligned with the approved version and source range.
- The tagged commit must be reachable from protected `main`. The three native
jobs build Linux x64, Linux arm64, and Darwin arm64 packages from the same source and locks;
the catalog job rejects missing architectures, changed bytes for an existing
version/platform/architecture identity, invalid prior signatures, and incompatible
matrix metadata.
- All three targets share the existing signed `agent-catalog.json`. Before the
first mixed-platform catalog is published, release the Desktop reader that
accepts Darwin entries and document the required Desktop upgrade. Older
Desktop readers reject the entire mixed catalog and cannot check online Agent
updates; preserve their locally verified packages and immutable historical
releases. Do not introduce a second catalog or silently weaken package
signature and integrity verification.
- Agent GitHub Releases are ordinary non-draft, non-prerelease releases but
must use `--latest=false`; they must never replace the desktop release marked
Latest. Publish immutable packages and the versioned catalog to
`agent-releases/v<agentVersion>/` in Beijing OSS, publish the GitHub Agent
Release, then atomically update the single `agent-releases/latest.json`
pointer to that version's immutable catalog and signature.
- Do not report an Agent release complete until both public package
architectures match the signed catalog by size and SHA-256, the GitHub and
OSS catalog bytes/signatures are identical, the application can read the
selected source, and GitHub's repository Latest tag remains the desktop tag.
### Tagged Release Process
Every version-tag release must follow this sequence. A branch-only push does
not require release notes.
1. Confirm that the user wants a release tag and identify the exact release
commit and the new `package.json` version. Separately ask whether this
release should also receive an experimental LoongArch preview build; never
infer that choice from an earlier release.
2. Find the latest stable version tag reachable before the release commit and
inspect the complete commit and file diff from that tag to the release
commit. For the first tagged release, inspect the relevant repository
history instead.
3. Draft concise, user-facing release notes in both Simplified Chinese and
English based only on verified changes in that range. Use the titles
`GoodBuddy <version> 更新内容` and
`What's New in GoodBuddy <version>`, with corresponding `功能更新` /
`Features` and `问题修复` / `Bug Fixes` sections when applicable. The two
language versions must describe the same changes. Do not expose
internal-only details, credentials, private content, or unverified claims.
4. Show the exact bilingual release-note draft to the user and wait for
explicit approval. If the release commit or either language version changes
after approval, inspect the updated tag range and request approval again.
5. Write the approved notes to the single entry for the release version in
`resources/release-notes.json`. A failed unpublished candidate whose content
is carried forward must not retain a duplicate packaged entry.
6. Verify that `package.json`, the root `package-lock.json` version, and
`package-lock.json.packages[""].version` all equal the release version. Run
`npm run release:notes:verify`, `npm test`, `npm run typecheck`, and
`npm run lint`. Do not run a local production build, packaging command, or
packaged-app launch probe unless the user explicitly requests one.
If the user requested a LoongArch preview in step 1, build it separately
from this exact candidate by following the LoongArch build document, record
its size and SHA-256, and keep it outside the standard GitHub/OSS release
asset set unless the user separately authorizes publishing it.
7. Commit the approved candidate and confirm the exact release commit with the
user. Fetch both remotes, inspect branch movement instead of overwriting or
silently merging it, then push that commit to `main` on `origin` and
`github`. Require the candidate commit's main-branch GitHub Actions source
validation and production build to succeed before tagging.
8. Fetch both remotes again immediately before tagging. Confirm the working
tree is clean, both remote `main` refs still equal the approved commit, the
candidate tag is unused locally and remotely, and the exact approved commit
has not changed. Only then create the annotated `v${package.version}` tag at
that commit, push the tag to both remotes, and verify each peeled tag SHA
(`refs/tags/<tag>^{}`) equals the release commit.
9. Keep both approved language versions as the single source for the GitHub
Release body and the packaged first-open release-notes modal. The modal
displays the release notes matching the current interface language and
contains no button linking to a full release page.
10. Observe the tag workflow through publication and complete the public
verification checklist below. A successful push is not a completed
release.
### OSS Publication Contract
The tagged release job publishes through the GitHub Environment selected by
`.github/workflows/packages.yml` (`aliyun-oss-release`) and reads the following
effective GitHub Actions variables. Before a release or same-tag rerun can
publish, verify that repository-, organization-, or environment-level
resolution exposes:
- `ALIYUN_OSS_BUCKET=goodbuddy`
- `ALIYUN_OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com`
- a non-empty `ALIYUN_OIDC_PROVIDER_ARN` matching
`acs:ram::*:oidc-provider/*`
- a non-empty `ALIYUN_ROLE_ARN` matching `acs:ram::*:role/*`
For environment-scoped values, use the exact Environment name from the
workflow; do not assume a similarly named UI environment such as `Production`
contains the active variables.
The Bucket and Endpoint are a deployment contract, not interchangeable
examples. They must stay aligned with the trusted URL checks in
`src/main/version-checker.ts`, `sites/app.js`, website validation, and related
tests. A host, Bucket, Region, or CDN migration must update and validate every
surface together before a new release.
- Use GitHub OIDC and the RAM Role to obtain short-lived STS credentials.
Never add long-lived AccessKeys to repository or environment secrets.
- Keep `ossutil` pinned. Its V4 signing requires the Region; derive it from the
canonical Endpoint, verify that the production value resolves to
`cn-beijing`, and pass `--region` to every `ossutil cp`, including the final
`latest.json` update.
- Grant the RAM Role only the actions and prefixes required by the workflow.
It must be able to write immutable version objects and the final latest
pointer without granting unrelated administration privileges.
- Upload release assets and `site-release.json` under the immutable
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
creating or publishing the GitHub Release. Update
`releases/latest.json` only after the GitHub Release is public and all prior
checks succeeded.
- The expected GitHub Release contains 20 assets: 12 installers (two formats
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
`site-release.json` is an OSS publication artifact, not a GitHub Release
asset.
### Failed Tag Recovery
Classify a failed tag by the external side effects that completed before
choosing a recovery:
- If no source or release metadata must change, correct only the external
configuration and use **Re-run failed jobs** for the same immutable tag. Do
not change, move, delete, or recreate the tag.
- If immutable OSS objects were partially uploaded but their source bytes are
unchanged, a same-tag rerun may idempotently re-upload or verify them. Never
point `latest.json` at a partially verified prefix.
- If the GitHub Release is already public but the final latest-pointer step
failed, it is a published version. Keep its packaged notes and rerun the
failed release job for the same tag; do not classify it as an unpublished
candidate.
- If code or release metadata must change, keep the failed tag immutable,
increment the patch version, obtain approval for the revised exact release
commit and notes, and create a new tag. Do not reuse the failed version.
When recovering from a version tag whose workflow never published a public
GitHub Release:
- If a code or metadata change requires a higher version and a new tag, carry
the failed candidate's approved user-facing notes forward into the recovery
version, then remove the superseded failed version's entry from
`resources/release-notes.json`.
- The packaged first-open modal must show that carried-forward content only
once under the recovery version. Never retain both the failed version and
its cumulative recovery copy, because users upgrading across them would see
duplicate content.
- Never remove the packaged history for a version that successfully published
a GitHub Release. Verify the failed release state before treating an entry as
superseded.
### Post-Publication Verification
Do not report a release complete until all of the following are verified:
1. The tag workflow and all six native package jobs succeeded. Verify the
recorded macOS signing mode: signed builds must pass `codesign`, `spctl`,
and `stapler`; unsigned builds must record the Gatekeeper caveat in the
Actions log and job summary. In the final release job, explicitly verify
the OSS configuration, OIDC authentication, release-index generation,
immutable upload, public asset check, GitHub Release publication, and
latest-pointer steps.
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
uses the expected tag and title. Its body must exactly match the Markdown
generated from the approved packaged bilingual notes.
3. GitHub release metadata contains exactly the expected 20 uploaded asset
names, including the expected Windows x64 and arm64 assets. Do not download,
hash, launch, or send individual network probes to published installers
unless the user explicitly requests that additional validation.
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
version, exact six targets and 12 installer entries, the trusted Beijing
URLs, and the GitHub fallback URL. It must match the immutable
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
metadata files is sufficient; rely on the successful publication job for
its per-asset public checks.
5. The live website successfully fetches the index and produces the 12 correct
platform/architecture/format links from that metadata.
6. Both remote `main` refs and both peeled tag refs still equal the approved
release commit, and the local working tree is clean.
Never create or push a release tag, and never push a previously created
release tag, before the release-note draft has received explicit approval.
- Before a push that updates the `github` remote, ask whether the user wants a
release tag unless they already specified that choice. A branch-only push
does not require a version bump or tag. When the user requests a release,
verify that `package.json` and `package-lock.json` contain the same release
version, create `v${package.version}` at the exact commit being pushed, and
push that tag so the native package matrix and GitHub Release run.
- Never move or reuse an existing release tag. If `v${package.version}` already
exists locally or on a remote at another commit, increment the package
version and create a new matching tag before the release push.
- Verified release baseline on 2026-08-18: commit
`60119a4317118fa3f077db0382664f15266a6682`, annotated tag `v0.10.4`, and
GitHub Actions run `32038633609` attempt 2 succeeded through all six native
packages, GitHub Release publication, Beijing OSS publication, and the final
`latest.json` switch.
+56
View File
@@ -0,0 +1,56 @@
# Desktop Window Recovery
This is recovery hardening for intermittent blank windows, not an established
diagnosis of their cause.
- `src/renderer/src/main.tsx` wraps the locale provider, App and storage-upgrade
page in `AppErrorBoundary`. A React render/lifecycle failure replaces the tree
with a blocking recovery page and an explicit Reload button. It uses the saved
UI language (including system preference resolution), shared theme tokens and
primary-button styling, without depending on the failed application providers.
- Reload refreshes the current document, retaining its URL and storage-upgrade
query. It can lose unsaved input. There are no automatic React retries.
- `src/main/window.ts` listens for `render-process-gone`. Abnormal exits open one
native dialog at a time, using the OS locale because renderer language storage
is unavailable to Main. Reload occurs only on explicit confirmation. Cancel
leaves the window unchanged; persistent failures require quitting and reopening
GoodBuddy. Clean exits, shutdown and destroyed windows do not trigger recovery.
- Main-frame load failures are logged; aborted navigation and subframe failures
are ignored. Initial load promises are caught, without automatic load retries
or a new load-failure UI.
- Native crashes and main-frame load failures use the existing bounded desktop
diagnostics store (`desktop.renderer.gone`, `desktop.renderer.load-failed`,
stage `renderer`). Persistent records retain fixed summaries rather than raw
errors. Renderer exit records also retain Electron's enumerated `reason` and
numeric `exitCode` when written, read back and exported; older records without
these fields remain readable. The exit details still go to the Main console.
React errors/component stacks remain console-only. There is no new diagnostic IPC.
The boundary does not catch module evaluation/startup failures before React
mounts, event-handler exceptions, asynchronous rejections, a hung renderer, or
main-process failure. Reload does not establish that a repeated underlying error
has been fixed. Agent runtimes and remote execution are unchanged.
Targeted regressions are in `AppErrorBoundary.test.tsx`, `window.test.ts` and
`desktop-diagnostics.test.ts`. Electron API mocks verify decision paths but do
not establish native dialog appearance or full application recovery.
Validation on Windows, 2026-09-15:
- Targeted Vitest run: 20 tests passed across the three files above.
- `npm test`: 359 files passed, 9 skipped; 4,225 tests passed, 67 skipped.
- `npm run typecheck`, `npm run lint` and `git diff --check` passed.
- An isolated Electron fixture bundled the current boundary, shared styles and
production window module. Both languages and themes at requested content sizes
1280x800, 960x720, 720x640 and 640x420 passed visibility, focus and horizontal
overflow checks. Windows scaling rounded actual widths down by one CSS pixel.
The 16-image overview and Chinese/light and English/dark short-window originals
were visually reviewed; no clipping or focus-style defect was found.
- A native Space key activated the recovery button and reloaded the document.
Two actual renderer crashes verified confirmed reload with URL retention and
cancellation without retry. Only native dialog responses were mocked; native
dialog appearance, full App business-state recovery and other OSes were not
verified. No model calls were made by this fixture.
- The first temporary Electron driver timed out during ESM startup. Removing its
top-level readiness await fixed the driver; the subsequent run passed. This
was a test-driver failure, not evidence of the reported application root cause.

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