Compare commits
62
Commits
agent-v0.15.5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b10ed583e | ||
|
|
f2f4c1273e | ||
|
|
ba30852058 | ||
|
|
12a0291279 | ||
|
|
3ebb4f0b79 | ||
|
|
c7f09e1e53 | ||
|
|
d9dcb65d10 | ||
|
|
1044a32b3b | ||
|
|
c6a8455f88 | ||
|
|
e07a17f79f | ||
|
|
4205ad6317 | ||
|
|
8f1b600798 | ||
|
|
dc582b7acf | ||
|
|
46a40e7fbf | ||
|
|
3556df8895 | ||
|
|
d7fb4299a3 | ||
|
|
4b4531abeb | ||
|
|
69f64df279 | ||
|
|
adb58d9200 | ||
|
|
47917094ad | ||
|
|
e2a5f83666 | ||
|
|
670b598530 | ||
|
|
1a59c2d880 | ||
|
|
13b58627c0 | ||
|
|
4266ff5bea | ||
|
|
377b14fca6 | ||
|
|
a406f5962e | ||
|
|
7f0a73e6d0 | ||
|
|
2d68d1043d | ||
|
|
4a2ea49069 | ||
|
|
277da413c6 | ||
|
|
e136026496 | ||
|
|
59a28209d3 | ||
|
|
9788b4cd8e | ||
|
|
c90ad2af6f | ||
|
|
95ece25c37 | ||
|
|
c4ab83a7e3 | ||
|
|
7eb113028f | ||
|
|
5ba127e332 | ||
|
|
3d063937c3 | ||
|
|
2a92f8f875 | ||
|
|
95f441958d | ||
|
|
384683e861 | ||
|
|
7eb51bd5ff | ||
|
|
9212fd4359 | ||
|
|
42ad8a98b4 | ||
|
|
fbfafa15e1 | ||
|
|
c1e4bbf66a | ||
|
|
3e3a021682 | ||
|
|
c465d39d58 | ||
|
|
13c29badfb | ||
|
|
e0607a153f | ||
|
|
425accf49d | ||
|
|
904bb691b8 | ||
|
|
4c25e85b08 | ||
|
|
8473189fe2 | ||
|
|
1f85d0c0e9 | ||
|
|
d80d42b032 | ||
|
|
bc677a3718 | ||
|
|
edf019aaa7 | ||
|
|
4bb7573b10 | ||
|
|
56b8b10d46 |
@@ -7,6 +7,8 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'sites/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/pages.yml'
|
||||
|
||||
permissions:
|
||||
@@ -29,8 +31,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
|
||||
- name: Validate website
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm run docs:build
|
||||
node sites/scripts/validate.mjs
|
||||
node --check sites/app.js
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ dist/
|
||||
# Local build output left by the extracted ShareServer project.
|
||||
shareserver/build-server/
|
||||
coverage/
|
||||
/temp/
|
||||
.vite/
|
||||
sites/docs-dist/
|
||||
sites/docs/.vitepress/
|
||||
.runtime-resources/
|
||||
.remote-runtime-resources/
|
||||
.agent-resources/
|
||||
|
||||
@@ -148,6 +148,8 @@ additional hardening beyond the actual internal-network threat model:
|
||||
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.
|
||||
@@ -305,6 +307,29 @@ Repository-wide release invariants:
|
||||
|
||||
## Validation
|
||||
|
||||
### Local Temporary Files
|
||||
|
||||
- Put all agent-created local temporary files under the repository-root `temp/`
|
||||
directory (`D:\my_git\goodbuddy\temp` in this checkout), in a task-specific
|
||||
subdirectory. This includes database copies, scratch scripts, test fixtures,
|
||||
logs, screenshots, and temporary build or validation artifacts. Do not put
|
||||
them in Windows Temp or `AppData/Local/Temp/opencode`.
|
||||
- Resolve this directory from the checkout root rather than hard-coding a
|
||||
machine path in scripts. Point test-tool temporary directory options and
|
||||
child-process `TEMP`, `TMP`, or `TMPDIR` here when needed. Leave production
|
||||
application data paths unchanged.
|
||||
- `temp/` is Git-ignored and disposable. Never store original user data,
|
||||
required backups, or the only copy of a deliverable there. Keep reusable
|
||||
validation scripts in `scripts/` or `tests/` and concise results in the
|
||||
owning feature documentation.
|
||||
- Clean task temporary files after validation, including database copies and
|
||||
their WAL/SHM files, on success and failure. Close owned processes and
|
||||
database connections first. Reuse a baseline copy where possible instead
|
||||
of retaining a full database for each test case. If cleanup is blocked,
|
||||
report the remaining path; do not silently accumulate copies.
|
||||
- Apply these rules to parallel agents as well. Remote Host temporary files
|
||||
continue to follow the dedicated remote-testing directory rules below.
|
||||
|
||||
Run validators after source changes when function/fix done, not on every source change:
|
||||
|
||||
```text
|
||||
|
||||
+27
-2
@@ -344,7 +344,7 @@ records are listed separately and do not introduce another feature status.
|
||||
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
||||
database. Successful tool START/END events appear exactly once, with no
|
||||
Prompt, provider, or tool replay observed. The current Agent source lock is
|
||||
`0.15.5`, paired with the current Desktop release candidate `0.15.12`; formal
|
||||
`0.15.6`, paired with the current Desktop release candidate `0.15.13`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels. Previous macOS validation covered native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
@@ -983,9 +983,34 @@ records are listed separately and do not introduce another feature status.
|
||||
|
||||
### Open source, builds, and releases
|
||||
|
||||
- The current Desktop candidate is `0.15.12`; Agent candidate `0.15.5` requires Desktop `0.15.12`, with
|
||||
- The current Desktop candidate is `0.16.2`; Agent `0.15.6` remains paired with Desktop `0.16.2`, with
|
||||
OpenCode pinned to `1.18.29` and Continue to `1.5.47`. Publication status follows the independent
|
||||
Desktop and Agent release channels.
|
||||
- Desktop `0.16.2` adds an all-time Supervisor review range and waits for messages and tasks to
|
||||
settle before including them in a review. It also improves Magic Notes action labels, contains
|
||||
terminal IME composition layout, and upgrades the database to schema 63.
|
||||
- Desktop `0.16.1` smooths the message-to-composer transition: messages extend to the composer
|
||||
outline before fading, and light/dark theme backgrounds and focus boundaries no longer create
|
||||
an abrupt solid band above the input. Schema remains unchanged.
|
||||
- Desktop `0.16.0` moves expert and team-role selection into the composer and persists the
|
||||
selection per conversation. Historical messages retain the role used for their request,
|
||||
including after a role is renamed or removed; schema remains unchanged.
|
||||
- Desktop `0.15.14` restores continuation for legacy Supervisor reviews that failed because a
|
||||
source changed or was removed. Sources that keep changing are re-frozen only within a bounded
|
||||
number of attempts and then omitted so the review can finish while retaining surviving progress.
|
||||
Settings categories also show matching icons to make the settings page easier to scan. Schema
|
||||
remains 62.
|
||||
- Desktop `0.15.13` / Agent `0.15.6` move desktop business SQLite ownership into
|
||||
one typed storage utility process and keep Remote Runtime changes coordinated.
|
||||
Assistant, Knowledge, attachments, runtime bindings and ledgers retain their
|
||||
paths and transaction semantics. Direct models continue after tool failures,
|
||||
while bounded paged output, Magic Notes tags, Supervisor aliases, settings
|
||||
preload and workspace metadata are corrected. Schema remains 62.
|
||||
Focused storage and Supervisor orchestration passed, but the required C01–C07
|
||||
concurrent acceptance matrix, cross-platform storage-process installation and
|
||||
low-end comparisons remain explicit follow-up gates; no universal performance
|
||||
claim is made. See the
|
||||
[preparation record](./docs/development/release-preparation-0.15.13.md).
|
||||
- Desktop `0.15.12` / Agent `0.15.5` add Telegram private-channel configuration
|
||||
(complete real-bot-plus-model acceptance remains pending) and custom local-date
|
||||
review ranges. Supervisor uses lazy graph/source reads, indexed Worker queries,
|
||||
|
||||
+6
-1
@@ -159,7 +159,12 @@
|
||||
### 开源、构建与发布
|
||||
|
||||
- Desktop `0.15.3` 修复本地直连手动压缩和请求有效期间的工具授权,区分取消与失败并去除重复提示,优化侧栏搜索、注册成功后的快捷键提示及笔记操作布局,减少启动和存储中的重复处理,并精简重复依赖及开发文件。不宣称整体渲染性能已显著提升;Office 实时编辑仍为设计。证据见[0.15.3 发布准备](./docs/development/release-preparation-0.15.3.md)。
|
||||
- 当前 Desktop 候选为 `0.15.12`,Agent 候选为 `0.15.5`、最低要求 Desktop `0.15.12`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
|
||||
- 当前 Desktop 候选为 `0.16.2`,Agent 保持 `0.15.6`、最低要求 Desktop `0.16.2`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
|
||||
- Desktop `0.16.2` 增加全部时间的监督回顾范围,并等待消息和任务完成后再纳入回顾;同时改进 Magic Notes 操作标签、修复终端输入法布局,数据库升级到 schema 63。
|
||||
- Desktop `0.16.1` 平滑消息与输入框的过渡:消息会延伸到输入框轮廓前再淡出,浅色和深色主题的背景与焦点边界也不再在输入框上方形成突兀的实色横带;schema 保持不变。
|
||||
- Desktop `0.16.0` 将专家和团队角色选择移入输入框,并按会话保存选择。历史消息保留请求当时使用的角色,即使角色之后被重命名或删除也不会丢失这项上下文;schema 保持不变。
|
||||
- Desktop `0.15.14` 恢复因来源变化或移除而失败的历史监督回顾的继续操作。持续变化的来源只会在有限次数内重新冻结处理,之后自动省略,使回顾能够结束并保留其余来源的进度;设置分类增加对应图标,方便快速查找;schema 仍为 62。
|
||||
- Desktop `0.15.13` / Agent `0.15.6` 将桌面业务 SQLite 的唯一 owner 移到一个类型化 storage utility process,Assistant、Knowledge、附件、运行绑定和账本保留原路径及事务语义。直连模型在工具失败后可继续,分页输出、Magic Notes 标签、监督来源别名、设置直达预加载和文件元数据同步修正;schema 仍为 62。storage/监督 focused 编排已通过,但 C01–C07 并发验收矩阵、跨平台 storage 进程安装和低端机对照仍是明确后续门槛,不作通用性能承诺。见[发布准备](./docs/development/release-preparation-0.15.13.md)。
|
||||
- Desktop `0.15.12` / Agent `0.15.5` 增加 Telegram 私聊配置(真实 Bot 与模型完整验收仍待完成)和本地日期自定义回顾。监督图谱/来源按需读取、索引 Worker 查询、摘要缓存和选择列表窗口化减少重复工作,最终发布仍可阻塞 Main;未发布回顾省略已删除会话并重算受影响批次,历史结果保留。临时模型失败最多多试一次,可能增加费用;远程启动取消、通道交付、长消息展开状态及历史缺失时长得到修正。schema 62 增加通道支持和查询索引,升级前备份完整数据目录,降级需恢复备份。两个独立开发审计阶段共记录 15 次外部文本请求,本次发布准备没有新增;精确源码 CI 与原生打包仍为门槛。见[发布准备](./docs/development/release-preparation-0.15.12.md)。
|
||||
- Desktop `0.15.11` / Agent `0.15.4` 移除 Ask/Execute 与通用工具审批,旧 Ask 会话也不再有产品级只读边界。schema 60 和 Runtime 设置 22 清理退休元数据,升级前备份完整数据目录,降级需恢复备份;远程需配套 runtime/acp 6 工件。工作区导入、文件时间排序、会话图片发现和远程专家保存随成果页移除一起提供,不删除既有成果。执行时长按运行状态计时、不补算旧历史,OpenCode 缓存率采用完整输入量。回顾历史、响应布局及螺旋实际区间着色保留未归属事件与中性空档;汇总读取和回顾初始化减少限定路径 I/O,不承诺普遍提速。开发证据含无模式执行与专家图片保存共 25 次真实文本请求,不是本次发布准备新调用或图片生成;精确候选 CI、签名包、公开验证和跨版本活动操作恢复与这些检查分开记录。见[发布准备](./docs/development/release-preparation-0.15.11.md)。
|
||||
- Desktop `0.15.10` 统一项目/会话菜单并显示十个最近会话,增加独立监督模型选择及默认开启的会话 Story Graph 开关。活动索引分页与完整汇总、文档 Worker、延迟启动、窄渲染订阅、列表窗口化、终端批次和旧 Runtime 缓存清理减少重复工作,不承诺通用提速比例。schema 58 要求升级前备份数据库及笔记;高频本地写入改用 NORMAL,断电/系统崩溃可能丢失最近部分写入,远程确认、通道投递和任务生命周期仍用 FULL。Agent 保持 0.15.3;四次真实 Host 模型请求及 1000 行 Unicode SSH 终端检查通过,精确候选 CI 与六平台打包仍为发布门槛,不宣称已修复尚未定位的远程恢复错误。见[发布准备](./docs/development/release-preparation-0.15.10.md)。
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
GoodBuddy is a desktop application for model chat, coding agents, knowledge
|
||||
retrieval, notes, and scheduled tasks. No GoodBuddy account is required.
|
||||
@@ -62,10 +62,6 @@ current limitations.
|
||||
|
||||
### Knowledge bases and graphs
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Magic Notes and Smart Heartbeat
|
||||
|
||||
Magic Notes displays a continuous record stream, with a narrow left index and
|
||||
@@ -77,10 +73,6 @@ default model's image-input capability, with an explicit text-only fallback when
|
||||
images are unsupported. See the [Magic Notes documentation](./docs/features/magic-notes/README.md)
|
||||
for behavior and final validation status.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Agent Runtimes
|
||||
|
||||
| Runtime | Functions | Configuration |
|
||||
|
||||
+1
-9
@@ -19,7 +19,7 @@
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时任务的桌面应用。使用 GoodBuddy 无需注册账号,工作数据默认保存在本机。模型服务可以运行在本机、组织内网或用户选择的云端服务。
|
||||
|
||||
@@ -47,10 +47,6 @@ GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时
|
||||
|
||||
### 知识库与知识图谱
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 魔法笔记与智能心跳
|
||||
|
||||
魔法笔记连续展示全部记录,左侧窄栏索引与右侧 AI 评论可独立收起,点击缩略图滚动定位并保留草稿。
|
||||
@@ -59,10 +55,6 @@ GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时
|
||||
默认模型的图像输入能力分析,不支持图像时明确提示仅文字降级。具体行为与最终验证状态见
|
||||
[魔法笔记文档](./docs/features/magic-notes/README.md)。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Agent Runtime
|
||||
|
||||
| Runtime | 适用场景 | 配置来源 |
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
- 页面顶部和底部内边距默认为 `32px`。
|
||||
- `master-detail` 的左栏建议为 `280px` 至 `360px`,右栏占剩余空间。分隔线和滚动容器属于壳层,不由内容卡片模拟。
|
||||
- 聊天输入区可以粘附在 `reading` 壳层底部,但消息内容和输入区必须共享同一阅读宽度。
|
||||
- 输入区外层不铺整块背景、不画横向分隔线。输入区以上内边距的高度向上叠入消息区,输入框上方保留 `--space-4`(16px)的透明到页面底色渐变,渐变终点对齐输入框上缘,让滚动的消息一直延伸到输入框轮廓前再淡出;输入框上缘以下才使用实色底;渐变位于输入框之下、不拦截鼠标事件,并随主题切换。输入框上缘只保留自身 1px 描边,不加向上的阴影,避免与渐变终点叠成粗边界。
|
||||
- 宽表格可在 `dashboard` 内容区内部横向滚动,不得撑宽整个应用窗口。
|
||||
|
||||
## 5. 信息层级
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"agentVersion": "0.15.5",
|
||||
"agentVersion": "0.15.6",
|
||||
"protocol": {
|
||||
"major": 2,
|
||||
"minor": 0
|
||||
|
||||
+102
-8
@@ -6,16 +6,17 @@
|
||||
const { spawn } = require('node:child_process')
|
||||
const { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs')
|
||||
const { createServer } = require('node:http')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { join, resolve } = require('node:path')
|
||||
|
||||
const root = resolve(__dirname, '..')
|
||||
const runnerRoot = resolve(__dirname, '..')
|
||||
const root = resolve(process.env.GB_PERF_ROOT || runnerRoot)
|
||||
const concurrentMode = process.env.GB_PERF_MODE?.startsWith('C')
|
||||
// 11434 is the fresh-profile default endpoint. Another GoodBuddy (or Ollama)
|
||||
// may already use it; GB_PERF_MODEL_PORT moves the fake model and points the
|
||||
// isolated profile at it through the GOODBUDDY_MODEL_* environment variables.
|
||||
const defaultModelPort = 11434
|
||||
const modelPort = Number(process.env.GB_PERF_MODEL_PORT || defaultModelPort)
|
||||
const totalTimeoutMs = Number(process.env.GB_PERF_TIMEOUT_MS || 600_000)
|
||||
const totalTimeoutMs = Number(process.env.GB_PERF_TIMEOUT_MS || (process.env.GB_PERF_MODE === 'C06' ? 900_000 : 600_000))
|
||||
const keepProfile = process.env.GB_PERF_KEEP_PROFILE === '1'
|
||||
const streamBytes = Number(process.env.GB_PERF_STREAM_BYTES || 40_000)
|
||||
const chunkChars = Number(process.env.GB_PERF_CHUNK_CHARS || 12)
|
||||
@@ -55,6 +56,11 @@ function startModelServer(ledger) {
|
||||
response.end(JSON.stringify({ object: 'list', data: [{ id: 'qwen3', object: 'model' }] }))
|
||||
return
|
||||
}
|
||||
if (request.method === 'GET' && request.url === '/c03-knowledge.txt') {
|
||||
response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
|
||||
response.end('C03 deterministic knowledge source\n'.repeat(80))
|
||||
return
|
||||
}
|
||||
if (request.method !== 'POST' || !request.url?.endsWith('/chat/completions')) {
|
||||
response.writeHead(404).end()
|
||||
return
|
||||
@@ -67,6 +73,75 @@ function startModelServer(ledger) {
|
||||
const messages = Array.isArray(parsed.messages) ? parsed.messages : []
|
||||
const last = messages.at(-1)
|
||||
const lastText = typeof last?.content === 'string' ? last.content : JSON.stringify(last?.content ?? '')
|
||||
if (lastText.includes('OUTPUT CONTRACT:') && lastText.includes('BOUNDED EVIDENCE:')) {
|
||||
const evidenceText = lastText.split('BOUNDED EVIDENCE:\n\n')[1]?.split('\n\nReturn only JSON.')[0]
|
||||
let sourceId = 'missing-source'
|
||||
try { sourceId = JSON.parse(evidenceText)?.[0]?.id || sourceId } catch { /* invalid fixture prompt is rejected by the production path */ }
|
||||
const navigation = lastText.includes('navigation summaries')
|
||||
const output = JSON.stringify({
|
||||
summary: 'Deterministic concurrent acceptance review', changeDigest: 'fixture', openItems: [],
|
||||
events: navigation ? [] : [{ title: 'Acceptance event', description: 'Deterministic review event', occurredAt: '2026-10-07T00:00:00.000Z', eventType: 'milestone', entityIds: ['acceptance'], sourceReferenceIds: [sourceId] }],
|
||||
entities: navigation ? [] : [{ id: 'acceptance', label: 'Acceptance', description: 'Deterministic fixture', sourceReferenceIds: [sourceId] }],
|
||||
entityChanges: [], relations: []
|
||||
})
|
||||
entry.kind = 'supervision-deterministic'
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
|
||||
const chunk = payload => response.write(`data: ${JSON.stringify(payload)}\n\n`)
|
||||
chunk({ id: 'supervision', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { role: 'assistant', content: output }, finish_reason: null }] })
|
||||
chunk({ id: 'supervision', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })
|
||||
response.end('data: [DONE]\n\n')
|
||||
entry.finishedAt = Date.now()
|
||||
return
|
||||
}
|
||||
if (concurrentMode && lastText.includes('gb-fault:hang')) {
|
||||
entry.kind = 'fault-hung'
|
||||
entry.identity = 'fault-hung'
|
||||
entry.stream = true
|
||||
entry.aborted = false
|
||||
// Keep the production request in flight until the UI cancels it.
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
|
||||
const timer = setTimeout(() => {
|
||||
if (!response.destroyed) response.end('data: [DONE]\n\n')
|
||||
}, 120_000)
|
||||
response.on('close', () => { clearTimeout(timer); entry.aborted = !response.writableFinished; entry.finishedAt = Date.now() })
|
||||
return
|
||||
}
|
||||
if (concurrentMode && messages.some(message => typeof message.content === 'string' && message.content.includes('gb-concurrent:'))) {
|
||||
const marker = messages.map(message => typeof message.content === 'string' ? message.content : '').join('\n').match(/gb-concurrent:(parent|child):([a-zA-Z0-9-]+)/)
|
||||
const child = marker?.[1] === 'child'
|
||||
const toolsDone = messages.some(message => message.role === 'tool')
|
||||
entry.kind = child ? 'concurrent-child' : 'concurrent-parent'
|
||||
entry.identity = marker?.[2]
|
||||
entry.stream = parsed.stream === true
|
||||
const durationMs = child ? 12_000 : toolsDone ? 1_000 : 12_000
|
||||
const count = Math.ceil(durationMs / 16)
|
||||
const calls = !child && !toolsDone ? [0, 1].map(index => ({ index, id: `child-${marker?.[2]}-${index}`, type: 'function', function: { name: 'subagent_delegate', arguments: JSON.stringify({ task: `gb-concurrent:child:${marker?.[2]}-${index} deterministic output` }) } })) : []
|
||||
entry.offeredFragments = count
|
||||
entry.offeredToolCalls = calls.length
|
||||
entry.chunks = 0
|
||||
entry.bytes = 0
|
||||
entry.latenessMs = []
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
|
||||
const send = (delta, finish_reason = null) => response.write(`data: ${JSON.stringify({ id: entry.identity, object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta, finish_reason }] })}\n\n`)
|
||||
const started = performance.now()
|
||||
const tick = () => {
|
||||
if (response.destroyed) { entry.aborted = true; entry.settledAt = Date.now(); return }
|
||||
while (entry.chunks < count && performance.now() >= started + entry.chunks * 16) {
|
||||
entry.latenessMs.push(performance.now() - started - entry.chunks * 16)
|
||||
const text = `[${entry.identity}:${String(entry.chunks).padStart(5, '0')}] sample\n`
|
||||
send({ content: text })
|
||||
entry.bytes += Buffer.byteLength(text)
|
||||
entry.chunks++
|
||||
}
|
||||
if (entry.chunks < count) { setTimeout(tick, 4); return }
|
||||
if (calls.length) send({ tool_calls: calls })
|
||||
send({}, calls.length ? 'tool_calls' : 'stop')
|
||||
response.end('data: [DONE]\n\n')
|
||||
entry.finishedAt = Date.now()
|
||||
}
|
||||
tick()
|
||||
return
|
||||
}
|
||||
const benchmark = lastText.includes('perf-stream')
|
||||
entry.kind = benchmark ? 'benchmark-stream' : 'auxiliary'
|
||||
entry.stream = parsed.stream === true
|
||||
@@ -153,10 +228,27 @@ function printSummary(report) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const taskDirectory = join(runnerRoot, 'temp', 'goodbuddy-concurrent-upgrade')
|
||||
mkdirSync(taskDirectory, { recursive: true })
|
||||
if (process.argv.includes('--launch')) {
|
||||
const { openSync, closeSync } = require('node:fs')
|
||||
const output = resolve(process.env.GB_PERF_OUTPUT || join(taskDirectory, `run-${Date.now()}`))
|
||||
mkdirSync(output, { recursive: true })
|
||||
const log = openSync(join(output, 'execution.log'), 'w')
|
||||
const child = spawn(process.execPath, [__filename], {
|
||||
detached: true, stdio: ['ignore', log, log],
|
||||
env: { ...process.env, GB_PERF_OUTPUT: output }
|
||||
})
|
||||
closeSync(log)
|
||||
writeFileSync(join(output, 'supervisor.json'), JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString(), root, mode: process.env.GB_PERF_MODE, timeoutMs: totalTimeoutMs }))
|
||||
child.unref()
|
||||
console.log(`Supervisor ${child.pid}; logs ${output}`)
|
||||
return
|
||||
}
|
||||
const required = ['out/main/index.js', 'out/preload/index.cjs', 'out/renderer/index.html']
|
||||
const missing = required.filter(file => !existsSync(join(root, file)))
|
||||
if (missing.length) throw new Error(`Missing production build (${missing.join(', ')}); run npm run build:bundle first`)
|
||||
const runDirectory = mkdtempSync(join(tmpdir(), 'goodbuddy-app-perf-'))
|
||||
const runDirectory = mkdtempSync(join(taskDirectory, 'profile-'))
|
||||
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
|
||||
mkdirSync(outputDirectory, { recursive: true })
|
||||
const ledger = []
|
||||
@@ -166,7 +258,8 @@ async function main() {
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot listen on 127.0.0.1:${modelPort} for the fake model (${error.code ?? error.message}); stop the local service using that port, or set GB_PERF_MODEL_PORT to a free port`, { cause: error })
|
||||
}
|
||||
const environment = { ...process.env, GB_PERF_DIRECTORY: runDirectory, GB_PERF_ARTIFACTS: outputDirectory, GB_PERF_ROOT: root }
|
||||
const environment = { ...process.env, GB_PERF_DIRECTORY: runDirectory, GB_PERF_ARTIFACTS: outputDirectory, GB_PERF_ROOT: root, TEMP: runDirectory, TMP: runDirectory, TMPDIR: runDirectory }
|
||||
if (concurrentMode) environment.GB_PERF_DRIVER_TIMEOUT_MS = String(totalTimeoutMs - 30_000)
|
||||
delete environment.ELECTRON_RUN_AS_NODE
|
||||
delete environment.ELECTRON_RENDERER_URL
|
||||
for (const key of ['GOODBUDDY_MODEL_API_KEY', 'GOODBUDDY_MODEL_BASE_URL', 'GOODBUDDY_MODEL_NAME', 'GOODBUDDY_BIGTOKEN_API_KEY', 'GOODBUDDY_BIGTOKEN_BASE_URL', 'GOODBUDDY_BIGTOKEN_MODEL']) delete environment[key]
|
||||
@@ -179,7 +272,8 @@ async function main() {
|
||||
environment.GOODBUDDY_WORKSPACE = join(runDirectory, 'workspace')
|
||||
mkdirSync(environment.GOODBUDDY_WORKSPACE, { recursive: true })
|
||||
const started = Date.now()
|
||||
const child = spawn(require('electron'), [join(root, 'tests', 'support', 'app-perf-driver.mjs')], { cwd: root, env: environment, stdio: 'inherit', windowsHide: false })
|
||||
const child = spawn(require('electron'), [join(runnerRoot, 'tests', 'support', 'app-perf-driver.mjs')], { cwd: root, env: environment, stdio: 'inherit', windowsHide: false })
|
||||
writeFileSync(join(outputDirectory, 'owned-processes.json'), JSON.stringify({ supervisorPid: process.pid, electronPid: child.pid, root, runDirectory, startedAt: new Date().toISOString() }))
|
||||
let timedOut = false
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
@@ -200,13 +294,13 @@ async function main() {
|
||||
total: ledger.length,
|
||||
benchmarkStreams: ledger.filter(entry => entry.kind === 'benchmark-stream').length,
|
||||
auxiliary: ledger.filter(entry => entry.kind === 'auxiliary').length,
|
||||
ledger: ledger.map(({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt }) => ({ method, path, kind, stream, chunks, bytes, aborted, durationMs: finishedAt ? finishedAt - startedAt : null }))
|
||||
ledger: ledger.map(({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt, identity, offeredFragments, offeredToolCalls, latenessMs }) => ({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt, identity, offeredFragments, offeredToolCalls, latenessMs, durationMs: finishedAt ? finishedAt - startedAt : null }))
|
||||
}
|
||||
if (timedOut || exit.code !== 0) report.status = 'failed'
|
||||
writeFileSync(resultPath, JSON.stringify(report, null, 2))
|
||||
printSummary(report)
|
||||
console.log(`\nreport: ${resultPath}`)
|
||||
if (!keepProfile) rmSync(join(runDirectory, 'profile'), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
||||
if (!keepProfile) rmSync(runDirectory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
||||
process.exitCode = String(report.status).startsWith('passed') ? 0 : 1
|
||||
// PERF-17: GB_PERF_CHECK=1 gates the run on build/perf-thresholds.json
|
||||
// (exit 1 on a regression, 2 when the report cannot be judged, e.g. throttled).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Knowledge retrieval benchmark (PERF-05). Bundles the production
|
||||
// Owner-local knowledge/read-worker microbenchmark (PERF-05), not a Main IPC probe.
|
||||
// Bundles the production
|
||||
// KnowledgeDatabase with esbuild, seeds synthetic libraries of increasing size
|
||||
// in a temporary SQLite file and times the synchronous search calls that the
|
||||
// Electron Main process runs inline during retrieval. Because these calls are
|
||||
// synchronous, their duration is the time Main is blocked per query.
|
||||
// storage owner runs during retrieval. Synchronous duration measures this harness's
|
||||
// event loop, not the Electron Main process of the application.
|
||||
const { buildSync } = require('esbuild')
|
||||
const { createHash } = require('node:crypto')
|
||||
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = require('node:fs')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
|
||||
|
||||
@@ -31,12 +31,16 @@ const dimensions = Number(process.env.GB_KPERF_DIMENSIONS || 384)
|
||||
const chunksPerDocument = 100
|
||||
const queries = Number(process.env.GB_KPERF_QUERIES || 15)
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-knowledge-perf-'))
|
||||
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
|
||||
mkdirSync(temporaryRoot, { recursive: true })
|
||||
const directory = mkdtempSync(join(temporaryRoot, 'knowledge-perf-'))
|
||||
const bundle = join(directory, 'knowledge-database.cjs')
|
||||
const workerBundle = join(directory, 'readonly-query-worker.cjs')
|
||||
const readerBundle = join(directory, 'readonly-query-reader.cjs')
|
||||
for (const [entry, outfile] of [
|
||||
['src/main/knowledge/knowledge-database.ts', bundle],
|
||||
['src/main/readonly-query-worker.ts', workerBundle]
|
||||
['src/main/readonly-query-worker.ts', workerBundle],
|
||||
['src/main/readonly-query-reader.ts', readerBundle]
|
||||
]) {
|
||||
buildSync({
|
||||
entryPoints: [join(root, entry)],
|
||||
@@ -51,6 +55,8 @@ for (const [entry, outfile] of [
|
||||
})
|
||||
}
|
||||
const { KnowledgeDatabase } = require(bundle)
|
||||
const { ReadonlyQueryReader } = require(readerBundle)
|
||||
const ownedDatabases = new Set(), readers = new Set()
|
||||
|
||||
// Deterministic pseudo-random generator so runs are comparable.
|
||||
let seed = 42
|
||||
@@ -76,7 +82,7 @@ function time(body) {
|
||||
}
|
||||
const round = value => Math.round(value * 10) / 10
|
||||
|
||||
// Main event-loop delay while `queries` searches run back to back. A sync
|
||||
// Harness event-loop delay while `queries` searches run back to back. A sync
|
||||
// search blocks the loop for its whole duration; a worker search should not.
|
||||
async function loopDelay(body) {
|
||||
const histogram = monitorEventLoopDelay({ resolution: 5 })
|
||||
@@ -103,6 +109,7 @@ async function main() {
|
||||
for (const size of sizes) {
|
||||
const path = join(directory, `knowledge-${size}.sqlite`)
|
||||
const database = new KnowledgeDatabase(path)
|
||||
ownedDatabases.add(database)
|
||||
database.initialize()
|
||||
const library = database.createKnowledgeBase({ name: `Perf ${size}`, storageMode: 'reference' })
|
||||
const sqlite = database.database
|
||||
@@ -119,6 +126,7 @@ async function main() {
|
||||
|
||||
// Reopen so timings start from a fresh connection, like an app restart.
|
||||
const reopened = new KnowledgeDatabase(path)
|
||||
ownedDatabases.add(reopened)
|
||||
reopened.initialize()
|
||||
const query = vector()
|
||||
const entry = {
|
||||
@@ -129,20 +137,24 @@ async function main() {
|
||||
vectorMs: time(() => reopened.vectorSearch({ knowledgeBaseId: library.id, provider: 'perf', model: 'perf-model', vector: query, limit: 40 })),
|
||||
hybridMs: time(() => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
|
||||
}
|
||||
// Main event-loop delay: synchronous search on Main vs the readonly worker.
|
||||
// Harness loop delay: owner-local synchronous search vs its explicit read worker.
|
||||
entry.mainLoopDelaySyncMs = await loopDelay(async () => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
|
||||
reopened.enableReadonlyWorker(workerBundle)
|
||||
entry.mainLoopDelayWorkerMs = await loopDelay(() => reopened.hybridSearchWithDiagnosticsAsync(hybridOptions(library.id, query)))
|
||||
const reader = new ReadonlyQueryReader('knowledge', path, workerBundle)
|
||||
readers.add(reader)
|
||||
entry.mainLoopDelayWorkerMs = await loopDelay(() => reader.call('hybridSearchWithDiagnostics', [hybridOptions(library.id, query)]))
|
||||
await reader.close()
|
||||
reopened.close()
|
||||
results.push(entry)
|
||||
console.log(`[kperf] ${size} chunks: fts p95 ${entry.ftsMs.p95} ms, vector p95 ${entry.vectorMs.p95} ms, hybrid p95 ${entry.hybridMs.p95} ms (seed ${entry.seedSeconds} s)`)
|
||||
console.log(`[kperf] ${size} chunks: Main loop delay max sync ${entry.mainLoopDelaySyncMs.max} ms vs worker ${entry.mainLoopDelayWorkerMs.max} ms (p99 ${entry.mainLoopDelaySyncMs.p99} / ${entry.mainLoopDelayWorkerMs.p99} ms; per query ${entry.mainLoopDelaySyncMs.perQueryMs} / ${entry.mainLoopDelayWorkerMs.perQueryMs} ms)`)
|
||||
}
|
||||
const output = process.env.GB_KPERF_OUTPUT
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-knowledge-retrieval-benchmark', node: process.version, results }, null, 2))
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-knowledge-retrieval-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
|
||||
console.table(results.map(r => ({ chunks: r.chunks, 'fts p50/p95': `${r.ftsMs.p50} / ${r.ftsMs.p95}`, 'vector p50/p95': `${r.vectorMs.p50} / ${r.vectorMs.p95}`, 'hybrid p50/p95': `${r.hybridMs.p50} / ${r.hybridMs.p95}`, 'loop max sync/worker': `${r.mainLoopDelaySyncMs.max} / ${r.mainLoopDelayWorkerMs.max}` })))
|
||||
}
|
||||
const hybridOptions = (knowledgeBaseId, vector) => ({ knowledgeBaseId, query: 'lighthouse budget', limit: 40, provider: 'perf', model: 'perf-model', vector, graphEnabled: false, candidateMultiplier: 1 })
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
|
||||
await Promise.all([...readers].map(reader => reader.close()))
|
||||
for (const database of ownedDatabases) database.close()
|
||||
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
||||
})
|
||||
|
||||
+25
-14
@@ -1,10 +1,11 @@
|
||||
// Main-process database benchmark (PERF-15). Bundles the production
|
||||
// Owner-local database/read-worker microbenchmark (PERF-15). Not a Main IPC latency probe.
|
||||
// Bundles the production
|
||||
// AssistantDatabase and readonly worker with esbuild, seeds a temporary
|
||||
// assistant database (default 1,000 conversations / 20,000 messages and 24,000
|
||||
// activity records) and measures, for the IPC paths the renderer hits often:
|
||||
// - per-call time
|
||||
// - Main event-loop delay while the calls run back to back
|
||||
// for the previous synchronous path ("sync") and the current path ("after").
|
||||
// - harness event-loop delay while the calls run back to back
|
||||
// for owner-local SQL ("sync") and explicit read-worker dispatch ("after").
|
||||
// conversations:list-summaries sync listConversationSummaries vs worker
|
||||
// conversations:get sync getConversation vs worker
|
||||
// activity-history:replace legacy replace (zod parse + BEGIN IMMEDIATE +
|
||||
@@ -17,7 +18,6 @@
|
||||
// GB_MPERF_CALLS, GB_MPERF_OUTPUT (JSON), GB_MPERF_NODE=1 (skip Electron).
|
||||
const { buildSync } = require('esbuild')
|
||||
const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { randomUUID } = require('node:crypto')
|
||||
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
|
||||
@@ -41,13 +41,17 @@ const messageCount = Number(process.env.GB_MPERF_MESSAGES || 20000)
|
||||
const activityCount = Number(process.env.GB_MPERF_ACTIVITY || 24000)
|
||||
const calls = Number(process.env.GB_MPERF_CALLS || 15)
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-main-perf-'))
|
||||
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
|
||||
mkdirSync(temporaryRoot, { recursive: true })
|
||||
const directory = mkdtempSync(join(temporaryRoot, 'main-perf-'))
|
||||
const bundle = join(directory, 'assistant-database.cjs')
|
||||
const workerBundle = join(directory, 'readonly-query-worker.cjs')
|
||||
const schemaBundle = join(directory, 'assistant-contracts.cjs')
|
||||
const readerBundle = join(directory, 'readonly-query-reader.cjs')
|
||||
for (const [entry, outfile] of [
|
||||
['src/main/assistant/assistant-database.ts', bundle],
|
||||
['src/main/readonly-query-worker.ts', workerBundle],
|
||||
['src/main/readonly-query-reader.ts', readerBundle],
|
||||
['src/shared/assistant-contracts.ts', schemaBundle]
|
||||
]) {
|
||||
buildSync({
|
||||
@@ -56,6 +60,8 @@ for (const [entry, outfile] of [
|
||||
})
|
||||
}
|
||||
const { AssistantDatabase } = require(bundle)
|
||||
const { ReadonlyQueryReader } = require(readerBundle)
|
||||
let ownedDatabase, reader
|
||||
const { activityHistorySnapshotSchema } = require(schemaBundle)
|
||||
|
||||
let seed = 42
|
||||
@@ -70,7 +76,7 @@ const percentile = (values, p) => {
|
||||
|
||||
/**
|
||||
* Runs `body` `calls` times (after one warm-up) and reports the per-call wall
|
||||
* time and the Main event-loop delay histogram (p99/max) over the run.
|
||||
* time and the harness event-loop delay histogram (p99/max) over the run.
|
||||
*/
|
||||
async function measure(body) {
|
||||
await body(-1)
|
||||
@@ -154,6 +160,7 @@ async function main() {
|
||||
mkdirSync(workspace)
|
||||
const path = join(directory, 'assistant.sqlite')
|
||||
const seeding = new AssistantDatabase(path)
|
||||
ownedDatabase = seeding
|
||||
seeding.initialize(workspace)
|
||||
const seedStarted = performance.now()
|
||||
const projectId = seeding.listProjects()[0].id
|
||||
@@ -186,6 +193,7 @@ async function main() {
|
||||
seeding.close()
|
||||
|
||||
const database = new AssistantDatabase(path)
|
||||
ownedDatabase = database
|
||||
database.initialize(workspace)
|
||||
const raw = database.database
|
||||
const totalMessages = counts.reduce((sum, value) => sum + value, 0)
|
||||
@@ -199,10 +207,10 @@ async function main() {
|
||||
results.listSummariesSync = await measure(async () => database.listConversationSummaries(detailIds))
|
||||
results.getLargestSync = await measure(async () => database.getConversation(largest))
|
||||
results.getTypicalSync = await measure(async () => database.getConversation(typical))
|
||||
database.enableReadonlyWorker(workerBundle)
|
||||
results.listSummariesWorker = await measure(() => database.listConversationSummariesAsync(detailIds))
|
||||
results.getLargestWorker = await measure(() => database.getConversationAsync(largest))
|
||||
results.getTypicalWorker = await measure(() => database.getConversationAsync(typical))
|
||||
reader = new ReadonlyQueryReader('assistant', path, workerBundle)
|
||||
results.listSummariesWorker = await measure(() => reader.call('listConversationSummaries', [detailIds]))
|
||||
results.getLargestWorker = await measure(() => reader.call('getConversation', [largest]))
|
||||
results.getTypicalWorker = await measure(() => reader.call('getConversation', [typical]))
|
||||
|
||||
// The renderer sends a fresh structured clone of the whole list every save.
|
||||
const unchanged = () => structuredClone({ records, legacyHistoryMayBeIncomplete: false })
|
||||
@@ -236,11 +244,12 @@ async function main() {
|
||||
results.activityFirstPageSync = await measure(async () => database.getActivityHistoryPage(firstPageRequest))
|
||||
results.activitySummarySync = await measure(async () => database.getActivityHistorySummary(summaryRequest()))
|
||||
results.activityStartupWorker = await measure(async () => {
|
||||
const page = await database.getActivityHistoryPageAsync(firstPageRequest)
|
||||
await database.getActivityHistorySummaryAsync({ conversationIds: [...new Set(page.records.map(record => record.conversationId))] })
|
||||
const page = await reader.call('activityHistoryPage', [firstPageRequest])
|
||||
await reader.call('activityHistorySummary', [{ conversationIds: [...new Set(page.records.map(record => record.conversationId))] }])
|
||||
})
|
||||
results.activityFullLoadBytes = JSON.stringify(database.getActivityHistory()).length
|
||||
results.activityFirstPageBytes = JSON.stringify(database.getActivityHistoryPage(firstPageRequest)).length
|
||||
await reader.close()
|
||||
database.close()
|
||||
|
||||
console.log(`[mperf] seeded ${results.conversations} conversations / ${results.messages} messages (largest ${results.largestConversationMessages}), ${results.activityRecords} activity records in ${seedSeconds} s`)
|
||||
@@ -264,9 +273,11 @@ async function main() {
|
||||
})))
|
||||
console.log(`[mperf] activity payload: full list ${results.activityFullLoadBytes} bytes, first page ${results.activityFirstPageBytes} bytes`)
|
||||
const output = process.env.GB_MPERF_OUTPUT
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-database-benchmark', node: process.version, results }, null, 2))
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-database-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
|
||||
}
|
||||
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
|
||||
await reader?.close()
|
||||
ownedDatabase?.close()
|
||||
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
||||
})
|
||||
|
||||
+20
-10
@@ -1,8 +1,8 @@
|
||||
// Main-process database WRITE benchmark on a real data directory (PERF-15).
|
||||
// Owner-local database WRITE microbenchmark on a closed data copy (PERF-15).
|
||||
// Copies assistant.sqlite (+ -wal/-shm) from GB_WPERF_DATA (for example the
|
||||
// portable build's data folder) into a temporary directory - the source is
|
||||
// never opened - then measures, with the production AssistantDatabase, the
|
||||
// writes Main performs synchronously while a run streams:
|
||||
// synchronous repository writes. This is not a production Main IPC latency probe:
|
||||
// task-events:subagent replay of the largest real subagent progress stream
|
||||
// (restored events, re-compacted on write, as live)
|
||||
// task-events:tool replay of real tool events
|
||||
@@ -10,14 +10,14 @@
|
||||
// conversations:save the renderer's 500 ms local save (header + the
|
||||
// streaming assistant message) for a real conversation
|
||||
// activity:update one incremental activity change
|
||||
// For each: per-call time and the longest Main event-loop block.
|
||||
// For each: per-call time and the longest owner-local harness event-loop block.
|
||||
// Event replays are paced at GB_WPERF_RATE events/s (default 270, the busiest
|
||||
// second in real data; 0 = back to back).
|
||||
// Env: GB_WPERF_DATA (required), GB_WPERF_EVENTS (default 2000),
|
||||
// GB_WPERF_OUTPUT (JSON), GB_WPERF_KEEP=1 (keep the copy).
|
||||
const { buildSync } = require('esbuild')
|
||||
const { mkdtempSync, rmSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { Worker } = require('node:worker_threads')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { randomUUID } = require('node:crypto')
|
||||
const { performance } = require('node:perf_hooks')
|
||||
@@ -43,7 +43,10 @@ if (!source || !existsSync(join(source, 'assistant.sqlite'))) {
|
||||
}
|
||||
const eventLimit = Number(process.env.GB_WPERF_EVENTS || 2000)
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-write-perf-'))
|
||||
const temporaryRoot = join(root, 'temp', 'goodbuddy-storage-foundation')
|
||||
mkdirSync(temporaryRoot, { recursive: true })
|
||||
const directory = mkdtempSync(join(temporaryRoot, 'write-perf-'))
|
||||
let ownedDatabase, checkpoint, checkpointExit
|
||||
const bundle = join(directory, 'assistant-database.cjs')
|
||||
const progressBundle = join(directory, 'subagent-progress.cjs')
|
||||
for (const [entry, outfile] of [
|
||||
@@ -115,6 +118,7 @@ async function main() {
|
||||
const workspace = join(directory, 'workspace')
|
||||
mkdirSync(workspace)
|
||||
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
|
||||
ownedDatabase = database
|
||||
const openStarted = performance.now()
|
||||
database.initialize(workspace)
|
||||
const raw = database.database
|
||||
@@ -124,9 +128,12 @@ async function main() {
|
||||
results.synchronous = raw.prepare('PRAGMA synchronous').get().synchronous
|
||||
if (process.env.GB_WPERF_AUTOCHECKPOINT) raw.exec(`PRAGMA wal_autocheckpoint = ${process.env.GB_WPERF_AUTOCHECKPOINT}`)
|
||||
// As in production: checkpoints run on a worker thread (GB_WPERF_CHECKPOINT_WORKER=0 disables).
|
||||
if (process.env.GB_WPERF_CHECKPOINT_WORKER !== '0' && typeof database.enableWalCheckpointWorker === 'function') {
|
||||
database.enableWalCheckpointWorker(join(directory, 'readonly-query-worker.cjs'))
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
if (process.env.GB_WPERF_CHECKPOINT_WORKER !== '0') {
|
||||
checkpoint = new Worker(join(directory, 'readonly-query-worker.cjs'), {
|
||||
workerData: { kind: 'checkpoint', databasePath: join(directory, 'assistant.sqlite'), intervalMs: 250 }
|
||||
})
|
||||
checkpointExit = new Promise(resolve => checkpoint.once('exit', resolve))
|
||||
await new Promise((resolve, reject) => { checkpoint.once('message', resolve); checkpoint.once('error', reject) })
|
||||
}
|
||||
results.walAutocheckpoint = raw.prepare('PRAGMA wal_autocheckpoint').get().wal_autocheckpoint
|
||||
const count = (sql, ...args) => Number(raw.prepare(sql).get(...args).n)
|
||||
@@ -208,6 +215,7 @@ async function main() {
|
||||
}
|
||||
results.walBytes = existsSync(join(directory, 'assistant.sqlite-wal'))
|
||||
? require('node:fs').statSync(join(directory, 'assistant.sqlite-wal')).size : 0
|
||||
if (checkpoint) { checkpoint.postMessage({ type: 'close' }); await checkpointExit; checkpoint = undefined }
|
||||
database.close()
|
||||
|
||||
console.log(`[wperf] ${results.taskEvents} task events, ${results.messages} messages, ${results.activityRecords} activity records; open ${results.openMs} ms`)
|
||||
@@ -226,10 +234,12 @@ async function main() {
|
||||
for (const [name, r] of rows) if (r.slowCalls.length) console.log(`[wperf] slow calls (index, ms) ${name}: ${JSON.stringify(r.slowCalls)}`)
|
||||
console.log(`[wperf] synchronous=${results.synchronous} wal_autocheckpoint=${results.walAutocheckpoint} wal=${results.walBytes} B`)
|
||||
const output = process.env.GB_WPERF_OUTPUT
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-write-benchmark', node: process.version, results }, null, 2))
|
||||
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-write-benchmark', executionBoundary: 'owner-local', node: process.version, results }, null, 2))
|
||||
}
|
||||
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
|
||||
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(async () => {
|
||||
if (checkpoint) { checkpoint.postMessage({ type: 'close' }); await checkpointExit }
|
||||
ownedDatabase?.close()
|
||||
if (process.env.GB_WPERF_KEEP === '1') console.log(`[wperf] kept ${directory}`)
|
||||
else rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Assistant Storage Consumers
|
||||
|
||||
The S02 domain consumers use the typed port in
|
||||
`src/main/assistant-storage-port.ts`. This implements the consumer side of
|
||||
[Desktop Storage Foundation](./desktop-storage-foundation.md). Production
|
||||
composition in `src/main/index.ts` supplies the client after storage readiness;
|
||||
the consumer services do not construct an Assistant database.
|
||||
|
||||
## Main Integration
|
||||
|
||||
```ts
|
||||
const storage = new DesktopStorageClient({
|
||||
...paths,
|
||||
onChanged: createAssistantStorageOnChanged({
|
||||
onMagicNotesChanged,
|
||||
onMagicTodosChanged,
|
||||
onModelUsageChanged,
|
||||
onExecutionStatsChanged
|
||||
})
|
||||
})
|
||||
await storage.ready
|
||||
const assistant = createAssistantStoragePort(storage)
|
||||
```
|
||||
|
||||
`createAssistantStoragePort` binds only the explicit Assistant operation allowlist
|
||||
and `supervisionContext`. Its return type is `AsyncAssistantStoragePort`: every
|
||||
operation returns a Promise. There is no SQL, arbitrary property proxy, remote
|
||||
callback or live repository API. `AssistantStoragePort` uses `Awaitable` results
|
||||
so existing synchronous database fixtures can be injected into unit tests.
|
||||
Domain change callbacks run in Main through `DesktopStorageClient.onChanged`.
|
||||
|
||||
The subagent service, native client coordinator, terminal manager, computer-control
|
||||
audit sink, channel dedup/outbox, image service and Magic Notes IPC helpers accept
|
||||
this port. Notes helpers retain sender validation, schema validation, source-message
|
||||
checks and revision checks. Note analysis awaits the model-usage callback before
|
||||
publishing its result. Channel and audit adapters return persistence completion to
|
||||
their existing asynchronous callers.
|
||||
|
||||
Image service `initialize`, `persistUploads`, `getOperation`, `cancel`,
|
||||
`cancelConversation`, `regenerate` and `dispose` return Promises. Await upload
|
||||
persistence before submitting generation. Await cancellation and shutdown writes.
|
||||
`onUsage` may return a Promise and must return the actual persistence operation.
|
||||
Notification-only `onOperation` errors are reported without changing provider
|
||||
outcomes; storage and usage-write failures reject instead of reporting durable
|
||||
success. Shutdown reports retained persistence failures.
|
||||
|
||||
Subagent event and model-usage callbacks may return Promises. The service awaits
|
||||
task creation, events and terminal status writes; shutdown also drains task
|
||||
creation and queued cancellation writes that have not entered a scheduler slot.
|
||||
Native client composition additionally forwards the storage-backed
|
||||
`openModelCallLedger` factory to both browser and terminal clients.
|
||||
|
||||
Stop producers and await service disposal before closing `DesktopStorageClient`.
|
||||
This consumer migration does not establish full desktop performance acceptance or
|
||||
replace the separate Main/IPC composition and concurrent acceptance work.
|
||||
|
||||
## Validation
|
||||
|
||||
The 11 focused suites pass with 94 tests, including asynchronous image storage,
|
||||
commit-before-notification, usage-write rejection, subagent shutdown during task
|
||||
creation, channel delivery, audit persistence and IPC sender/schema checks.
|
||||
Repository-wide type checking remains dependent on the concurrent Main/IPC,
|
||||
Knowledge and runtime-storage integrations. No latency or full concurrent-desktop
|
||||
acceptance claim is made by these unit and loopback tests.
|
||||
@@ -1,75 +1,388 @@
|
||||
# Desktop Storage Direction
|
||||
# Desktop Storage And Concurrent Execution
|
||||
|
||||
## Status and scope
|
||||
## Decision and delivery
|
||||
|
||||
2026-10-05: proposed target for the remaining PERF-15/PERF-16 work. The user has
|
||||
requested documentation first, followed by bounded local fixes and a commit.
|
||||
The full storage-service migration remains a separate discussion after those fixes.
|
||||
Current desktop storage still has Main-owned business writes and selected worker
|
||||
reads; this document does not describe a completed migration.
|
||||
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.
|
||||
|
||||
## Target responsibilities
|
||||
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.
|
||||
|
||||
- Renderer owns presentation, domain stores and disposable visible-data caches.
|
||||
It consumes summaries, pages and incremental notifications, not full database copies.
|
||||
- Electron Main owns windows, trusted IPC, credentials, OS integration and service
|
||||
lifecycle. Heavy SQL, JSON assembly and document processing execute elsewhere.
|
||||
- A desktop storage utility process would own business database connections and
|
||||
complete domain transactions. Retain the existing database files, schemas and
|
||||
durability distinctions. One write owner per database does not require merging databases.
|
||||
- Existing read-only workers can serve heavy queries under the storage service's
|
||||
lifecycle. Do not place every read and write behind one long-running global job.
|
||||
- Parsing, OCR, model inference and Runtime execution remain separate workloads.
|
||||
Remote Agent databases remain remote; desktop persistence acknowledges remote
|
||||
events only after the required local commit succeeds.
|
||||
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.
|
||||
|
||||
The service boundary consists of domain operations such as publishing a review or
|
||||
committing an event batch. Do not expose arbitrary SQL, transport transaction
|
||||
callbacks, or split an atomic transaction across individual IPC calls. Synchronous
|
||||
SQLite inside its dedicated execution environment can remain appropriate.
|
||||
## Requirements
|
||||
|
||||
## Correctness and lifecycle
|
||||
| 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. |
|
||||
|
||||
Preserve write-before-read ordering, result/checkpoint atomicity, cancellation
|
||||
settlement, event deduplication and commit-before-ACK. A rejected caller Promise
|
||||
does not establish that a writer stopped. Reuse existing operation IDs and unique
|
||||
constraints to resolve uncertain outcomes; do not blindly replay mutations or add
|
||||
a general recovery journal.
|
||||
## Responsibilities
|
||||
|
||||
Open business connections after required upgrades. Stop producers before draining
|
||||
required writes and closing storage. A failed storage process must not silently
|
||||
fall back to Main SQL or an empty persistent-data replacement. Keep bounded requests
|
||||
and explicit failure/retry behavior without adding product approval gates.
|
||||
```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]
|
||||
```
|
||||
|
||||
## VS Code comparison
|
||||
- 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.
|
||||
|
||||
VS Code is a reference for service separation, asynchronous work, lazy activation,
|
||||
cached UI state and shutdown coordination, not a requirement to copy its process map.
|
||||
Its application/profile/workspace state connections are owned by Electron Main;
|
||||
the backend uses asynchronous `@vscode/sqlite3`, while cached reads are synchronous
|
||||
and writes are batched. GoodBuddy's relational history, source bodies and durable
|
||||
remote-event acknowledgements require different payload and persistence boundaries.
|
||||
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.
|
||||
|
||||
Sources reviewed on 2026-10-05 (upstream main, not a pinned performance comparison):
|
||||
## 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)
|
||||
|
||||
## Current implementation boundary
|
||||
## One-delivery completion
|
||||
|
||||
The immediate work reuses existing workers, narrows Supervisor loading and payloads,
|
||||
optimizes measured queries, virtualizes lists, and removes repeated digest/snapshot
|
||||
work. It does not add the utility process, convert every storage interface, or claim
|
||||
exclusive ownership of assistant.sqlite. Read-only workers plus remaining Main writes
|
||||
are an explicit transition state.
|
||||
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.
|
||||
|
||||
Full migration should proceed through verified domain operations and converge by
|
||||
database ownership. Do not leave undocumented competing write paths. Completion
|
||||
requires no Main-owned desktop business SQL, transactions wholly inside the owner,
|
||||
responsive reads/writes under heavy work, and validated startup/cancellation/shutdown.
|
||||
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 [local implementation record](../review/kiss-local-fixes-2026-10-05.md) owns this
|
||||
round's scope and measurements. The [performance principles](./performance-principles.md)
|
||||
remain the acceptance criteria; regression ceilings are not equivalent to those budgets.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -5,11 +5,12 @@
|
||||
- **适用范围:** Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。
|
||||
- **用途:** 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。
|
||||
- **关联文档:** `docs/roadmap/product-performance-experience-improvement-plan.md`(测量记录与实施进度)。
|
||||
- **架构与验收:** [桌面存储与并发执行](./desktop-storage-direction.md)定义一次完整交付的职责和范围;[桌面并发验收](../quality/concurrent-desktop-acceptance.md)定义多项目、多会话、多子任务负载及证据。本文件是性能规范的唯一数值来源。
|
||||
- **参照:** VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。
|
||||
|
||||
## 1. 性能预算
|
||||
|
||||
“流畅”用下面的数字判断,不靠感觉。“标准机”指不降速运行 `npm run perf:app`,“低端机”指加上 `GB_PERF_CPU_THROTTLE=4`。
|
||||
“流畅”用下面的数字判断,不靠感觉。“标准机”指记录了硬件环境、不降速运行的参考机;Renderer 降速场景使用 `GB_PERF_CPU_THROTTLE=4`。该降速仅作用于 Renderer,不等于真实低配整机,也不模拟慢 Main 或磁盘。预算适用于单场景及规定的并发负载。
|
||||
|
||||
| 指标 | 标准机 | 低端机 |
|
||||
| --- | --- | --- |
|
||||
@@ -21,7 +22,9 @@
|
||||
| Main 事件循环延迟,p99 | ≤ 30 ms | — |
|
||||
| DOM 节点数 | 不随会话数、消息数增长 | 同左 |
|
||||
|
||||
标准机的阈值由 `npm run perf:check` 检查。低端机的数值目前只做记录,等结果稳定后再加进阈值检查。
|
||||
当前 `npm run perf:check` 使用历史回归上限,部分数值宽于本表,也未覆盖全部并发入口。检查通过不能宣称本表达标。目标交付须在验收报告中分别给出规范预算和回归上限的结果;低端机降速目前只做记录,真实低配和新增自动检查的完成状态另行记录。
|
||||
|
||||
点击反馈、所需内容可用和后台工作完成分别计时。显示加载状态只能证明及时反馈;不能用它替代结果就绪。每个进程分别报告长任务/事件循环和排队耗时,禁止把多个独立探针相加为端到端延迟。
|
||||
|
||||
## 2. 原则
|
||||
|
||||
@@ -32,6 +35,7 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
- 新代码不能在 Main 里使用 `DatabaseSync` 和同步文件 API。lint 已有白名单,名单只能减少。
|
||||
- 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
|
||||
- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
|
||||
- 目标交付后 Main 不再直接或通过 helper 打开桌面业务 SQLite;只读、失败回退、维护及退出均在检查范围内。当前迁移例外不是新增同步工作的许可。存储宿主可继续使用同步 SQLite,事务和计算仍需有界。
|
||||
|
||||
### P2 界面状态局部订阅
|
||||
|
||||
@@ -53,7 +57,8 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
|
||||
- `App.tsx` 只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。
|
||||
- 每个 store 的单元测试和 store 文件放在一起(`<domain>-store.test.ts`),覆盖顺序、失败重试和边界情况。
|
||||
- Main 侧对应:`<domain>` 的数据库访问集中在一个 repository 类里,IPC handler 只做校验和转发。
|
||||
- 领域数据库访问集中在 repository,目标架构中由存储宿主执行;Main 的 IPC handler 只做校验并等待领域操作。受管只读 Worker 与远端 Agent 按各自明确的连接归属执行,不能按源码目录把它们误判为 Main 访问。
|
||||
- 多项目/多会话流式更新只通知相关领域与实体;后台子任务不得让全部项目视图重渲染。隐藏视图保留草稿、选择和必要执行订阅,停止无关绘制与全量重读,切回时按已有摘要及增量恢复。
|
||||
|
||||
### P3 高频数据走专门通道,按帧合并
|
||||
|
||||
@@ -62,12 +67,16 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
|
||||
- 高频路径上不能读取布局(`getBoundingClientRect`、`scrollHeight` 等),布局读取要集中到每帧一次。
|
||||
- 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
|
||||
- 显示合并与持久化分开。可以合并进度或替换过时刷新,不能静默丢弃消息、工具结果或需确认的事件;多生产者缓冲只有在用户设置、外部协议、数据安全或实测故障证据要求时才设置容量,否则将负载交给所属 Host。
|
||||
|
||||
### P4 数据只有一份来源,只传变化部分
|
||||
|
||||
- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
|
||||
- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
|
||||
- 写入合并成批次和事务,不按每个事件单独开一个事务。
|
||||
- 一个数据库有一个业务写入所有者,完整事务不跨 IPC 拆成单条 SQL。Main 不保存第二份可写业务状态;依赖写入的读取等待对应提交,远程 ACK 等待所需事件与 checkpoint 提交成功。
|
||||
- 缓存必须说明归属、失效和释放条件,不按浏览过的项目/会话无限积累。查询返回摘要、分页或所需详情,保持完整信息可访问,不用截断历史满足预算。
|
||||
- GoodBuddy 自身的中转数据优先使用有界内存;已有完整记录或文件时复用引用,避免重复落盘。预览、最终回答与工具全文分别核实,不能以“已保存消息”推断全文已保存。需要超出内存保留的内容直接使用所属存储的唯一正文,分页读取同一内容,避免临时全文再复制到正式存储。必要文件及清理范围遵守[输出与临时资源设计](./desktop-storage-direction.md#output-and-temporary-resource-ownership)。
|
||||
|
||||
### P5 工作量和可见内容成正比
|
||||
|
||||
@@ -80,22 +89,26 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
- 首屏之前只做显示应用壳和恢复上次界面所需的工作。
|
||||
- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
|
||||
- 后台模块在第一次使用时才加载。
|
||||
- 冷启动、首次进入功能和暖返回分别验收;预加载只使用有界空闲工作,不能为每个项目预建进程、连接或整份历史缓存。进程 spawn 不代表服务就绪,首次可用反馈与数据实际可用分开记录。
|
||||
|
||||
### P7 先测量,再修改
|
||||
|
||||
- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
|
||||
- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
|
||||
- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
|
||||
- 必测多项目、多会话、多子任务及导入/检索/回顾发布重叠场景,负载和采样按[并发验收](../quality/concurrent-desktop-acceptance.md)。记录 offered load、实际吞吐、排队和排空、资源高水位;不得降低输入速率、串行化夹具或丢事件来取得流畅结果。
|
||||
- 基准使用固定版本、数据和环境。性能测量与真实模型功能验收分开;受发布构建限制时遵守发布手册,不以性能原则授权额外生产打包。
|
||||
|
||||
### P8 正确性优先于速度
|
||||
|
||||
- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
|
||||
- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
|
||||
- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
|
||||
- 取消请求、执行结算和容量释放不是同一时刻;调用方停止等待不能提前释放仍运行的工作。取消一个会话或子任务不得杀死仍服务其他任务的共享宿主。已提交事务如实报告成功,不伪装成回滚;未确认写入不盲目重放。
|
||||
|
||||
### P10 体验不倒退
|
||||
|
||||
性能优化只能让体验变好,不能用体验换速度。“行为不变”不仅指数据和结果,也包括用户能感知到的交互:
|
||||
性能优化必须保留数据、结果和用户交互语义,不能用体验换速度:
|
||||
|
||||
- **动画和过渡保持原样。** 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入 `scrollTop`/`scrollTo` 的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。
|
||||
- **只渲染可见部分(P5)时,用户状态不能丢。** 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过 `<details>` 或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。
|
||||
@@ -109,6 +122,25 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
|
||||
- 规则只能收紧,白名单和上限只能往下调。
|
||||
- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
|
||||
- 检查直接 SQL 导入及已知间接打开函数/构造点,按实际宿主区分测试、存储与远端代码。白名单和上限需要与已提交基线比较,不能只靠注释约定;全局同步回退同样要被发现。
|
||||
- 普通 CI 与性能执行环境分别提供功能和预算证据;新增并发与首次进入场景纳入候选必需检查,缺少运行环境或结果时保留验收缺口,不由单测通过推断达标。
|
||||
|
||||
### P11 并发隔离与有界工作
|
||||
|
||||
同一会话的事件顺序和同一数据库的提交顺序必须保持;不同项目、会话、子任务的模型等待、重查询与清理不因此共用一把长时间全局锁。
|
||||
|
||||
- Main 全局控制队列不得等待模型、网络、原生启动或耗时清理。必要登记和状态提交可短暂串行,等待在所属操作范围内完成。
|
||||
- 重查询复用有界 Worker;必要写入不排在整个导入解析或模型请求之后。长事务先减少事务内无关计算,再测写入排队,不把阻塞简单搬进唯一存储队列。
|
||||
- 复用现有池、批处理和队列,记录在途数、排队数及实际资源使用。只有有明确依据时才设置容量;没有依据时将负载交给所属 Host,不因内部数字拒绝用户工作。有实测饥饿才引入最小调度调整,不预建通用优先级框架。
|
||||
- 控制和取消不等待新的执行槽位;如果外部或用户设置要求排队,按该语义执行。禁止静默丢失、无限重启或自动扩增进程;不能把未经证明的队列上限包装成稳定性。
|
||||
- `ReadonlyQueryReader` 的生产 storage 模式不传 `maxPending`,因此请求直接透传给 storage host;其等待队列只服务显式有界的调用方和测试,不构成第二个 storage owner。
|
||||
- 每个异步响应携带已有项目/会话/任务/操作归属;迟到响应不得覆盖新选择或新操作。归属检查先于改变共享请求代次、缓存或队列状态。
|
||||
|
||||
### P12 架构完整性与 KISS
|
||||
|
||||
一次性交付按最终职责边界验收,不以全面 Promise 化或局部热点迁出代替。目标范围内正常、后台、失败、重试、取消和退出路径均使用同一权威实现,清理原直接入口和回退。
|
||||
|
||||
保持现有技术栈、数据库文件及业务合同,复用已有资源机制。不得顺带新增分布式事务、持久 RPC receipt、通用恢复框架、每项目独立服务或产品审批模式。同步内存缓存读取可以保留,不把纯函数和每个 getter 远程化。设计新增机制必须指出当前失败、现有机制的不足与对应验证,不以“将来可能并发”代替证据。
|
||||
|
||||
## 3. 评审清单
|
||||
|
||||
@@ -125,21 +157,30 @@ Main 负责窗口、生命周期、权限和消息转发。数据库查询、文
|
||||
9. 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
|
||||
10. 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
|
||||
11. 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
|
||||
12. 多项目/多会话/多子任务同时运行时,取消或清理一个请求是否拖住其他任务?队列和缓存是否持续增长?
|
||||
13. 存储提交、远程 ACK、调用方返回和资源释放的顺序是否有真实路径及故障注入验证?
|
||||
14. 是否删除目标范围内的旧直接访问和同步回退?新增进程或调度机制是否复用现有设施并有实际必要性?
|
||||
15. 请求或输出是否产生非必要临时文件、重复全文?是否核实完整内容与预览的区别,并验证分页、引用释放、必要文件的创建失败清理及升级后的残留回收?
|
||||
|
||||
## 4. 当前差距
|
||||
|
||||
截至 2026-10-04(第一期结项,见 `docs/review/performance-phase-1-report-2026-10-04.md`),按原则对照的主要差距:
|
||||
截至 2026-10-05,以下状态结合[存储升级实施记录](../quality/storage-upgrade-implementation-2026-10-05.md)和[并发验收规范](../quality/concurrent-desktop-acceptance.md)核对。表中“已实现”只表示代码或定向测试已有证据,不表示 PERF-15、PERF-16 或 C01-C07 已完成。
|
||||
|
||||
| 原则 | 差距 |
|
||||
| --- | --- |
|
||||
| P1 | 同步数据库调用仍在 Main,没有独立数据进程;但热点读已在 worker,高频写单次 0.1–0.3 ms,WAL checkpoint 在后台线程,启动恢复按索引读取。文档解析、分块和大文件哈希已移到 worker。消息附加数据冗余(子任务工具输出、`tools` 与 `blocks` 重复),大会话读取约 170 ms |
|
||||
| P1 | Assistant、Knowledge、文件和 Runtime 生产域已由 `DesktopStorageClient` 接入 utility process;剩余缺口是全量间接访问扫描、异常/维护/退出路径核验,以及 Main 阻塞和存储排队的并发测量。文档解析、分块和大文件哈希仍在各自 worker。 |
|
||||
| P2 | `App.tsx` 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App |
|
||||
| P3 | 已基本满足:流式输出和终端输出都已按帧合并 |
|
||||
| P4 | 活动记录已改为增量保存、按页读取(界面只保留已加载的页,计数和分组来自 Main 的汇总);部分 `*Changed` 通知之后还会重新查全量 |
|
||||
| P5 | 已基本满足:消息、会话、笔记、文件树、知识库文档表格都只渲染可见部分 |
|
||||
| P6 | 空配置启动到输入框可用约 0.96 秒;真实数据(2.4 GB)约 1.36 秒。本地工具环境已后台初始化,低频模块按需加载 |
|
||||
| P9 | 性能阈值还没有接入 CI,也没有真实低端机的基线 |
|
||||
| P3 | 流式输出和终端输出已有按帧合并;C01/C02/C06 的长期负载仍需验证是否出现排队增长、长帧或内容丢失。 |
|
||||
| P4 | 存储域已统一到各自 owner,输出 backing 和附件引用已接入;部分 `*Changed` 通知仍会重新读取全量,重复缓存和跨域通知的并发影响待测。 |
|
||||
| P5 | 消息、会话、笔记、文件树、知识库文档表格已有窗口化;C07 仍需记录冷/暖内容可用时刻和切换后的状态保留。 |
|
||||
| P6 | 空配置和真实数据启动有历史测量;当前 C07 尚未完成监督概览、图谱、历史选择和来源正文的完整冷/暖验收。 |
|
||||
| P9 | 已有 lint 和历史回归上限;间接 SQLite 打开、白名单单调收紧、并发指标缺失和 CI/性能环境接入仍待补齐。 |
|
||||
| P10 | 已有局部输入、刷新和项目切换修复;需要在 C01/C02/C06 中继续验证动画、焦点、滚动、草稿和取消语义没有因降载或合并而退化。 |
|
||||
| P11 | 存储请求已移除产品侧 admission 上限并透传给 storage host;运行时仍有若干并发与消息容量数字,需逐项证明属于用户设置、外部协议、数据安全或实测故障约束,否则应删除。C02、C04、C05、C06 尚未证明健康 peer 隔离、故障恢复和队列排空。 |
|
||||
| P12 | 目标边界和 KISS 约束已写入架构文档;S01-S07 已有生产组合,但删除旧直接入口、异常路径和全候选统一验收仍未完成。 |
|
||||
|
||||
2026-10-04 补充:监督回顾的来源初始化和继续前版本校验已移入现有 Worker 入口的独立连接,扫描不持有写锁,清单按 200 条提交,生产失败不回退 Main 同步扫描。最终发布事务仍在 Main;实现与实测边界见[监督者进度](../features/conversation-supervision/progress.md#2026-10-04-回顾初始化阻塞修复)。
|
||||
|
||||
2026-10-05 补充:监督者 UI 的概览、图谱、故事读取也已迁到现有只读 Worker;概览按需加载、列表窗口化及摘要缓存见[KISS 局部修复](../review/kiss-local-fixes-2026-10-05.md)。这与工具侧 Story Graph 读取是不同入口。Main 最终发布及部分大响应仍有超预算样本,不能把局部完成视为全项达标。存储服务目标另见[桌面存储方向](./desktop-storage-direction.md),本轮不实施全面迁移。
|
||||
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)约束当前候选的剩余验证,不能作为已有实现或实测结果;上述第一期历史状态也不替代当前验收。
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
# Runtime Storage And Background Reads
|
||||
|
||||
## Current Implementation
|
||||
|
||||
This file records current runtime timing, reader behavior and temporary-resource
|
||||
ownership. The production runtime receives the storage-owned output backing store and
|
||||
output adoption function during Main startup. Main also computes one per-version,
|
||||
per-run temporary root and passes it to the local runtime cache paths and native
|
||||
client coordinator.
|
||||
|
||||
SQLite owns persisted task, event and conversation evidence. The desktop business
|
||||
connection is long-lived. WAL permits readers alongside a writer; synchronous SQL
|
||||
still blocks the thread that executes it. Memory caches are disposable derived data.
|
||||
Persisted evidence is not necessarily full tool output; the inventory below records
|
||||
the current distinction. The target ownership rules are in the architecture document.
|
||||
|
||||
Startup storage upgrades run in `assistant-storage-worker` before the business
|
||||
connection opens. Runtime readers must not invoke initialization, migrations or
|
||||
@@ -16,6 +26,17 @@ or result cache. Main owns current segments independently of renderer visibility
|
||||
The [duration design](../features/task-and-job/technical-design.md) defines the
|
||||
state boundaries, restart handling and incomplete history.
|
||||
|
||||
## Planned Architecture
|
||||
|
||||
The [complete desktop architecture](./desktop-storage-direction.md) defines the
|
||||
remaining target, including desktop binding databases and native-client model ledgers.
|
||||
Its migration and [concurrent acceptance](../quality/concurrent-desktop-acceptance.md)
|
||||
are one delivery; remote Agent database ownership remains unchanged. Main will still
|
||||
observe runtime transitions while the storage service persists them in order; the
|
||||
indexed timing representation and semantics remain.
|
||||
|
||||
## Runtime Read Lifecycle
|
||||
|
||||
Read transactions end before replies are posted. Long-lived read transactions
|
||||
would prevent WAL checkpoints from reclaiming old pages. Each reader has its own
|
||||
SQLite page cache; adding workers increases memory usage and requires evidence of
|
||||
@@ -34,3 +55,193 @@ reopening. Results remain scoped; superseded responses are discarded.
|
||||
The [task statistics design](../features/task-and-job/technical-design.md) owns the
|
||||
snapshot contract and timing rules. Tests and timings demonstrate only their fixture
|
||||
conditions; they do not establish a latency guarantee for arbitrary retained history.
|
||||
|
||||
## Output And Temporary File Investigation
|
||||
|
||||
Source review on 2026-10-05. The inventory records the current production flow. No
|
||||
user Temp directory was scanned or deleted.
|
||||
|
||||
| Producer | Current content flow | Retention and consequence |
|
||||
| --- | --- | --- |
|
||||
| Local process and ripgrep | [Process capture](../../src/main/agent/direct-model-process-service.ts) writes stdout/stderr through [PagedOutputStore](../../src/main/agent/paged-output-store.ts); model receives a JSON-bounded preview, up to 96 KiB per stream, and a reference when truncated | Every stream opens a file, including empty stderr. Small files are deleted on finish; retained files and their in-memory handles belong to the conversation/service |
|
||||
| Direct-model subagent | [Subagent service](../../src/main/assistant/direct-model-subagent-service.ts) captures child text through the same store, returning up to 192 KiB and an optional reference | [Provider event bridge](../../src/main/agent/model-tool-provider.ts) forwards preview/status but drops the output reference from the subagent event; child text is not an independent full parent-history record |
|
||||
| Tool events and conversation history | [Model runtime](../../src/main/agent/model-runtime.ts) emits at most 16,000 characters of serialized tool result as the event preview; [IPC](../../src/main/ipc.ts) and [database](../../src/main/assistant/assistant-database.ts) persist the supplied event | Tool events still carry bounded previews, while output history adopts finished handles through attachment storage. Even `output_read` responses are previewed in tool events. Persisted event history does not by itself guarantee a complete stdout/stderr copy |
|
||||
| Remote ACP | [Agent transcript](../../src/agent-daemon/semantic-prompt-store.ts) retains received notifications pending ACK; [desktop adapter](../../src/main/agent/acp-remote-runtime.ts) bounds mapped events before persistence | ACK prunes Agent transcript rows. Notifications themselves may already be bounded by the Runtime. Do not treat this journal as a permanent full-output source or remove commit-before-ACK |
|
||||
| MCP calls | [MCP transport](../../src/main/capabilities/mcp-client-transport.ts), provider and [gateway](../../src/main/agent/knowledge-mcp-gateway.ts) pass objects through stdio/HTTP/SSE | No per-call request/response scratch-file creation was found in these paths. Calling a tool over MCP does not itself require a payload file; third-party server internals are outside this inventory |
|
||||
|
||||
`PagedOutputStore` keeps short and empty output in memory and delegates larger
|
||||
or JSON-preview-truncated content to an injected `PagedOutputBackingStore`. Capture
|
||||
has a shared 8 MiB budget, at most 1,024 pending chunks per writer and 4,096 active or
|
||||
retained handles per store; backing writes are at most 64 KiB. Producers must await
|
||||
append, or receive an explicit capacity error. Local process pipes pause until each
|
||||
append settles. The store does not create a temporary file backend or another DB.
|
||||
UTF-8 byte cursors, JSON-escaped preview bounds, owner checks and failed/cancelled
|
||||
output remain covered by focused tests using real async files and local children.
|
||||
|
||||
The production owner exposes output create, append, finish, read, adopt and release
|
||||
through the existing storage transport. Main passes its `backingStore` to the runtime
|
||||
and passes `outputAdopt` to the tool provider. When output history is enabled, finished
|
||||
stdout/stderr references are adopted into the conversation attachment reference for
|
||||
the target message; the bounded preview and output reference are returned together.
|
||||
Missing backing configuration still explicitly fails capture that requires full
|
||||
storage. A model's final answer is not an equivalent copy of its tool output.
|
||||
|
||||
### Planned Architecture And Remaining Acceptance
|
||||
|
||||
The planned architecture still requires full end-to-end history and restart coverage,
|
||||
resource reconciliation and the remaining storage-owner migration. The file-backed
|
||||
test fixture alone does not establish that architecture or its acceptance.
|
||||
|
||||
Tool previews can also appear in both conversation `tools` and `blocks`. This is a
|
||||
separate persisted-preview duplication issue, not evidence that full paged output is
|
||||
already stored. Existing [subagent progress compaction](../../src/main/assistant/subagent-progress-storage.ts)
|
||||
already handles repeated progress snapshots; retain that implementation.
|
||||
|
||||
### Other Owned Files
|
||||
|
||||
| Existing owner | Source finding | Disposition for the upgrade |
|
||||
| --- | --- | --- |
|
||||
| [DocumentResultStorage](../../src/main/document-result-storage.ts) and [attachment storage](../../src/main/conversation-attachment-storage.ts) | `parsed.md` duplicates `manifest.json.content`; normal reads use the manifest and `copyResult` copies the extra file | Remove the redundant new write/copy. Preserve original files, extracted images, manifests and ownership transfer; old directories follow existing reference cleanup |
|
||||
| [OpenCode runtime](../../src/main/agent/opencode-runtime.ts) | Configuration already uses `OPENCODE_CONFIG_CONTENT`; scratch supplies isolated XDG state and skill registration | Keep required isolation, reuse immutable skill caches. Do not invent a config-file optimization for a path already using memory/environment |
|
||||
| [Continue adapter](../../src/main/agent/continue-host-adapter.ts) | CLI uses `--config` plus an isolated global directory; creation can fail before the outer owner receives the path | Retain the external-file interface and clean partial creation at the creating function, including skill-staging failure; preserve pre-existing files on `EEXIST` |
|
||||
| [Continue helper](../../src/agent-daemon/continue-acp-helper.ts) and [model bridge helper](../../src/agent-daemon/model-bridge-helper.ts) | Temporary config/plugin resources use helper finalization; setup before the protected scope and termination need lifecycle coverage | Extend existing ownership and shutdown, verify child exit before deletion. Plugin file URLs and Unix sockets remain necessary interfaces under current contracts |
|
||||
| [Managed Python](../../src/main/local-tool-environment/managed-python-install.ts), [model packages](../../src/main/model-package-utils.ts), [Agent packages](../../src/main/remote-agent/agent-package-manager.ts) | Install staging, partial downloads and backups support validation/publication; existing cleanup has owner-specific patterns | Extend those cleanup functions for demonstrated leftovers; preserve active staging and verified reusable installations. No global installer sweep |
|
||||
| [Magic note storage](../../src/main/magic-notes/magic-note-storage.ts) | Sibling atomic-write `.tmp` files protect replacement; memory-database fixtures have a separate temporary root | Keep atomic replacement and normal failure cleanup. Test scratch follows repository rules; persistent note directories are user data |
|
||||
|
||||
Document results may move from temporary ownership into attachment references, including
|
||||
shared branch references. A directory under `temp/document-parsing` can still be owned
|
||||
by persisted data. Its location alone does not authorize deletion. Native client history,
|
||||
model caches, database WAL/SHM and exported user files are likewise not disposable scratch.
|
||||
|
||||
Some tests/build helpers share production-looking prefixes, including `goodbuddy-process-*`
|
||||
and `goodbuddy-opencode-*`. Prefix and age alone cannot establish who owns a legacy directory.
|
||||
The [architecture rules](./desktop-storage-direction.md#output-and-temporary-resource-ownership)
|
||||
define the replacement and cleanup scope; the [acceptance matrix](../quality/concurrent-desktop-acceptance.md#输出与临时资源验收)
|
||||
defines evidence required before claiming improvement.
|
||||
|
||||
### Creator Cleanup Validation (2026-10-05)
|
||||
|
||||
Continue now removes its partially written config and failed skill-staging directory
|
||||
at the creator, before returning ownership to the run. Exclusive creation failures
|
||||
leave existing files/directories intact. Run finalization preserves prepared caches
|
||||
and external configuration. Both Agent helpers protect setup with finalization and
|
||||
handle SIGTERM/SIGINT; Continue stops prompt admission, aborts and drains active
|
||||
runs, while the OpenCode helper waits for child exit before deleting its plugin.
|
||||
Managed Python's existing pre-install cleanup also removes regular download files
|
||||
with the exact UUID layout, preserving directories, unknown names, caches and history.
|
||||
|
||||
Validation in the concurrent working tree:
|
||||
|
||||
- Five focused suites (`continue-host-adapter`, `continue-acp-helper`, `model-bridge`,
|
||||
`managed-python-install`, `local-tool-environment-service`): 81 passed, 10 Unix-only
|
||||
cases skipped on Windows. Failure injection covers partial creation, EEXIST,
|
||||
failed skill staging, setup-time signals and shutdown cleanup.
|
||||
- Final `npm run typecheck` passed; ESLint passed for all eight owned source/test files.
|
||||
Repository lint passed with `npm run lint -- --ignore-pattern temp`; unfiltered
|
||||
lint encountered generated validation bundles in ignored task/release directories.
|
||||
Task-owned `temp/goodbuddy-scratch-upgrade` was subsequently removed and its absence
|
||||
verified; unrelated release scratch was preserved.
|
||||
- Full Vitest run: 517 suites passed, 7 failed, 15 skipped; 6,244 tests passed,
|
||||
6 failed, 94 skipped. Failures outside this cleanup scope included OpenCode config
|
||||
preparation (`spawnSync npm.cmd EINVAL`), the MCPVault Electron fixture, and
|
||||
message-expansion and workspace-import Electron assertions. These results do not
|
||||
establish full architecture acceptance.
|
||||
- Reused the existing `a01-runtime-host-build.cjs`/`a01-runtime-host-run.cjs` harness
|
||||
logic, rebuilding Agent, daemon and Desktop driver from current source under
|
||||
`temp/goodbuddy-scratch-upgrade`. Connected to Linux x64 over the LAN using
|
||||
GoodBuddy encrypted credentials and the pinned Host identity. Both routes were
|
||||
reachable; only the LAN route was used for execution.
|
||||
- Both OpenCode and Continue passed real tool execution, startup cancellation,
|
||||
disconnect/reconnect during execution, checkpoint persistence/replay deduplication,
|
||||
and retirement. Final-source cancellation took 217 ms and 233 ms respectively;
|
||||
each run reported zero owned processes after retirement and shutdown. There were
|
||||
eight real model requests across the initial and final runs, four per runtime.
|
||||
Remote artifacts stayed in dedicated `/root/tmp/gb-lifecycle-*` directories and
|
||||
followed the harness cleanup. These timings are scenario observations, not a
|
||||
comparative performance claim.
|
||||
|
||||
### Version And Run Scratch Follow-up (2026-10-05)
|
||||
|
||||
`runtimeTemporaryRoot(userDataPath, appVersion)` computes the lazy
|
||||
`temp/goodbuddy-runtime/<version>/<runId>/goodbuddy-runtime-launch` path.
|
||||
Main passes that same value through existing `continueHostCacheRoot` and
|
||||
`opencodeSharedCacheRoot` options. The native client coordinator receives it as
|
||||
`temporaryRoot` and passes it to both native clients. The existing constructors need
|
||||
no new parameter.
|
||||
OpenCode creates its isolated launch directory beneath the supplied shared root;
|
||||
Continue puts generated configuration and isolated globals in one per-call directory.
|
||||
Prepared bundles and immutable Skill/config caches remain reusable within the run.
|
||||
The helpers on a remote Host use `goodbuddy-runtime-launch` beside their existing
|
||||
model-bridge socket, under that process owner's scratch directory. Global `TEMP`
|
||||
is unchanged.
|
||||
|
||||
Each local launch has a sibling owner file containing creator and child PIDs. Keeping
|
||||
metadata outside the deleted tree preserves retry evidence after partial deletion.
|
||||
The launch root also records its creator for cache-only reclamation. After the
|
||||
single-instance check and first screen, call `cleanupRuntimeTemporaryDirectories`
|
||||
in the existing delayed background startup work. It examines at most 256 directory
|
||||
entries per invocation, sequentially, and accepts the startup shutdown signal.
|
||||
Service disposal still stops children before deleting its own launch material;
|
||||
an unconfirmed child exit retains the material. Startup preserves live/reused PIDs,
|
||||
permission-denied process checks, malformed ownership and the ambiguous spawn-to-PID
|
||||
recording window. Empty version/run parents are removed after owned material.
|
||||
This is not a new process owner or an installer collector.
|
||||
|
||||
Legacy candidates are limited to the known OS-Temp layouts `goodbuddy-opencode-*`
|
||||
(desktop isolated state), `goodbuddy-continue-*` (Continue helper), and
|
||||
`goodbuddy-opencode-plugin-*` (model-bridge plugin). Deletion requires valid owner
|
||||
metadata and confirmed inactive creator/child PIDs. Releases that wrote no such
|
||||
metadata cannot be identified as obsolete from their names, contents or age alone;
|
||||
those directories are retained and reported. A bounded pass can leave candidates
|
||||
unvisited. Native-client histories, existing version-only caches and unrelated
|
||||
OS-Temp files are outside this reclamation contract. The old version/layout-only
|
||||
cache sweep is not evidence that a surviving child has exited.
|
||||
|
||||
Remote environment preparation now retains its existing pending-operation record
|
||||
when connection acquisition or staging cleanup fails. Because that store has one
|
||||
record per Host, a replacement prepare retries cleanup before overwriting it;
|
||||
confirmed cleanup or `operation-unavailable` clears it. Already adopted environments
|
||||
remain usable. Agent package delayed removals retain their pending entry until
|
||||
deletion succeeds, retry on inventory access, and rediscover verified obsolete
|
||||
versions through the existing package scan after restart. Active archive leases
|
||||
still prevent deletion.
|
||||
|
||||
Continue bundle staging now waits for all parallel writes to settle before failure
|
||||
cleanup. Existing settings atomic replacement and selected-runtime diagnostic
|
||||
teardown already use `finally`; no global `.tmp` sweep or new diagnostic owner was
|
||||
added. Native clients now accept an optional `temporaryRoot` for the same application
|
||||
version/run launch root. `NativeTerminalClient` keeps histories and reusable caches
|
||||
under its existing `rootDirectory`; only per-launch material moves to scratch.
|
||||
DS Web keeps `DSH_HOME`, sessions and user preferences in place and supplies its
|
||||
generated configuration through `web --patch <scratch-file>`. Its ledger also lives
|
||||
in scratch. Both reuse the existing sibling PID metadata and await ledger close
|
||||
before releasing material; unconfirmed child exit retains it. ConPTY registration
|
||||
waits for the actual child PID, which can be unavailable immediately after spawn.
|
||||
The collector recognizes `goodbuddy-native-dsh-*` alongside the existing prefixes.
|
||||
The production wiring propagates `temporaryRoot` through the native coordinator to
|
||||
both constructors without relocating the retained-history root.
|
||||
Focused verification passed 60 tests, including a real Windows PTY, official DS Web
|
||||
session persistence across restart, and delayed ledger-close cleanup. Typechecking
|
||||
and scoped lint passed; this native follow-up made zero real model calls.
|
||||
|
||||
Final-source verification for this follow-up is separate from the earlier eight
|
||||
real requests above: two real provider requests total, one per runtime. Current-source
|
||||
Continue/OpenCode helpers ran on Linux x64 over pinned SSH, each executed its native
|
||||
command tool, returned the real model answer, and removed launch files after exit.
|
||||
A separate active-request shutdown probe passed for both helpers with zero real
|
||||
provider requests. Four successful scenarios used deterministic command responses;
|
||||
probe setup retries before these results did not reach a provider. A previous-process
|
||||
version/run directory was reclaimed using actual dead PIDs. After the final
|
||||
enumeration-error guard, a separate Host probe confirmed that the current collector
|
||||
retains a real live child and reclaims that same run after its exit, with zero model
|
||||
requests. The timed-out probe's ledger was audited and contained zero calls; all
|
||||
task probe artifacts were removed locally and remotely. These probes cover the
|
||||
changed helper and scratch lifecycle, not full Desktop UI or installation acceptance.
|
||||
|
||||
The seven primary suites passed 267 tests with 10 platform skips; related runtime,
|
||||
native-terminal and diagnostic-owner suites also passed. Agent and web typechecks
|
||||
passed. Full desktop typecheck and repository lint remain blocked by concurrent
|
||||
`portable-storage-upgrade-fixture.ts` typing and `portable-storage-upgrade.mjs` lint
|
||||
errors; unfiltered lint also visits generated files under `temp`. Focused lint for
|
||||
the edited source/tests passed. The C01-C07 concurrent acceptance matrix remains
|
||||
incomplete; these results do not establish full storage-architecture acceptance.
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# Agent 功能维护约束
|
||||
|
||||
本文面向在 GoodBuddy 中新增功能的 Agent。它只记录做决定时必须遵守的维护规则,
|
||||
不替代架构设计、性能规范或功能文档。若本文与代码或权威文档不一致,以已验证的生产
|
||||
代码和下列权威文档为准,并修正文档链接或内容:
|
||||
|
||||
- [桌面存储与并发执行](../architecture/desktop-storage-direction.md):职责边界、存储所有权和操作边界。
|
||||
- [桌面存储基础集成](../architecture/desktop-storage-foundation.md):`DesktopStorageClient`、domain operation 和生命周期。
|
||||
- [桌面 Runtime 存储集成](../architecture/desktop-storage-runtime-integration.md):binding、model-call ledger 和关闭顺序。
|
||||
- [性能原则](../architecture/performance-principles.md):唯一的性能数值、并发规则和评审清单。
|
||||
- [桌面并发架构验收](../quality/concurrent-desktop-acceptance.md):真实生产路径、负载和证据要求。
|
||||
- [运行时存储与后台读取](../architecture/runtime-storage.md):输出正文、预览、临时资源和当前迁移状态。
|
||||
- [数据库迁移与大数据升级指南](./database-migrations.md):已发布用户数据的迁移边界。
|
||||
- [AGENTS.md](../../AGENTS.md):临时文件、真实 Host 验证、测试和文档维护规则。
|
||||
|
||||
## 决策检查
|
||||
|
||||
开始编码前,逐项回答下面的问题。任一项没有答案时,先补充设计或查找现有机制,
|
||||
不要直接增加新的 owner、队列、缓存或恢复记录。
|
||||
|
||||
- 数据的唯一权威来源和写入 owner 是什么?它是否已经存在?
|
||||
- 该功能是否需要数据库事务、跨表快照、文件引用或运行时 ledger?若需要,操作是否落在对应 owner 内?
|
||||
- Main 是否只负责 IPC 校验、凭据、生命周期和调度?是否把 SQL、大文件读写、哈希或模型工作留在了 Main?
|
||||
- 新操作能否使用现有 domain port 和显式 allowlist?是否误把 SQL、回调、实例、函数或 handle 放进跨进程 DTO?
|
||||
- 依赖链是否按“先提交,再通知、ACK、读取或返回成功”的顺序实现?提交未确认时是否禁止盲目重放?
|
||||
- 排队取消、在途取消、执行结算和资源释放分别是什么时刻?容量是否一直占用到实际结算或确认的 Host 退出?
|
||||
- 输出是预览还是完整正文?完整正文是否只有一个所属存储,引用是否随历史、分支和重启继续有效?
|
||||
- 是否创建了临时文件?创建者、使用者、删除条件、子进程退出确认和删除失败后的重试位置是否明确?
|
||||
- 新的操作数、排队数、字节数、句柄数和缓存是否有界?是否复用了已有预算并避免无限缓冲?
|
||||
- 测试是否覆盖真实 UI 到生产实现的路径、失败、取消、重试、关闭和迟到响应?是否需要真实数据、模型或 Host?
|
||||
- 受影响的架构、功能、质量、构建或运维文档由谁维护?是否已有同一事实的权威定义,新增内容能否只链接它?
|
||||
|
||||
## 存储所有权
|
||||
|
||||
桌面业务 SQLite 由一个 storage utility process 持有。Main 通过
|
||||
`DesktopStorageClient.call(domain, method, args, { signal })` 使用显式操作;
|
||||
`src/main/desktop-storage-contracts.ts` 的 allowlist 是可调用面的边界。Main 不得再
|
||||
打开同一业务数据库,也不得通过 helper、同步回退或隐藏的 repository 构造绕过 owner。
|
||||
远程 Agent 的数据库仍由远程 Agent 持有,不能因桌面存储改造而迁移。
|
||||
|
||||
新增持久化时,优先扩展已有 domain repository 和 owner operation。完整事务、跨表快照、
|
||||
去重、checkpoint 与发布规则留在 owner 内;模型、网络、解析和文件准备在事务外完成,
|
||||
进入事务后只校验当前前提并提交完整结果。需要多个语句的读快照,应增加一个有名字的
|
||||
owner/reader operation,不要在 Main 中拆成多次 `await`。
|
||||
|
||||
Main 侧服务只持有异步 port 和普通 DTO。live repository、数据库连接、事务 callback、
|
||||
订阅、函数、AbortSignal 和运行时 handle 不跨 utility transport。`StorageData` 允许的
|
||||
值以当前契约为准;新增类型先确认序列化、大小计算和错误传播,再加入 allowlist。
|
||||
|
||||
文件和输出也有 owner。附件引用、document result、paged output backing、Runtime
|
||||
binding 和 native model-call ledger 应通过既有 storage facade 访问。文件所在目录不等于
|
||||
可删除的临时目录;持久化引用、WAL/SHM、native history、用户导出文件和仍被分支引用的
|
||||
正文都必须按 owner 释放。
|
||||
|
||||
## 增加领域操作
|
||||
|
||||
按以下顺序定位扩展点:
|
||||
|
||||
1. 找到已有 repository、port 或 facade,以及它实际打开的数据库和文件。
|
||||
2. 在 owner 内保留原有事务和 ID、顺序、去重、耐久性语义,必要时抽出可复用的内部函数。
|
||||
3. 为外部调用增加一个有明确名称的 domain operation,更新类型 allowlist 和 transport handler。
|
||||
4. 在 Main 组合处注入已有 typed port;调用者等待 Promise,不保存第二份可写事实。
|
||||
5. 为提交前、提交后响应前、取消、失败、重试、关闭和重启增加针对性断言。
|
||||
|
||||
操作应表达业务动作,例如 `appendRemoteTaskEventsBatch`、`publishDocument` 或
|
||||
`transition`。不要暴露 `executeSql`、任意方法代理、通用对象代理或把 repository
|
||||
整个返回给调用者。一个已有操作能表达需求时,扩展其参数或返回值并更新权威文档,
|
||||
不要平行创建同义 operation。
|
||||
|
||||
通知不是提交确认。持久化成功后才能发送依赖该事实的 change notification、远程 ACK、
|
||||
模型下一轮输入或成功响应;通知失败要按现有语义处理,不能把已提交事实伪装成未提交。
|
||||
|
||||
## Main 与 utility 的边界
|
||||
|
||||
Main 保留窗口、可信 sender 和 schema 校验、凭据解密、网络与 Runtime 编排、OS API、
|
||||
请求归属和进程生命周期。utility storage 保留业务 SQLite、owner-local 事务、升级后
|
||||
的读 worker 生命周期和必要的同步 SQL。Parser、OCR、embedding、模型请求和远程 Agent
|
||||
继续使用各自宿主,不因“改成异步”而塞入 storage 事务。
|
||||
|
||||
Main 里的同步数据库调用、同步文件 IO 和可能超过 16 ms 的计算都要按性能原则处理。
|
||||
Promise 返回本身不能证明工作已离开 Main。启动顺序是先完成 storage readiness,再发布
|
||||
依赖它的服务;关闭顺序是先停止生产者并等待必要的持久化,再关闭 ledgers、bindings、
|
||||
读 worker、数据库 owner 和 utility process。失败或未确认退出时保留可能仍被 child
|
||||
持有的文件,并将错误传播给调用方。
|
||||
|
||||
## 异步顺序与取消
|
||||
|
||||
为每个操作写清四个时点:入队、开始执行、提交或执行结算、资源释放。最小必要范围内
|
||||
保持顺序:同一会话事件按序提交,同一数据库事务按序落盘;不同项目、会话和子任务
|
||||
不得共用长时间的全局等待。
|
||||
|
||||
取消规则如下:
|
||||
|
||||
- 排队中的请求在 dispatch 前检查 abort,取消后不得执行。
|
||||
- 在途请求先向实际 owner 或 Host 发送取消,调用方可以先得到取消结果,但容量只能在执行结算或确认 Host 退出后释放。
|
||||
- 同步 SQL 无法被 IPC 消息打断;安全批次之间检查取消,已经提交的事务必须报告为已提交。
|
||||
- 取消一个请求不得杀掉仍被其他请求使用的共享 Runtime、utility process 或 Worker。
|
||||
- 迟到结果必须携带已有项目、会话、任务或 operation 归属,先通过归属检查,再改变代次、缓存或队列。
|
||||
- 存储进程替换前等待旧进程退出并结算旧请求;未确认写入按 domain identity reconciliation 处理,不能自动重放写入。
|
||||
|
||||
`AbortSignal` 是 transport option,不是业务 DTO 字段。不要用 UI 隐藏、组件卸载或清空
|
||||
按钮代替服务层取消。控制和取消不得等待新的执行槽位。
|
||||
|
||||
## 输出引用与临时文件
|
||||
|
||||
短输出可以保留有界预览;超过预览上限的输出必须由所属 backing store 保存完整正文,
|
||||
并在后续模型 `output_read`、历史和分支路径中复用同一 handle。分别核对模型收到的内容、
|
||||
工具事件预览和历史正文,不能用最终回答代替工具全文,也不能把正文再次复制成临时全文。
|
||||
输出 writer 的 append 必须被生产者等待;容量错误要显式传播,不能丢块或无限排队。
|
||||
|
||||
创建临时配置、插件、socket 或 launch 目录的函数承担创建失败清理。删除前关闭拥有
|
||||
文件或目录的 child、ledger 和句柄,并确认 child 已退出;无法确认时保留材料和 owner
|
||||
metadata,等待后续有界回收。Agent 和测试产生的验证文件使用仓库根目录 `temp/<task>/`;
|
||||
产品运行时临时资源继续使用各自 owner 管理的运行时路径,不能因为位于仓库 `temp/` 就获得删除权。
|
||||
远程 Host 使用专用 `/root/tmp` 子目录。不要扫描或删除用户 Temp、未知归属目录或仅凭名称和年龄
|
||||
判断为过期的资源。
|
||||
|
||||
持久化目录可以位于 `temp/` 下,位置本身不能授权删除。删除前先检查 attachment、
|
||||
document result、history、branch 或其他 durable reference;释放一个调用引用不能删除
|
||||
仍被历史引用的正文。旧数据清理沿用所属 owner,不增加全局 Temp sweep。
|
||||
|
||||
## 并发预算
|
||||
|
||||
数值只引用[性能原则](../architecture/performance-principles.md),负载和证据只引用
|
||||
[并发验收](../quality/concurrent-desktop-acceptance.md)。当前 storage client 的请求、
|
||||
读写 pending 不设置产品侧 admission 上限,负载透传给 storage host;paged output 的 8 MiB capture、
|
||||
每 writer 1,024 个 pending chunks、4,096 个 active/retained handles 和 64 KiB backing
|
||||
write 也必须服从当前实现与测试。修改前先确认代码中的常量和适用范围,不在本文件复制
|
||||
一套新数字。
|
||||
|
||||
只有用户明确配置、外部协议要求、数据结构安全,或已有实测证明不限制会导致崩溃、
|
||||
数据损坏或不可恢复失败时,才设置上限。读写请求默认透传给所属 Host;不能用内部
|
||||
并发数、队列长度或容量数字替用户决定吞吐。仍禁止静默丢事件、无限重启、按项目预建
|
||||
服务、为每个调用新增 scheduler,或用串行夹具掩盖真实负载。
|
||||
|
||||
Worker 发生故障时可以进入已有退避和恢复流程;这不应被伪装成负载上限。正常排队的
|
||||
解析、存储和 Host 工作不能因为一个未经测量的队列数字直接返回 busy。
|
||||
|
||||
`ReadonlyQueryReader` 同时服务只读 Worker 和桌面 storage client。只有调用方显式传入
|
||||
`maxPending` 时,`waiting` 队列才承担有界等待语义;生产 storage client 不传该选项,
|
||||
请求直接交给 storage host。这个共享类不是第二个 storage owner,不能因为看到等待队列
|
||||
就给生产路径恢复默认 admission 上限。若未来实测证明某个边界需要容量策略,应先说明
|
||||
资源、故障和测量依据,再拆出最小的显式 wrapper 或增加调用方配置。
|
||||
|
||||
## 测试与真实验证
|
||||
|
||||
单元测试用于确定字节边界、事务回滚、顺序、归属、取消竞态、失败清理和重试。行为改动
|
||||
还必须覆盖生产组合:正常 UI action 经过 Preload、Main、storage/Runtime 宿主并产生
|
||||
实际结果。mock bridge、独立 SQL、只测试握手或只测试 schema 都不能替代这条路径。
|
||||
|
||||
需要持久化时使用真实 SQLite owner 和隔离数据库;需要文件时使用真实异步文件;需要
|
||||
Runtime 时使用本地确定性模型验证工具、事件、取消和关闭。不要把真实提供商网络速度
|
||||
混入性能基准。需要模型能力证明时,配置检查不够,必须发起实际生成或工具请求,并
|
||||
记录实际请求数和范围。
|
||||
|
||||
涉及桌面到 Agent 的 Runtime、安装、workspace、模型 bridge 或生命周期时,按
|
||||
`AGENTS.md` 在共享 Linux x64 Host 使用当前源码验证。覆盖面应对应改动,包括断开、重连、
|
||||
取消或重启;远程临时目录必须专用,验证后清理。未能连接 Host 时记录为验证阻塞,不能
|
||||
用单测或旧发布包替代当前源码证据。
|
||||
|
||||
性能改动先取得基线,至少按权威验收矩阵记录 offered load、排队、吞吐、排空、资源高水位、
|
||||
Main/Renderer 指标和样本条件。功能测试通过不等于并发预算通过;未测平台和缺失指标保留
|
||||
为缺口。
|
||||
|
||||
## 文档与重复设计
|
||||
|
||||
代码行为改变后,检查受影响的架构、功能、质量、构建和运维文档,更新真正的 owner、
|
||||
顺序、限制和验证证据。每项事实只保留一个权威定义:本文件写维护决策,架构文档写
|
||||
职责与边界,性能原则写数值,验收文档写场景和证据,功能 README 或 progress 写功能
|
||||
行为和状态。其他地方使用链接,不复制完整表格、状态机、预算或验收结果。
|
||||
|
||||
新增机制前先搜索同类名称、队列、owner、transport、shutdown 和测试。若现有机制只缺
|
||||
一个操作、一个容量检查或一个清理分支,扩展它并补测试;只有现有 owner 无法表达真实
|
||||
边界时才提出新设计,并在对应架构文档中记录问题、取舍和验证条件。未提交的实验代码、
|
||||
测试夹具或接口存在,不等于生产路径已经接入。
|
||||
|
||||
## 允许与禁止的模式
|
||||
|
||||
| 允许 | 禁止 |
|
||||
| --- | --- |
|
||||
| 在已有 storage domain 增加命名 operation,并由 owner 完成事务 | 在 Main 新建同一 SQLite 的第二个连接、同步回退或隐藏 writer |
|
||||
| 先在 Main 完成模型/解析,再把不可变 DTO 交给 owner 原子发布 | 在 SQLite 事务内 `await` 模型、网络、IPC 或文件回调 |
|
||||
| `storage.call('assistant', 'appendRemoteTaskEventsBatch', [batch], { signal })`,提交后再 ACK | 暴露 `executeSql`、任意 repository proxy,或把 live repository/callback 放进 DTO |
|
||||
| 使用同一 output handle 进行预览、分页、历史引用和 release | 每个工具结果写一个临时全文,再复制到 history 或用最终回答代替全文 |
|
||||
| child 退出确认后删除创建者拥有的 launch material | 按目录前缀、年龄或路径位置删除未知归属的用户文件 |
|
||||
| 取消在途请求并等待 settlement,再归还容量 | 调用方停止等待后立即释放槽位,或取消一个请求时杀共享宿主 |
|
||||
| 用真实 owner、隔离数据、确定性模型和生产 UI 路径验证 | 只验证 schema、mock bridge、旧包或 utility ready 就宣称功能完成 |
|
||||
| 更新一个权威文档,其他文档链接它 | 在多个文档复制同一预算、状态机、所有权表并分别维护 |
|
||||
@@ -0,0 +1,51 @@
|
||||
# Desktop 0.15.13 / Agent 0.15.6 发布准备
|
||||
|
||||
日期:2026-10-07。基线 `v0.15.12`,冻结源码
|
||||
`c1e4bbf66a418dbd4e0f99a36ff0522dbf610553`。只纳入已提交实现和发布准备,
|
||||
保留开始准备后出现的并行工作。标准六平台 Desktop、三平台 Agent,不含 LoongArch。
|
||||
|
||||
## 范围与兼容性
|
||||
|
||||
- 一个 Desktop storage utility process 统一拥有 Assistant、Knowledge、附件、
|
||||
Runtime binding/ledger 等业务 SQLite;Main 通过类型化异步操作访问。
|
||||
保留数据库路径、表、ID、事务、提交顺序和远程 Agent 数据库。
|
||||
- storage utility 使用现有读取 Worker 和受界定 IPC,覆盖 readiness、取消、
|
||||
容量、进程退出、关闭排空和错误传播;移除生产 Main 业务 SQLite owner。
|
||||
- schema 仍为 62,无数据迁移;升级前仍备份完整数据目录、notes 和设置。
|
||||
- 直连模型工具失败后可继续下一轮,分页输出保持有界;Magic Notes 标签、
|
||||
监督来源别名、设置直达预加载、文件元数据和 Composer 过渡同步修复。
|
||||
- Agent 生产 Runtime/模型桥有变化,需 0.15.6;runtime/acp 6、wire 2.0、
|
||||
Node/Electron/OpenCode/Continue 锁不变。
|
||||
|
||||
## 验证边界
|
||||
|
||||
storage foundation focused 结果:utility process 真实 Electron 编排、监督
|
||||
heartbeat/回顾叶子/导航、异步 Worker、存储帧高水位约 248,332 bytes Main、
|
||||
252,578 bytes storage;focused 70 项跨 11 suites 通过,Node/Agent/Renderer
|
||||
typecheck 和 scoped lint 通过。工具失败继续、分页输出、Magic Notes 和监督
|
||||
别名另有定向回归。
|
||||
|
||||
并发验收文档明确的 C01–C07 和 A01–A10 完整矩阵、跨平台 storage process
|
||||
安装、低端机对照及完整 UI→Preload→Main→storage→SQLite 验收仍不是这份文档
|
||||
中的已完成事实。不得把 focused 结果或架构文档目标写成全部 PERF-15/PERF-16
|
||||
已验收。Agent 远程路径需在当前源码 CI/Host 规则下重新验证。
|
||||
|
||||
## 发布门槛
|
||||
|
||||
按用户约定不运行本地测试套件、typecheck、lint、生产构建或安装包探针;
|
||||
先校验发布元数据,推送双远端,要求精确候选 main CI 和生产构建通过后再创建
|
||||
Desktop/Agent 不可变标签。通过后核验六平台 20 项 Desktop 资产、macOS 签名公证、
|
||||
Agent 三平台目录、OSS/GitHub 元数据和网站 12 种标准下载选择。
|
||||
|
||||
## 精确候选首轮 CI
|
||||
|
||||
`fbfafa15` 的 Agent CI 已通过。Desktop CI `37636800633` 为 6430 通过、
|
||||
58 跳过、3 失败,生产构建未执行:
|
||||
|
||||
- C05 storage 进程故障 fixture 没有稳定观察到拒绝;
|
||||
- utility-process fixture 的临时数据库父目录权限在 Linux runner 上未满足;
|
||||
- readonly reader 测试把快速完成的 worker 请求固定断言为拒绝。
|
||||
|
||||
修正只涉及测试稳定性:显式把隔离目录设为 `0700`,允许快速 worker 在终止
|
||||
竞争中完成(专用 crash 测试仍覆盖拒绝),并保留 storage 故障恢复和已提交
|
||||
数据断言。下一候选 CI 仍必须重新验证这些场景;不跳过 storage owner 测试。
|
||||
@@ -0,0 +1,25 @@
|
||||
# Desktop 0.15.14 发布准备
|
||||
|
||||
日期:2026-10-09。基线 `v0.15.13`,候选源码 `8f1b6007986f31fb5e2a80e80021602a13fa20a2`。
|
||||
本候选只更新 Desktop;Agent 保持 `0.15.6`,不创建新的 Agent 标签。标准六平台 Desktop,
|
||||
不含 LoongArch。
|
||||
|
||||
## 范围与兼容性
|
||||
|
||||
- 恢复旧版因监督来源变化或移除而失败的历史回顾的继续操作。
|
||||
- 来源在回顾期间变化时,在有限次数内重新冻结处理;持续变化的来源之后省略,
|
||||
已删除、归档或不可用来源沿用既有省略规则,其他来源继续保留进度。
|
||||
- 保留暂停、失败、取消和已保存批次的监督活动状态及项目范围、来源处理规则。
|
||||
- 设置分类显示对应图标,帮助用户更快找到各类设置。
|
||||
- 不新增数据库 schema,继续使用 schema 62;Agent、runtime/acp、wire 和运行时锁版本不变。
|
||||
|
||||
## 验证边界
|
||||
|
||||
本候选包含监督存储、Worker、服务和 Renderer 回归测试,以及历史失败回顾继续操作的
|
||||
界面覆盖。完整并发验收矩阵、跨平台存储进程安装和其他未完成验收不在本候选中宣称通过。
|
||||
|
||||
## 发布门槛
|
||||
|
||||
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
|
||||
要求精确候选的主分支 CI 和生产构建通过,再由用户确认后创建不可变的 `v0.15.14` 标签。
|
||||
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
|
||||
@@ -0,0 +1,23 @@
|
||||
# Desktop 0.16.0 发布准备
|
||||
|
||||
日期:2026-10-09。基线 `v0.15.14`,候选源码为本次发布准备提交。
|
||||
本候选只更新 Desktop;Agent 保持 `0.15.6`,不创建新的 Agent 标签。标准六平台 Desktop,
|
||||
不含 LoongArch。
|
||||
|
||||
## 范围与兼容性
|
||||
|
||||
- 专家和团队角色选择移动到 Composer 输入框顶部,发送前更容易发现。
|
||||
- 角色选择按会话保存,切换会话不会带入其他会话的选择。
|
||||
- 历史消息保留请求发送时使用的角色,即使角色后来重命名或删除,历史上下文仍可识别。
|
||||
- 不新增数据库 schema,Agent、runtime/acp、wire 和运行时锁版本不变。
|
||||
|
||||
## 验证边界
|
||||
|
||||
本候选包含 Composer、会话选择状态、历史消息元数据和角色选择的回归测试。
|
||||
完整跨平台安装、远程 Host 和未覆盖的整包验收仍以 CI 与发布工作流结果为准。
|
||||
|
||||
## 发布门槛
|
||||
|
||||
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
|
||||
要求精确候选的主分支 CI 和生产构建通过,再由用户确认后创建不可变的 `v0.16.0` 标签。
|
||||
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
|
||||
@@ -0,0 +1,18 @@
|
||||
# Desktop 0.16.1 发布准备
|
||||
|
||||
日期:2026-10-10。基线 `v0.16.0`,候选源码包含 `d9dcb65d` 和 `c7f09e1e`
|
||||
两个 Composer 视觉修复提交。本候选只更新 Desktop;Agent 保持 `0.15.6`,
|
||||
不创建新的 Agent 标签。标准六平台 Desktop,不含 LoongArch。
|
||||
|
||||
## 范围与兼容性
|
||||
|
||||
- 消息内容延伸到 Composer 输入框轮廓前再淡出,不再被输入区上方的实色区域提前截断。
|
||||
- 浅色和深色主题使用匹配的消息到输入框渐变,输入框焦点边界不再形成粗边。
|
||||
- 滚动到底部按钮保持与新的输入区过渡相配的可见位置。
|
||||
- 不新增数据库 schema,Agent、runtime/acp、wire 和运行时锁版本不变。
|
||||
|
||||
## 发布门槛
|
||||
|
||||
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
|
||||
要求精确候选的主分支 CI 和生产构建通过,再创建不可变的 `v0.16.1` 标签。
|
||||
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
|
||||
@@ -0,0 +1,24 @@
|
||||
# Desktop 0.16.2 发布准备
|
||||
|
||||
日期:2026-10-10。基线 `v0.16.1`,候选源码包含 `12a02912`、`ba308520`
|
||||
和 `f2f4c127` 三个提交。本候选只更新 Desktop;Agent 保持 `0.15.6`,
|
||||
不创建新的 Agent 标签。标准六平台 Desktop,不含 LoongArch。
|
||||
|
||||
## 范围与兼容性
|
||||
|
||||
- 监督回顾增加从最早记录到当前时间的全部时间范围。
|
||||
- 监督回顾只读取已完成消息和任务,实时生成中的内容等待完成后再进入回顾。
|
||||
- Magic Notes 操作入口增加文字标签,次要更多操作按需显示。
|
||||
- 终端输入法组合区域保持在终端边界内。
|
||||
- 数据库 schema 从 62 升到 63;Agent、runtime/acp、wire 和运行时锁版本不变。
|
||||
|
||||
## 升级须知
|
||||
|
||||
首次启动会执行 schema 63 数据库升级。升级前关闭应用并备份完整数据目录、
|
||||
相邻 `notes/` 和设置文件。旧客户端回退需要恢复升级前备份。
|
||||
|
||||
## 发布门槛
|
||||
|
||||
完成发布说明校验、测试、三套 typecheck 和 lint 后,推送候选到 `origin` 与 `github`,
|
||||
要求精确候选的主分支 CI 和生产构建通过,再创建不可变的 `v0.16.2` 标签。
|
||||
本轮不构建或发布 LoongArch,也不创建新的 Agent 标签。
|
||||
@@ -1,5 +1,14 @@
|
||||
# 工作栏实现与验证进度
|
||||
|
||||
## 2026-10-05:文件列表只保留修改时间
|
||||
|
||||
按 [FR-WF3、US-WF2](./workspace-files.md) 移除创建时间列及排序入口,修改时间采用紧凑格式,
|
||||
完整日期保留在悬停提示中。更多操作与时间共用行尾空间,悬停、聚焦及菜单展开时文件名宽度不变。
|
||||
本机与远程共用 Renderer,无文件访问或 Agent 协议变化。
|
||||
|
||||
验证:`WorkspaceFilesPanel.test.tsx` 41 项通过,Renderer TypeScript 检查、相关文件 ESLint
|
||||
及 `git diff --check` 通过。尚未进行真实 Electron 窗口视觉验收。
|
||||
|
||||
## 2026-10-05:Runtime 提交失败与启动取消
|
||||
|
||||
本机 OpenCode 提交失败后会结束所属 SSE 等待;远程原生 Session 启动等待期间,
|
||||
@@ -12,7 +21,7 @@ Windows/Linux x64 实测、真实数据检查及本轮 5 次文本模型调用
|
||||
## 2026-10-04:移除成果页,工作区导入与文件时间
|
||||
|
||||
对应 [FR-WF1~FR-WF3、US-WF1~US-WF3](./workspace-files.md)。成果 Tab、目录注册及独立导入
|
||||
界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间与可用创建时间,支持同级排序,
|
||||
界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间(2026-10-05 收敛为唯一时间列),支持同级排序,
|
||||
向所选目录导入原件。本机及远程共用分块管理动作,同名文件不覆盖,失败项独立反馈并清理部分文件。
|
||||
|
||||
来源检查确认:`AssistantDatabase.createImageArtifact` 把图片保存为 `artifacts.inline_content`
|
||||
|
||||
@@ -15,16 +15,17 @@ Host 的项目目录。支持二进制和空文件,不调用文档解析器,
|
||||
显示。写入失败时尝试移除本次新建的部分文件,清理失败则报告残留路径。取消文件选择不写入,
|
||||
导入期间禁用重复提交。完成后刷新目录。当前不提供传输中取消或进度百分比。
|
||||
|
||||
FR-WF3:名称默认升序。名称、修改时间和有可用数据时的创建时间列均可点击切换排序;首次
|
||||
FR-WF3:文件列表仅显示名称和修改时间,不显示创建时间列。名称默认升序,两列均可点击切换排序;首次
|
||||
选择时间列按最新优先。每一级目录独立排序且目录优先,缺失时间放在同类条目末尾,显示 `-`。
|
||||
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。窄栏横向滚动仅发生在
|
||||
文件视口,工具栏保持可见。
|
||||
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。修改时间常显月日和时分,
|
||||
悬停显示完整日期时间;行悬停、键盘聚焦或菜单展开时,在时间位置显示更多操作,文件名宽度保持不变。
|
||||
窄栏中文件名省略,不因时间列撑宽列表。
|
||||
|
||||
US-WF1:选中子目录后导入二进制及空文件,目录刷新后可见,原文件不变。同名项失败而已有
|
||||
内容保留;成功项仍落在所选目录。
|
||||
|
||||
US-WF2:切换排序后文件按真实文件系统时间显示;不支持创建时间的文件系统不显示创建时间列,
|
||||
混合数据中缺失值保留为空。Linux 的 inode 状态变更时间不冒充创建时间。
|
||||
US-WF2:切换排序后文件按真实修改时间显示;即使文件系统提供创建时间,列表也只显示修改时间。
|
||||
缺失修改时间显示 `-`。文件夹使用目录本身的修改时间,不递归计算子文件的最新时间。
|
||||
|
||||
US-WF3:升级前会话图片在聊天和重新打开应用后仍可读取;打开工作区不会把会话图片或附件复制
|
||||
到项目目录。生成的普通文件由 Runtime 文件工具写入工作区并出现在同一目录列表。
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 聊天历史导航
|
||||
|
||||
聊天历史导航负责长会话的连续滚动、窗口化渲染,以及后续的对话 minimap。第一阶段先取消向上滚动时每次增加 80 条消息的产品限制,保留已有的消息窗口化机制;第二阶段再评估 minimap。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [产品需求](./prd.md) | 用户问题、范围、验收和非目标 |
|
||||
| [技术设计与测试计划](./technical-design.md) | 当前实现边界、实验分组、性能指标和执行步骤 |
|
||||
| [实施进度](./progress.md) | 已验证事实、命令、结果和遗留问题 |
|
||||
|
||||
## 术语
|
||||
|
||||
- **历史窗口**:允许滚动访问的消息范围。第一阶段扩大为当前已加载会话的全部消息。
|
||||
- **DOM 窗口**:实际挂载消息组件的范围。继续由消息高度估算和最多约 60 条挂载消息控制。
|
||||
- **minimap**:对话右侧的轻量纵向全局导航概览,不等于完整渲染所有消息。
|
||||
@@ -0,0 +1,34 @@
|
||||
# 聊天历史连续滚动
|
||||
|
||||
## 用户问题
|
||||
|
||||
长会话向上阅读时,当前界面每次只增加 80 条消息,并在顶部显示“加载更早的消息”控件。这个控件把连续滚动切成多个操作,用户无法像 VSCode 一样直接拖动滚动条或连续滚到会话顶部。
|
||||
|
||||
## 目标
|
||||
|
||||
- 已加载的当前会话可以从底部连续滚动到顶部。
|
||||
- 消息仍按可见范围挂载,长会话不会因为取消 80 条限制而一次性创建全部消息组件。
|
||||
- 滚动位置、锚点、消息展开状态和流式输出行为保持稳定。
|
||||
- 为后续 minimap 提供完整的消息索引和可定位滚动范围。
|
||||
|
||||
## 范围
|
||||
|
||||
- 取消真实聊天会话的 80 条历史窗口增量。
|
||||
- 保留现有消息窗口化、占位高度、滚动锚定和按需挂载。
|
||||
- 对 100、500、2,000、10,000 条消息进行功能和性能对比。
|
||||
- 记录 minimap 的下一阶段性能测试入口,但本阶段不实现 minimap。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改变未打开会话的摘要加载策略。
|
||||
- 不要求一次从数据库加载所有未加载的会话历史。
|
||||
- 不取消最多约 60 条消息组件的 DOM 窗口化限制。
|
||||
- 不在本阶段加入 minimap、消息摘要接口或新的 IPC。
|
||||
|
||||
## 产品验收
|
||||
|
||||
- 打开一个已加载历史的会话后,顶部不再出现“加载更早的消息”按钮。
|
||||
- 用户可以连续滚动到第一条消息,滚动位置不会因消息挂载或高度测量而跳动。
|
||||
- 2,000 条消息会话仍保持窗口化渲染,实际挂载消息数量维持现有上限附近。
|
||||
- 流式输出、输入、消息展开、附件和工具输出行为没有回归。
|
||||
- 若会话只有摘要,仍按现有逻辑按需加载完整历史。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 实施进度
|
||||
|
||||
## 2026-10-08
|
||||
|
||||
- 已确认 80 条限制由 `ChatHistoryPane` 的 `messageRenderBatchSize` 和 `revealEarlierMessages` 驱动。
|
||||
- 已确认消息窗口化仍以高度占位和最多约 60 条挂载消息为上限。
|
||||
- 已建立基线、本阶段和 minimap 阶段的对比测试计划。
|
||||
- 已将生产会话接线改为直接使用当前已加载消息总数,保留 DOM 窗口化。
|
||||
- `ChatHistoryPane` 与 `ChatTimeline` 定向测试通过:58/58。
|
||||
- 改动文件定向 web 类型检查和 lint 通过。
|
||||
- `npm run build:bundle` 通过。
|
||||
- `npm run perf:app` 通过。本次 B 组测量中,长会话打开交互延迟为 86.9 ms,长会话滚动帧间隔 p95 为 16.9 ms、无长任务;流式输入场景帧间隔 p95 为 16.9 ms,输入延迟 p95 为 17.1 ms。
|
||||
- B 组第二次运行仍通过:长会话打开交互延迟为 46.9 ms,长会话滚动帧间隔 p95 为 16.8 ms、无长任务;流式输入场景输入延迟 p95 为 19.1 ms。
|
||||
- App 测试通过 341/349;剩余 8 个失败集中在既有图片附件异步场景,未出现聊天历史窗口相关失败。
|
||||
- 全局 `npm run typecheck` 仍被既有 `src/main/desktop-storage-electron-fixture.ts:215-217` 类型错误阻断;全局 lint 仍包含生成的 VitePress cache 错误。
|
||||
- A 组临时恢复后连续两次在 composer 初始化阶段超时,未进入性能场景,因此没有有效 A 组数据,不能据此推导 A/B 数值差异。B 组代码已恢复并重新完成 bundle、定向类型检查、定向 lint 和 58/58 聊天窗口测试。
|
||||
- 下一步实现 minimap 前仍需先修复性能驱动在 A 组接线下的 composer 初始化问题,或建立不依赖 composer 的长会话专用 benchmark,再进行严格 A/B 测量。
|
||||
@@ -0,0 +1,52 @@
|
||||
# 技术设计与测试计划
|
||||
|
||||
## 当前边界
|
||||
|
||||
`ChatHistoryPane` 当前有两层机制:`messageRenderBatchSize = 80` 控制向上滚动时可访问的历史窗口;`maxRenderedMessageCount = 60` 控制实际挂载的消息组件。第一阶段只移除前者在生产接线中的作用,保留后者以及消息高度占位和锚点恢复。
|
||||
|
||||
已加载历史的消息数组仍由会话 store 提供。带 `messageSummary` 的会话继续先显示加载状态,由现有 `loadHistory` 路径取得完整历史。这样本实验测量的是滚动历史窗口的变化,不混入存储或 IPC 改造。
|
||||
|
||||
## 实验分组
|
||||
|
||||
| 组别 | 历史窗口 | DOM 窗口 | minimap |
|
||||
| --- | --- | --- | --- |
|
||||
| A 基线 | 每次增加 80 条 | 保留 | 无 |
|
||||
| B 本阶段 | 当前已加载消息全部可访问 | 保留 | 无 |
|
||||
| C 下一阶段 | 当前已加载消息全部可访问 | 保留 | 有 |
|
||||
|
||||
A 组保留在组件级回归测试中;B 组是当前生产实验;C 组待 B 组稳定后实现。
|
||||
|
||||
## 功能测试计划
|
||||
|
||||
- 100 条消息:打开会话后直接滚到第一条,不出现加载按钮。
|
||||
- 500 条消息:从底部连续滚到顶部,检查消息顺序、首尾消息和滚动位置。
|
||||
- 2,000 条消息:在中部、顶部和底部来回滚动,确认高度占位和锚点不跳动。
|
||||
- 消息交互:展开 reasoning/tool details、输入答案、选择文本、打开附件后滚出再滚回,确认状态保留。
|
||||
- 流式消息:生成过程中滚动、暂停跟随底部,再回到底部,确认自动跟随行为不变。
|
||||
- 摘要会话:打开只有 `messageSummary` 的会话,确认仍然先加载历史,不把摘要误当作完整消息。
|
||||
|
||||
## 性能测试计划
|
||||
|
||||
每个规模至少运行 5 次,分别记录冷打开和已经打开后的滚动表现。低端机模拟沿用已有性能测试配置,结果记录中注明运行环境。
|
||||
|
||||
记录指标:
|
||||
|
||||
- 会话打开到首屏可交互的耗时;
|
||||
- 首次滚到顶部的耗时;
|
||||
- 连续滚动期间长任务和掉帧;
|
||||
- Renderer 内存峰值和测试结束后的回收情况;
|
||||
- 实际挂载消息数量;
|
||||
- 流式输出时输入延迟。
|
||||
|
||||
通过条件:B 组功能测试全部通过,实际挂载消息数量仍受窗口上限控制;相对 A 组没有明显的输入、流式输出或滚动回归。若 B 组稳定,再以同一批数据实现 C 组并做增量对比。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```text
|
||||
npm exec vitest run src/renderer/src/ChatHistoryPane.test.tsx src/renderer/src/ChatTimeline.test.tsx
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run perf:app
|
||||
```
|
||||
|
||||
`perf:app` 的具体参数和输出以仓库脚本当前实现为准;无法在自动化环境观察的内存和掉帧指标,需要补充 Electron 手工记录。
|
||||
@@ -802,7 +802,7 @@ npx eslint scripts/review-algorithm.mjs scripts/review-algorithm.test.mjs script
|
||||
|
||||
### 2026-09-24 模型阶段超时、并发与设计边界
|
||||
|
||||
- 本次静态核对当前新代码:报告 `heartbeatReportTimeoutSeconds`、监督 `supervisorOrganizeTimeoutSeconds` 已接入共享校验、设置存储、App/设置表单及 Main 摘要器;整数 30..600 秒、默认 240。报告在等待池前冻结值,监督在摘要器入池前读取值,均在获槽及 Runtime 解析后才计时。完整控制及未覆盖环节见[控制清单](./review-scheduling-design.md#当前超时与调度控制清单已实现)。
|
||||
- 本次静态核对当前新代码:报告 `heartbeatReportTimeoutSeconds`、监督 `supervisorOrganizeTimeoutSeconds` 已接入共享校验、设置存储、App/设置表单及 Main 摘要器;整数 30..600 秒、默认 240。报告在等待池前冻结值,监督在摘要器入池前读取值,均在获槽及 Runtime 解析后才计时。该次记录对应的完整控制及未覆盖环节见[初稿控制清单](./review-scheduling-design.md#初稿时超时与调度控制清单历史截面),不代表当前实现。
|
||||
- `supervisorModelConcurrency` 已接入同一设置链路,整数 1..4、默认 1;报告与监督共用 FIFO 池,普通聊天排除,降限不终止在途项。报告获槽后才领取,每次到期领取最多一条;租约为 `max(300, 报告超时 + 60)` 秒,实际请求开始前按运行 ID、owner、attempt、claimed 和未过期条件校验并刷新。报告提交或无变化后先释放槽位,再进入下游监督。尚无提供商全局并发、RPM、TPM 或监督层 Retry-After 控制。
|
||||
- 本轮协作提供的新增验证记录:24 项并发相关测试、33 项租约相关测试及 typecheck、lint 通过。本次文档修订仅核对实现,没有重新执行这些测试;记录未附精确过滤命令,不补造命令或与此前批次相加。
|
||||
- 实现代理报告:87 项测试、App/IPC 聚焦用例、另 19 项取消测试,以及 typecheck、lint 通过。本次文档修订未复跑这些命令,不将数量相加当作独立用例总数,也不推断全量测试已通过。
|
||||
|
||||
@@ -98,9 +98,11 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
|
||||
|
||||
目标是不再静默裁剪,把单次上限改成分页、分块和暂缓派发的依据,直至冻结范围内全部项目、会话、消息及其他入选来源处理完毕。模型提取仍可能遗漏原文含义,但已产出的叶子事实、事件、冲突及来源必须完整保留、可分页读取。摘要只用于导航,不能替代叶子结果;“原文已处理”也不等于模型识别了全部事实。
|
||||
|
||||
### 当前超时与调度控制清单(已实现)
|
||||
### 初稿时超时与调度控制清单(历史截面)
|
||||
|
||||
以下是当前代码事实,不是后文拟议的多层调度预算。设置暴露两个阶段超时和一个监督并发数。
|
||||
以下表格及其后说明保留初稿时的实现截面,不定义当前生产行为或本次存储迁移需要新增的机制。
|
||||
其中“无用户取消入口”“收齐后检查 100KB”和原心跳报告交接已被后续实现替代。
|
||||
现状以本页第 0 节及[监督者技术设计](./technical-design.md)为准,复用既有回顾准入、模型池和暂停/继续机制,不据此再建一套调度器。
|
||||
|
||||
| 控制 | 实际值、读取时点与范围 | 代码依据与边界 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -21,7 +21,7 @@ FR-S1:将现有智能心跳入口整合、升级为监督者,不增加另一
|
||||
|
||||
FR-S2:应用包含“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”五个主视图。工作回顾提供统一监督结果阅读、历史选择和手动新回顾,结果显示自身范围与日期;自动监督集中展示计划状态、范围、周期及下次执行时间,以及运行概览、指标、趋势、自动监督报告、待处理记忆与行动建议和心跳审计,创建和编辑使用同一份 Modal 表单;活动记录展示统一实际执行、进度、详情及结果入口,支持按计划 ID 筛选;设置管理模型选择、超时、并发、分页、分批及单次响应容量,分类页签见 UI 设计。计划保存失败保留草稿,关闭未保存修改须确认;编辑已暂停计划保持暂停,创建须显式保存并启用。原有运行记录及应用、存储标识保留。
|
||||
|
||||
当前设置还支持分别调整报告与监督整理的模型超时,以及两者共用的监督并发数。并发设置不限制普通聊天,也不保证避免上游限流;排队不消耗模型阶段时限,降低并发不打断在途请求。取值及生效规则由[当前控制清单](./review-scheduling-design.md#当前超时与调度控制清单已实现)定义。
|
||||
当前设置支持监督整理模型超时及监督模型并发数。并发设置不限制普通聊天,也不保证避免上游限流;排队不消耗模型阶段时限,降低并发不打断在途请求。取值、生效规则及历史报告设置的保留边界由[模型阶段调度](./technical-design.md#模型阶段超时与调度)定义。
|
||||
|
||||
FR-S13:监督者的“设置 → 模型”可选择跟随应用默认文本模型或已有文本模型连接,复用连接及其凭据。回顾执行中修改设置不切换模型;生效时点、继续和重试规则见[监督者模型选择](./logic-design.md#监督者模型选择)。
|
||||
|
||||
|
||||
@@ -129,7 +129,9 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
|
||||
|
||||
时间螺旋使用所选回顾的时间区间与完整事件集合,半径和区间着色规则见 [3D 密度规则](../story-graph/3d-concept.md)。当前故事成员按精确事件 ID 与结果求交;其余事件按来源确定的项目进入“当前无故事归属”分组,项目未知时显示“项目范围不可用”。图例说明“本次回顾事件按当前故事归属分组;未保存当时的故事归属”。故事与经验列表仍提供该范围的当前完整记录,供查看和调整。
|
||||
|
||||
根视图直接显示事件点,同一主故事的邻近事件按画布密度聚合并显示数量,未归属点使用中性色。事件和经验从左侧列表选择,画布不重复提供这两种选择器。保留一个层级菜单:左侧列表选择记录并打开详情,标题区层级菜单改变画布中的项目、功能或子线索层级。菜单与面包屑均可用键盘操作。默认斜俯视,不显示视角按钮、菜单或页签;自由拖动、平移、滚轮、方向键、数字键和 Escape 返回上层操作保留。
|
||||
根视图直接显示事件点,同一主故事的邻近事件按画布密度聚合并显示数量,未归属点使用中性色。事件和经验可从左侧列表或画布标记选择,画布不重复提供这两种选择器。保留一个层级菜单:左侧列表选择记录并打开详情,标题区层级菜单改变画布中的项目、功能或子线索层级。菜单与面包屑均可用键盘操作。无选择时默认斜俯视,不显示视角按钮、菜单或页签;自由拖动、平移、滚轮、方向键、数字键和 Escape 返回上层操作保留。
|
||||
|
||||
时间螺旋选中故事、事件或经验后,相机沿较短方向旋转并平移到对应位置,保留当前缩放;缩放超出 0.8~1.4 时收回到该区间。列表选择的目标不在当前层级时,切换到能显示该目标的层级;经验须在本次回顾内有跨木片的形成与应用连接才能定位,没有对应几何对象的记录仍可查看详情。选中的经验不受默认六条连接的显示上限限制。鼠标拖动、滚轮和方向键立即接管相机,后续相同选择和同内容刷新不重新定位。启用减少动态效果时直接定位,离开视图时取消未完成动画。
|
||||
|
||||
关联来源详情属于会话时,显示“打开会话”按钮,使用来源保存的会话 ID 跳转并切换到所属项目;不发送消息或启动回顾。另提供“继续讨论”,使用上述可编辑正文预览,点击发送后提交到来源会话并打开它。详情继续展示整理时保存的原文快照。会话已删除或读取失败时,沿用应用会话导航的提示;其他来源类型不显示此按钮。
|
||||
|
||||
|
||||
@@ -10,7 +10,24 @@
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联入口 | [直连模型 Agent 能力](./README.md) |
|
||||
|
||||
## 当前结论
|
||||
## 2026-10-05 工具执行异常回传
|
||||
|
||||
- 普通工具执行异常也返回模型,保留失败活动与错误详情,由模型决定后续行动;规则见
|
||||
[失败恢复](./technical-design.md#读取与搜索失败恢复)。请求取消与 `AbortError` 仍停止循环。
|
||||
- 新增三协议回归使用真实 Provider、进程服务和文件系统:无效 cwd 产生 `ENOENT`,
|
||||
下一次模拟模型请求收到对应失败结果,再调用同一工具执行真实命令成功。另覆盖错误详情
|
||||
有界处理和取消;未使用真实外部模型,不作为桌面 UI 或真实模型自主决策验收。
|
||||
- 修复验证中发现 `DOMException` 不一定满足 `instanceof Error`,改为按错误名称识别取消;
|
||||
取消测试在意外后续模型请求时立即失败,避免模拟响应无限重复耗尽内存。
|
||||
- `model-runtime.test.ts`、`model-tool-provider.test.ts`、`direct-model-process-service.test.ts`:
|
||||
161 项通过、1 项跳过。`npm run typecheck`、`npm run lint` 通过。
|
||||
- 全量 `npm test`:6,230 项通过、87 项跳过、1 项失败;失败为
|
||||
`obsidian-electron.test.ts:29` 要求临时目录在仓库外,与本次按仓库规范设置
|
||||
`TEMP`/`TMP` 到仓库 `temp/` 冲突。未修改该测试,全量不能记为通过。
|
||||
- 托管 SSH 使用远端 OpenCode/Continue 的工具循环,不运行本次修改的 `ModelAgentRuntime`;
|
||||
不涉及 gbagent 生产路径。临时目录创建、保留和清理逻辑未修改。
|
||||
|
||||
## 既有本机验收结论
|
||||
|
||||
本机生产路径已经完成源码接线:直连模型 Execute 可运行平台 Shell,Ask/Execute 均可按
|
||||
父模式委派单层编程 Subagent;长输出可通过 `output_read` 续读。OpenCode、Continue、
|
||||
|
||||
@@ -98,8 +98,11 @@ type BuiltinModelToolSummary = {
|
||||
`EISDIR`、`EACCES`、`EPERM`、`ELOOP`、`ENAMETOOLONG` 等已列明的文件系统错误。
|
||||
Runtime 向模型返回 `{ ok: false, recoverable: true, error, nextAction }` 的 JSON 文本工具结果,
|
||||
工具活动记为 `recoverable`,继续工具循环,由模型修正路径、分页参数、glob 或搜索表达式;
|
||||
提示不得原样重复失败参数。取消、远端断线、失效工作区、缺失 rg 可执行文件及未分类的内部
|
||||
错误继续向外传播,不转为可恢复结果。
|
||||
提示不得原样重复失败参数。其他工具执行异常(包括进程启动或输出文件的 `ENOENT`、
|
||||
远端断线、失效工作区、缺失 rg 可执行文件)由 Runtime 返回 `{ ok: false, error }`,
|
||||
工具活动记为 `failed`,错误详情使用已有有界处理并保留 cause 信息。失败结果继续进入
|
||||
下一次模型请求,由模型决定重试、调整方法或说明失败;不自动重放工具,也不增加重试次数限制。
|
||||
Anthropic 的失败结果同时设置 `is_error: true`。请求取消及 `AbortError` 仍向外传播并停止循环。
|
||||
|
||||
`workspace_read_text` 仍只接受工作区内相对路径。搜索原样接受 rg 参数及账号可访问的路径,
|
||||
不增加逐工具授权;保留 `--no-config` 默认参数、原生退出码、取消和分页,不提升系统权限。
|
||||
|
||||
@@ -116,13 +116,11 @@ confidence 改为可选,保持本地返回原值;多块内容组成逐页 se
|
||||
original.<ext>
|
||||
documents/<attachmentId>/
|
||||
original.<ext>
|
||||
parsed.md
|
||||
manifest.json
|
||||
images/<imageId>.<ext>
|
||||
knowledge/<kbId>/<sourceId>/
|
||||
<managed-original-files>
|
||||
knowledge-assets/<kbId>/<documentId>/
|
||||
parsed.md
|
||||
manifest.json
|
||||
images/<imageId>.<ext>
|
||||
temp/document-parsing/<operationId>/
|
||||
@@ -155,7 +153,7 @@ confidence 改为可选,保持本地返回原值;多块内容组成逐页 se
|
||||
也需验证同一规则。采用暂存、现有事务和按数据库引用核对清理,不新增恢复引擎。
|
||||
|
||||
测试解析使用任务临时目录,结束预览且无有效引用后清理。知识库正文及 chunks 继续存数据库,
|
||||
`knowledge-assets` 的 `parsed.md` 为解析正文文件,随同一次结果提交保持一致,不独立修改
|
||||
`knowledge-assets` 的 `manifest.json` 保存解析正文,随同一次结果提交保持一致,不独立修改
|
||||
或替代数据库中的检索内容。图片归知识库文档所有,不依赖聊天存活;managed 与 reference
|
||||
派生文件均存该目录,reference 外部原件保持原路径,现有 managed 原件目录不迁移。
|
||||
重解析或同步先在本次暂存目录完成新结果与必要资源校验,成功提交替换后清理无引用旧资源;
|
||||
|
||||
@@ -44,17 +44,21 @@ PPTX 回退提示按实际 OCR 来源显示,并在图片处理失败时包含
|
||||
conversation-assets/<conversationId>/
|
||||
images/<resourceId>/original.<ext>
|
||||
documents/<resourceId>/original.<ext>
|
||||
documents/<resourceId>/parsed.md
|
||||
documents/<resourceId>/manifest.json
|
||||
documents/<resourceId>/images/<imageId>
|
||||
knowledge/<libraryId>/<sourceId>/<managed-original>
|
||||
knowledge-assets/<libraryId>/<documentId>/<resultId>/
|
||||
parsed.md
|
||||
manifest.json
|
||||
images/<imageId>
|
||||
temp/document-parsing/<resourceId>/
|
||||
```
|
||||
|
||||
S04 (2026-10-05): new results store parsed content in `manifest.json` only;
|
||||
`parsed.md` is no longer written or copied. Released manifests remain readable,
|
||||
including directories containing the old duplicate. Async storage-host integration
|
||||
and its remaining composition work are specified in
|
||||
[Desktop File Storage Integration](../../architecture/desktop-storage-files.md).
|
||||
|
||||
会话资产的 `request.json` 保存已转换的请求内容,用于恢复内存 context;原件字节单独保存,
|
||||
缩放和 JPEG 请求副本不覆盖原图。提取图片文件使用 UUID 无扩展名,MIME 在清单记录。
|
||||
图片显式 OCR 后生成解析文件。切回原图保留已有结果,并重新进入图片能力校验。
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Magic Notes Progress
|
||||
|
||||
## 2026-10-08: Rich Text Table Editor Fix
|
||||
|
||||
Replaced the Electron-incompatible native prompt flow for table insertion with the shared form
|
||||
Modal. The dialog validates 1-20 rows and 1-12 columns, supports cancellation and Escape, restores
|
||||
focus to the toolbar trigger, and preserves the editor selection while it is open. Table row,
|
||||
column, and delete controls now use compact localized icon buttons, remain disabled outside a table
|
||||
cell, guard against nested insertion, preserve the active cell, and create separate Quill undo
|
||||
steps. Non-empty table Delta content remains readable in the read-only record renderer.
|
||||
|
||||
Focused validation: `npx vitest run src/renderer/src/MagicNoteEditor.test.tsx
|
||||
src/renderer/src/MagicNoteContent.test.tsx` passed 18 tests. `npm run typecheck` and affected-file
|
||||
ESLint passed. Full suite and runtime Electron validation were not run.
|
||||
|
||||
The table content check now also applies to save validation. An empty-cell table remains an active
|
||||
draft after focus leaves the composer, saves through the renderer, IPC and storage layers, and is
|
||||
restored after reopening. A rich Delta containing only a terminal newline remains rejected, and a
|
||||
failed save leaves the draft available for retry. Focused renderer and storage regression tests
|
||||
cover these cases.
|
||||
|
||||
## 2026-10-03: Compact Notes List And Narrow Drawer
|
||||
|
||||
The notes list now defaults to 200px with a 160px minimum. At workspace widths
|
||||
|
||||
@@ -71,6 +71,16 @@ save remounts at the returned revision and establishes a new baseline; an unchan
|
||||
background revision retains the existing editor and baseline. Aborted canvas
|
||||
initialization and file reads for an unmounted text editor cannot write back.
|
||||
|
||||
Rich text tables remain native Quill table Delta operations. Insertion stores the active editor
|
||||
range while the shared table-size dialog is open, rejects invalid dimensions before changing the
|
||||
Delta, and refuses insertion when that range is already a table cell. Row, column, and whole-table
|
||||
commands require a cell selection, preserve the selected range, and cut Quill history around each
|
||||
command so each toolbar action is independently undoable. The read-only rich renderer loads the
|
||||
same Delta with Quill's `readOnly` mode, so table content follows the same persisted schema without
|
||||
an editable toolbar. A table row operation is content even when its cell text is empty: the
|
||||
workspace keeps the composer open after focus leaves it and submits the Delta through the normal
|
||||
IPC and storage path. A genuinely empty rich Delta is still rejected before IPC.
|
||||
|
||||
The real saved formatted-text probe reproduced identical values with different
|
||||
Delta key order: storage returned `{insert, attributes}` and Quill emitted
|
||||
`{attributes, insert}`. The old `JSON.stringify` comparison therefore reported
|
||||
|
||||
@@ -49,4 +49,12 @@
|
||||
|
||||
Tab 使用工作栏现有的共享页签、焦点和关闭模式。搜索有持久标签,列表用可键盘访问的按钮;打开采集后聚焦预览,取消返回触发按钮,触发按钮已不可见时尝试返回会话标题菜单按钮。新建标题出现后聚焦输入框。面板中的 Escape 取消当前草稿,或从详情返回列表;需要放弃输入时先显示草稿确认。确认框初始聚焦“继续编辑”,限制 Tab 焦点范围,Escape 保留草稿并关闭确认。
|
||||
|
||||
## 富文本表格
|
||||
|
||||
富文本编辑器使用共享表单 Modal 插入表格,行数限制为 1-20,列数限制为
|
||||
1-12。取消、关闭、Escape 和遮罩点击不会修改内容,关闭后焦点返回“插入表格”
|
||||
按钮。表格操作使用紧凑图标按钮,保留本地化的可访问名称和提示;未选中表格单元格
|
||||
时,插入行、插入列和删除操作保持禁用,避免误操作或嵌套表格。每个表格操作形成
|
||||
独立的撤销步骤。已保存表格在只读记录视图中继续由 Quill 以不可编辑单元格显示。
|
||||
|
||||
面板服从工作栏现有宽度和停靠规则,以单列切换列表、详情和采集编辑。按钮组可换行,文字摘要保留换行并折行长文本;Markdown 和代码以原始文字显示。头部与快速追加区固定,中间内容独立滚动;完整预览输入区可纵向调整大小。配色、按钮、焦点环、通知和确认使用共享组件与令牌。真实 Electron 已在实测 214px 面板验证无横向溢出,以及原生键盘返回列表、打开笔记和追加保存。
|
||||
|
||||
@@ -10,7 +10,7 @@ Smart Heartbeat 已提供按计划或手动执行的有界回顾,生成报告
|
||||
|
||||
## 文档导航
|
||||
|
||||
当前监督者设置已接入报告与监督整理两项模型阶段超时(30 至 600 秒,默认 240)及共享监督并发数(1 至 4,默认 1),模型超时不包含排队、采集、保存或整次执行期限。报告先获槽再领取,实际请求开始前校验并刷新租约,报告提交或无变化后释放槽位再进入监督。普通聊天不入监督池,尚无跨聊天与监督的提供商全局并发或 RPM/TPM 限制。租约、固定传输时限及重试与用户超时分开,见[控制清单](../conversation-supervision/review-scheduling-design.md#当前超时与调度控制清单已实现)。完整分页与持久化续跑仍为设计,不能把现有增量检查称为完整调度器。
|
||||
当前心跳将到期计划交给监督回顾,不再额外生成独立报告。监督整理使用模型阶段超时和共享监督并发池,普通聊天不入池;模型超时不包含排队、采集和保存,也不是整次回顾期限。分页、批次持久化和暂停/继续已接入生产路径,范围及剩余限制见[生产接线](../conversation-supervision/review-scheduling-design.md#0-生产接线与剩余边界)。超时、并发、重试及保留的历史报告设置由[模型阶段调度](../conversation-supervision/technical-design.md#模型阶段超时与调度)定义,不据初稿控制表另建调度器。
|
||||
|
||||
监督者“活动记录”页签同时显示手动回顾和心跳执行,将新心跳报告与下游监督按运行 ID 合并,保留各阶段真实状态。心跳审计、运行概览、指标、趋势、自动监督报告阅读和建议处理与计划列表同在自动监督页;工作回顾只读取统一监督结果,设置只保留配置。当前布局见[监督者 UI 设计](../conversation-supervision/ui-design.md#应用入口)。心跳审计继续读取报告自身状态,完整执行状态以[活动规则](../conversation-supervision/logic-design.md#活动记录)为准。
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ type HeartbeatScope =
|
||||
- Main 在创建、编辑、列出和执行时验证 Project 归属。
|
||||
- Global 读取所有 Project 的有界会话和任务,但长期记忆仍只读取 Global。
|
||||
- 指定 Project 读取选中项目的有界会话和任务,以及 Global 与选中项目记忆。
|
||||
- 现有输入条数、字符预算、输出大小、超时、重试、租约和工具禁用边界继续生效。报告与监督整理共用监督专用并发池,普通聊天不入池;具体设置、租约和未实现的限流边界见[当前控制清单](../conversation-supervision/review-scheduling-design.md#当前超时与调度控制清单已实现)。
|
||||
- 心跳交接、监督分页与输出容量、模型阶段超时、重试和工具禁用边界以[生产接线](../conversation-supervision/review-scheduling-design.md#0-生产接线与剩余边界)及[模型阶段调度](../conversation-supervision/technical-design.md#模型阶段超时与调度)为准。监督模型池不限制普通聊天,不将历史独立报告阶段作为当前必须执行的路径。
|
||||
- 多项目汇总后统一应用上限,不能按项目倍增预算。
|
||||
- 心跳结果默认不在系统通知中暴露私人正文。
|
||||
- 数据迁移必须使用 SQLite 事务,保留外键、级联删除和现有历史。
|
||||
|
||||
@@ -259,3 +259,8 @@ SSH 专家保留父请求已解析的 OpenCode 或 Continue 选择,通过现
|
||||
`deai-writing` 扫描九份文档,阻断项为 0。复核项主要为未验证状态、数据保留条件和字段枚举;结合技术文档语境保留这些限定,不将迁移计划改写为已完成事实。
|
||||
|
||||
`git diff --check -- docs/features/unified-execution docs/features/assistant-workbar/README.md` 通过已跟踪改动的空白检查。新建文档另经内容与链接核对;该命令不会把未跟踪文件当作已验证差异。文档检查不替代运行时验收,后者按用户故事与技术设计执行。
|
||||
|
||||
## 2026-10-05 直连模型工具错误回传
|
||||
|
||||
普通工具执行失败不再直接终止直连模型循环,错误结果继续交给模型处理,取消行为保留。
|
||||
实现、三协议回归及全量测试边界见[直连模型修复记录](../direct-model-agent/progress.md#2026-10-05-工具执行异常回传)。
|
||||
|
||||
@@ -1,388 +1,77 @@
|
||||
# GoodBuddy 更新:故事图谱、Obsidian接入、Runtime原生终端
|
||||
# GoodBuddy 更新:故事图谱、Telegram 与 Obsidian 接入
|
||||
|
||||
> **编辑标注说明(对外发布前删除):** 本稿汇总桌面版 0.13.8 至 0.15.11,以及其后截至 `e50ca6d1` 的已提交变化,更新于 2026-10-05。所述功能与修复均已开发完成并提交,验证及发布归属另行标注;原故事线概念图仅作示意,待换为生产界面截图。“待补图”表示缺少可发布素材,“待替换”表示发布信息未核验,“待核对”表示发布前需要确认的行为。本文是宣传草稿,尚未对外发布。
|
||||
> **编辑标注说明(对外发布前删除):** 本稿汇总桌面版 0.13.8 至 0.15.13,以及截至 `3556df88` 的已提交变化,更新于 2026-10-09。本文所述功能均已完成并提交;截图、下载信息和正式发布链接属于发布材料,另行补齐。原故事线概念图仅作示意,发布前替换为生产界面截图。
|
||||
|
||||
GoodBuddy 这一轮更新围绕工作记录的整理与使用展开:监督者把分散的事件归入持续发展的故事,从中整理带有来源和适用条件的经验,供人和助手在后续工作中查阅。笔记连接支持保存聊天正文、返回原会话,以及在聊天中读写本机 Obsidian 笔记。本地项目还可以从输入区打开 Continue、OpenCode 原生终端或 DS Web 客户端,沿用当前项目与模型配置。
|
||||
GoodBuddy 这一版完成了三条连接:用故事图谱整理持续推进的工作,用 Telegram 在桌面之外发起对话,用 Obsidian 连接已有的本地笔记。故事图谱帮助人和助手回看决定、依据与后续变化;Telegram 适合在手机或其他设备上继续处理文字请求;Obsidian 则让原有笔记仓库可以直接参与对话。
|
||||
|
||||
本文接续上一份画布笔记与文档处理更新稿,覆盖 0.13.8 起的实际增量。这次还加入了智能心跳、笔记标签、会话图片保存、Telegram 私聊和工作区文件导入。项目与会话集中到同一个菜单,提问和执行任务也无需再切换工作模式。远程统一执行与文件导入建议配套 Desktop 0.15.11 和 Agent 0.15.4,请先升级桌面端,再更新 Host 环境;最新远程启动修复还需包含后续提交的 Agent。
|
||||
当前配套版本为 Desktop `0.15.13` 和 Agent `0.15.6`。远程项目使用这些能力前,请先升级桌面端,再更新 Host 环境。
|
||||
|
||||
> **【待替换 R01|版本与体验入口】** 最新已核查桌面 tag `v0.15.11` 与 Agent tag `agent-v0.15.4` 均指向 `78afec77`。本轮检查 `9527402..e50ca6d1`,共 60 个提交;Telegram 私聊、历史耗时缺失提示、自定义回顾日期及最新运行修复位于这些 tag 之后,发布版本待确认。对外发布前核验公开安装包、发布日期、下载地址和完整更新日志链接,并在此补入。桌面 0.13.14 的发布失败后由 0.13.15 承接。
|
||||
> **【发布信息待补】** 正式发布前补入发布日期、公开安装包、下载地址和完整更新日志链接。本文所述功能已经完成,发布信息待补不代表功能处于开发中。
|
||||
|
||||
**升级前,请先关闭应用并备份完整用户数据。** 数据库、设置和相邻的 `notes/` 目录需要成套保留;如有 WAL 文件,也应一并备份。当前核查提交的助理数据库格式为 61,0.15.11 标签对应格式为 60;升级后旧客户端不能直接打开新版数据库,回退需要升级前的备份。大库首次转换可能需要等待。高频本地保存调整了磁盘同步策略,断电时可能丢失最近尚未同步的写入。
|
||||
## 故事图谱:从工作记录中看清一件事如何发展
|
||||
|
||||
## 新增功能
|
||||
### 工作回顾留下进展和依据
|
||||
|
||||
### 监督者:从工作记录中整理进展,查看每条结论的依据
|
||||
监督者可以按全局或项目范围回顾最近一天、数天或指定日期内的工作记录,整理出工作总结、变化和未解决事项,并按故事列出推进、新出现和结束的内容。回顾结果会保留范围与生成时间,方便之后回看。
|
||||
|
||||
项目讨论持续一段时间后,可以打开“监督者”的“工作回顾”,选择全局或一个项目,再选择最近 1 天、7 天、30 天、自定义最近天数或起止日期。指定日期按本地时区包含结束日全天,结束日为今天时截至发起回顾的时刻。回顾会整理工作总结、本次变化和未解决事项,并按故事列出这段时间的推进、新出现与结束的事项,以及形成或用上的经验。点击故事或经验,可以进入图谱查看依据;历史结果保留各自的范围和生成时间,方便回看。
|
||||
每条结论都可以继续追溯到事件、实体、关系和来源正文。打开来源后,可以检查模型依据的是哪段讨论,再决定是否确认、改名或调整关系。
|
||||
|
||||
监督者默认关闭,在应用中心启用后即可手动回顾,无需先建立自动计划。当前分析来源包括范围内的用户与助手消息、任务标题与当前状态,以及消息中已有的知识引用片段。它不会自动遍历全部笔记、整个知识库或附件原件。
|
||||
### 故事、经验和时间螺旋
|
||||
|
||||
> **【待补图 01|必补|工作回顾结果】** 在隔离演示项目中通过正常聊天和任务操作形成公开、虚构的工作记录,运行一次真实回顾。截图同时展示范围、时间和实际生成的总结、未解决事项;不能用预置答案冒充模型结果。计划文件:`assets/01-work-review.png`。
|
||||
>
|
||||
> 建议图注:按项目回顾近期讨论与任务,查看进展和仍待处理的问题。
|
||||
事件可以归入持续发展的故事,按项目、功能和子线索浏览。用户可以调整故事名称、合并重复故事、移动事件或撤销调整。故事中的经验会保留适用条件、形成依据和后续应用记录,也可以人工编辑、合并或删除。
|
||||
|
||||
回顾中的事件、实体与关系可以在“故事线图谱”中查看。选中一个对象,就能检查它的说明、关联和保存的来源正文,再判断模型的归纳是否准确。实体支持确认、改名或撤销,关系可以确认或移除。
|
||||
|
||||
这适合检查“某个结论来自哪段讨论”“一个事项与哪些事件有关”。图谱提供平铺和时间螺旋两种浏览方式,时间螺旋按事件时间排列;平铺视图中的节点间距不表示实际时长。历史回顾中的故事分组使用当前保存的故事结构,尚不提供任意时刻的完整状态还原。
|
||||
|
||||
> **【待补图 02|必补|图谱与来源】** 使用图 01 同一份真实回顾结果,选中事件或实体,展示对应的来源正文。画面应能辨认对象与依据的关联;不使用设计原型代替生产结果。计划文件:`assets/02-story-sources.png`。
|
||||
>
|
||||
> 建议图注:从回顾图谱查看关联对象,并打开保存的讨论依据。
|
||||
|
||||
### 故事与时间螺旋:追踪一件事如何持续发展
|
||||
|
||||
一项功能往往经过多次讨论、试验和修订。回顾后,相关事件可以归入同一个故事,按项目、功能和子线索逐层浏览;后续有新进展时继续积累,不必每次从零查找。用户可以调整名称、合并重复故事、移动归错的事件,也能撤销调整;移除故事会保留其中的事件。
|
||||
|
||||
时间螺旋把这些故事放到连续时间上,便于查看不同工作如何交错推进。同一件事可以隔一段时间再继续,中间的停顿保留下来。螺旋高度对应时间,半径按所选回顾范围内的消息密度变化,不代表实际工时、效率或成果价值;消息按小时汇总,无法还原小时内的精细分布。
|
||||
|
||||
实际事件区间和事件点使用主故事的颜色,空白区间与未归属事件保持中性色。历史回顾中尚未归入故事的事件也会保留,可以在“未归属”分组查看;故事分组采用当前关系,不把它当作当时分类的完整还原。
|
||||
|
||||
视图支持斜俯视、侧视、俯视和自由视角,可以逐层进入项目、功能与子线索,选中故事或事件查看详情。当前层可显示部分经验及其跨故事应用连线,完整经验列表仍在“经验”页签中查阅。设备无法使用 3D 时,可以切回平铺视图。
|
||||
故事图谱提供平铺和时间螺旋两种视图。时间螺旋按事件时间和消息密度展示工作如何交错推进,适合观察停顿、延续和阶段变化;平铺视图适合查看对象之间的关系。设备不适合 3D 浏览时,可以切换回平铺视图。
|
||||
|
||||

|
||||
|
||||
*早期概念示意,使用示例数据;不是当前生产界面的截图,实际布局与操作以应用为准。*
|
||||
*早期概念示意,使用示例数据;发布前替换为当前版本的生产界面截图。*
|
||||
|
||||
> **【待补图 09|必补|故事与经验的生产视图】** 原图保留作概念参考。发布前用目标版本的真实回顾结果替换,展示同一故事的事件、经验形成依据与后续应用,并核对平铺和时间螺旋中的选择联动。计划文件:`assets/09-story-experience.png`。
|
||||
>
|
||||
> 建议图注:沿故事查看工作进展,从经验节点核对形成依据与后续应用记录。
|
||||
### 助手可以接着既有决定继续工作
|
||||
|
||||
### 从故事中整理经验,保留适用条件和依据
|
||||
当用户追问“此前为什么选这个方案”或“这项决定后来怎样修改”时,助手可以查询已经保存的故事、经验、事件、实体、关系和回顾结果,再读取相关来源。查询发生在需要时,不会在每次对话中自动注入整张图谱。
|
||||
|
||||
故事积累到一定规模,并出现新的决定、变化或里程碑后,监督者可以从事件摘要中归纳经验,保存适用条件、边界和形成依据。自动归纳的内容有明确标记,用户可以编辑、合并或删除,再回到原讨论核对。
|
||||
输入区可以单独控制当前会话是否允许访问故事图谱。查询默认使用当前项目,也可以指定全局或项目集合;故事图谱工具只读取已有记录,不会因为一次查询重新运行回顾或修改图谱。
|
||||
|
||||
后续回顾还可以记录某条经验在哪些事件中得到应用。应用记录会核对事件是否晚于最早形成事件的起始时间,并与至少一个形成事件涉及共同实体;这些记录仍是模型对历史的归纳,需要结合来源判断,不能据此断定经验已经产生效果。助手可以查阅这些经验,为后续工作提供参考。
|
||||
### 智能心跳持续整理变化
|
||||
|
||||
### 助手可以查阅故事图谱,接着已有决定继续工作
|
||||
设置每日或每周计划后,智能心跳会处理新增和变化的工作记录,更新故事图谱,并提出未决事项、分歧、修订、候选约定、长期没有进展的故事和可能适用的经验。没有新的工作来源时会跳过事实提取;建议生成失败可以单独重试,已保存的回顾和图谱仍然保留。
|
||||
|
||||
继续一个项目时,可以请助手“查一下此前选了哪个方案,依据是什么”,或“找出这项决定后来的修订”。助手可搜索监督者已经保存的故事、经验、事件、实体、关系和回顾结果,读取相关上下文,再查看来源片段,作为后续讨论的依据。故事上下文带出相关事件,经验上下文带出形成和应用记录。
|
||||
## Telegram:从私聊继续处理任务
|
||||
|
||||
工具说明也会引导助手在继续旧工作、追问决定原因或遇到可能已有约定的任务时主动查阅,优先找故事与经验。是否调用仍由当前模型判断;普通闲聊和独立的通用问题不要求查询,也不会在每次对话中自动塞入整张图谱。
|
||||
配置自己创建的 Telegram Bot、数字用户 ID 白名单和通道项目后,可以通过 Telegram 私聊发送文字问题或任务。GoodBuddy 会沿用该项目的 Runtime、模型和工作目录处理请求,并把最终文字回复返回到 Telegram,桌面端同时保留会话记录。
|
||||
|
||||
在应用中心启用监督者,并在 MCP 设置中保持 Story Graph 开启、分配给所用 Runtime 后即可使用。支持直连模型、DeepSeek Harness,以及本地和远程 OpenCode、Continue;本地 Continue、OpenCode 原生终端也可读取。
|
||||
首次配置时可以先留空发送者 ID,启用并保存通道;连接 Bot 后发送 `/start` 和 `/whoami` 获取自己的数字用户 ID,再填入白名单并保存。连接支持断线恢复和停用取消,桌面应用需要保持运行并能连接 Telegram。
|
||||
|
||||
输入区的选项中可以为当前会话单独开关故事图谱访问。默认允许查询,但仍受监督者总开关和工具配置约束;开启不会自动运行回顾或注入上下文,关闭也不会删除图谱,或将这个会话排除在后续回顾之外。
|
||||
当前通道面向单个 Bot 的普通私聊和文字请求。群聊、媒体输入和附件发送仍在桌面端处理,图片等附件结果需要回到桌面查看。
|
||||
|
||||
查询默认使用当前项目,也可明确指定全局或项目集合。事件可从共享时间线中按项目查阅,回顾结果仍保留各自的范围。工具只读取已有记录,不会为一次查询重新运行回顾或修改图谱;助手根据结果生成回答仍使用当前模型。保存的来源片段可以分页读取,尚不支持任意时刻的完整历史状态重建。
|
||||
## Obsidian:直接连接已有笔记仓库
|
||||
|
||||
### 智能心跳:按新增工作回顾,并提出可处理的建议
|
||||
在“设置 → 能力与工具 → MCP”的 Obsidian 卡片中,可以选择本机已注册的仓库或限定文件夹,测试连接并分配给所用 Runtime。无需启动 Obsidian,也无需安装插件或配置 REST 服务。
|
||||
|
||||
为项目设置每日或每周计划后,智能心跳会整理回顾窗口内新增、修改或尚待处理的记录,把变化保存到故事图谱。计划默认生成建议,也可以选择“仅更新记忆”;没有新来源时跳过事实提取,仍可补做尚未完成的故事与经验整理。没有待处理工作时,不调用模型。
|
||||
接入后,可以让助手搜索和读取笔记,也可以把讨论结果写回仓库。支持追加、局部修改、目录浏览,以及标签和属性操作。笔记仍保存在原仓库,不会自动复制到 GoodBuddy 知识库。
|
||||
|
||||
建议围绕本次回顾发现的未决事项、分歧、修订和候选约定展开,并保留来源供核对。接受未决事项后,会创建一项暂停任务,方便先检查再开始;候选约定经确认后才进入长期记忆。建议生成失败可以单独重试,已经保存的图谱和回顾进度仍然保留。
|
||||
Obsidian 接入默认关闭,启用后只在配置范围内读写。当前支持直连模型及 GoodBuddy 管理的本机 OpenCode、Continue 聊天;读出的笔记会随当前会话交给所选模型处理。
|
||||
|
||||
心跳完成回顾后,还可以提示达到设定天数仍没有新进展的故事,或指出某条已有经验可能适用于当前故事。建议可以直接打开图谱;接受暂无进展建议会建立暂停任务,接受经验建议只标记已参考,不会据此写入经验应用记录。暂无进展默认按 14 天筛选;仅时间流逝到期、但本次回顾没有变化时,不会单独触发提醒。
|
||||
## 其他改进
|
||||
|
||||
计划需要应用保持运行,并按所选全局或项目范围处理。心跳不会在每条聊天消息后实时介入;有候选建议时会额外调用模型整理措辞,回顾本身也可能分批调用模型。
|
||||
### 输入区和 Runtime 控制
|
||||
|
||||
### 在任务中心跟踪当前会话或任务
|
||||
上下文来源、专家角色、模型和 Runtime 控制收进输入区工具栏。故事图谱与知识库使用同一个上下文入口,OpenCode 和 Continue 的运行配置放入独立弹窗。窄窗口下仍能按可用空间排列,弹窗支持自动聚焦、外部点击关闭和 Escape 返回焦点。
|
||||
|
||||
右侧任务中心的项目概览下方新增监督反馈。选中任务时查看该任务的匹配结果,否则跟随当前会话;也可以固定目标,切换聊天后继续查看同一个事项。没有匹配回顾时会明确显示空状态。
|
||||
### 笔记、文件和图表
|
||||
|
||||
监督反馈中的“继续讨论”可以直接使用,无需先打开来源。侧栏和图谱来源中的入口都会展示完整、可编辑的发送正文,包含相关回顾结论和来源内容;可以删改引用、补充问题,再确认单独显示的目标会话。发送以编辑后的正文为准,已删除的上下文不会被重新附加;发送失败保留草稿。在图谱来源中发送后会打开目标会话,需要先看原讨论时,也可单独点击“打开会话”。
|
||||
魔法笔记支持插入和编辑表格,表格与其他富文本一起保存;笔记支持标签筛选、标签管理和列表与正文双栏浏览。聊天回答可以保存到笔记并返回原会话,工作区支持批量导入本地文件到本地或远程项目,Mermaid 图表可以导出完整 PNG。
|
||||
|
||||
带入的结论按所选来源关联的事件、实体与关系整理,方便围绕当前事项继续追问。监督界面已移除知识实体写回控件。
|
||||
### Runtime、会话和日常操作
|
||||
|
||||
后续讨论的最终内容最多 12,000 字符。固定任务没有关联会话时,不能直接发送继续讨论;后续对话沿用目标会话的 Runtime、模型与能力配置。
|
||||
项目和会话集中到同一个菜单,Runtime 与模型可以分别按会话、项目和全局配置解析,也可以在当前会话临时切换。聊天历史、侧栏和长消息减少重复渲染,阅读位置、展开状态和流式回复更稳定;桌面通知、磨砂外观和任务用量可以分别设置。
|
||||
|
||||
### 聊天正文存入魔法笔记,并保留来源
|
||||
### 请求、存储和故障处理
|
||||
|
||||
遇到值得保留的回答,可以点击助手消息旁的“加入笔记”,预览并编辑文字,再追加到已有笔记,或新建笔记保存。需要保留整段讨论时,会话菜单中的“保存当前对话到笔记”会读取完整历史,收录用户与助手的正文,不局限于屏幕上已经显示的消息。
|
||||
SQLite 存储、后台读取、请求取消和并发处理路径已经合并调整,减少主进程等待。直连模型支持手动压缩上下文,工具执行失败后可以继续对话;长请求结束、取消或失败时会正确回收工具授权。监督回顾支持暂停、继续、取消和失败后重试,远程 Runtime 启动和本地 OpenCode 连接也补充了取消与恢复处理。
|
||||
|
||||
保存后的记录带有“来自会话”入口。单条回答可以返回对应消息;原会话或消息被删除后,已保存的笔记内容仍保留。每次采集新增一条记录,后续对话修改不会自动同步到笔记。
|
||||
## 升级提示
|
||||
|
||||
工作栏“+ → 笔记”可以打开紧凑面板,在聊天旁搜索笔记标题与正文、浏览摘要、追加文字。切换项目或会话时草稿保留,关闭有未保存内容的面板前会提示;草稿仅在本次应用运行期间保留。画布、待办和完整编辑继续在魔法笔记工作区中使用。
|
||||
升级前请关闭应用,并备份完整用户数据、设置、数据库、WAL 文件和相邻的 `notes/` 目录。当前数据库格式为 62,0.15.13 之前的客户端不能直接打开新版数据库,回退需要使用升级前的备份。首次转换较大的数据库时需要等待;高频本地保存调整了磁盘同步策略,断电时可能丢失最近尚未同步的写入。
|
||||
|
||||
快速笔记入口集中在工作栏和聊天采集操作中,移除了侧栏与顶栏的重复快捷按钮。面板精简重复标签,采集范围说明按需展开,标题或正文超限时在对应输入位置提示。保存和来源操作统一靠右排列,窄面板中的保存按钮也保持紧凑。
|
||||
|
||||
采集保存文本与 Markdown,不复制推理过程、工具日志或附件图片。生成中的回答暂不可采集,单条记录上限为 500 KiB,超限会提示缩减。采集本身无需调用模型;保存后自动评论仍按已有设置执行。笔记继续属于全局,来源项目只用于帮助回看。
|
||||
|
||||
> **【待补图 06|必补|聊天采集与来源返回】** 用公开演示对话预览并保存一条回答,展示笔记记录中的来源入口,再验证能返回原消息。计划文件:`assets/06-chat-note-source.png`。
|
||||
>
|
||||
> 建议图注:把有用的回答存入笔记,需要上下文时返回原会话。
|
||||
|
||||
### 笔记支持标签筛选,列表与正文并排浏览
|
||||
|
||||
给笔记加上“项目资料”“待整理”等标签后,可以按标签查找,也可以同时选择多个标签,筛出同时带有这些标签的笔记。每篇最多添加 10 个标签;标签支持改名、同名合并和删除,删除标签会保留笔记正文。
|
||||
|
||||
宽窗口打开笔记时,左侧保留列表,右侧阅读和编辑正文,两栏宽度可以拖动调整。切换笔记时保留当前工作区内的筛选与阅读位置,有未保存内容时会提示。窄窗口可从正文工具栏打开笔记列表抽屉,选完笔记后继续阅读;记录索引默认收起,“新建笔记”固定在页头。
|
||||
|
||||
正文使用当前栏的可用宽度,宽窗口下阅读画布和长内容有更多空间。新建文字或画布记录都可以取消,有草稿时先确认是否放弃;取消编辑、切换记录类型或返回聊天后,焦点回到对应的按钮或输入位置。
|
||||
|
||||
标签也可由支持魔法笔记工具的助手按你的要求添加或修改。快速采集面板继续用于保存聊天内容,标签管理在完整笔记工作区中进行。
|
||||
|
||||
### 在聊天中查找和整理 Obsidian 笔记
|
||||
|
||||
已有 Obsidian 笔记可以直接接入 GoodBuddy。在“设置 → 能力与工具 → MCP”的 Obsidian 卡片中,选择本机已注册的仓库,或限定一个文件夹,测试连接并保存,然后启用并分配给所用 Runtime。无需启动 Obsidian,也无需额外安装 Obsidian 插件或配置 REST 服务。
|
||||
|
||||
接入后,可以请助手查找笔记、读取内容,或将讨论结果写回仓库;也支持追加、局部修改、目录浏览,以及标签和属性操作。笔记仍保存在原仓库,不会自动复制成 GoodBuddy 知识库。
|
||||
|
||||
目前支持直连模型及 GoodBuddy 管理的本机 OpenCode、Continue 聊天,默认关闭;DeepSeek Harness 和远程执行暂不支持。启用并分配后,可按请求读写已配置范围内的笔记;多仓库调用需要明确目标仓库。接入本机文件不等于离线推理,读出的内容会随当前会话交给所选模型处理。
|
||||
|
||||
> **【待补图 07|建议|Obsidian 接入与读写】** 已有候选 `obsidian.png` 展示仓库范围和 Runtime 分配,尚需确认构建来源;正文主图优先补公开笔记的搜索、写入与回读结果。计划文件:`assets/07-obsidian-notes.png`。
|
||||
>
|
||||
> 建议图注:按仓库范围接入本机 Obsidian,在聊天中查找笔记并保存整理结果。
|
||||
|
||||
### 从输入区打开本地原生客户端
|
||||
|
||||
本地项目选择 Continue 或 OpenCode 后,输入框下方可以打开工作栏内的原生交互终端;选择 DS 后,可以在系统默认浏览器中打开官方 DeepSeek Harness Web 客户端。启动时沿用当前项目目录与所选模型,适合希望使用 Runtime 自身交互界面的用户。
|
||||
|
||||
原生客户端使用独立会话,不会自动接续或同步 GoodBuddy 中的聊天历史。DS 网页关闭后,本地服务继续运行,可以回到 GoodBuddy 重新打开或停止;退出应用会清理所属服务。
|
||||
|
||||
当前入口支持本地项目,远程原生客户端尚未接通。Continue 与 DS Web 需要可用的标准 Node 环境,首次准备环境可能需要下载。Continue、OpenCode 原生终端可使用已启用并分配的内置工具及自定义 MCP;故事图谱工具保持只读。
|
||||
|
||||
> **【待补图 08|建议|原生终端与 DS Web】** 已有 `continue.png`、`opencode.png`、`deepseekweb.png` 候选;发布前确认构建来源,并处理本机路径、浏览器书签及与演示无关的会话。Continue 图含模型能力警告,建议换用合适模型重拍;不能据启动画面宣称完整对话验收通过。计划文件:`assets/08-native-clients.png`。
|
||||
>
|
||||
> 建议图注:在工作栏打开原生终端,或在系统浏览器中使用 DS Web 的独立会话。
|
||||
|
||||
### Runtime 与模型可以分别跟随项目或临时切换
|
||||
|
||||
项目可以保留常用的执行方式与模型,某次会话需要换模型时,在输入区的两栏选择器中临时选择即可。菜单标明当前选择来自会话、项目还是全局,也可以恢复项目默认,减少每次新建会话重复配置。
|
||||
|
||||
执行方式与模型分别按“会话 → 项目 → 全局”解析,并检查模型是否适用于当前 Runtime。连接被删除或不兼容时按规则回退;缺少 API Key 时保留选择并提示配置,不会悄悄换用另一个模型。排队消息在实际发送时重新读取有效选择,定时任务则保留创建时确定的配置。
|
||||
|
||||
托管 SSH 项目支持 OpenCode、Continue;外部 OpenCode Server 使用服务端自身配置。
|
||||
|
||||
### 复制模型连接,保留已有配置
|
||||
|
||||
在“设置 → 模型连接”中打开一条 LLM 连接,点击“复制”,即可在副本中修改模型名称、地址或其他选项,再保存为新连接。协议、请求定制和认证配置随副本带入,已保存的 API Key 无需重新输入。
|
||||
|
||||
复制后仍需保存才会生效;当前最多保留 20 条连接。保存地址有误时,界面会指出对应连接并定位到地址字段,便于处理多条连接中的配置问题。
|
||||
|
||||
### Mermaid 图表可导出完整 PNG
|
||||
|
||||
聊天中的流程图或关系图可以打开大图窗口,再导出完整 PNG。导出内容不受当前缩放比例和拖动位置影响,适合插入报告、文档或演示材料。
|
||||
|
||||
图片沿用当前主题背景,最长边上限为 4096 像素。此功能用于 Mermaid 图表,故事线图谱的图片导出不在本轮范围内。
|
||||
|
||||
> **【待补图 03|建议|图表导出】** 在正常聊天中打开一张公开示例 Mermaid 图,执行 PNG 导出,并核对图片包含整张图。优先展示实际导出结果,图注注明它是导出图片。计划文件:`assets/03-mermaid-export.png`。
|
||||
>
|
||||
> 建议图注:从 Mermaid 大图窗口导出完整 PNG,保留图中的边缘节点。
|
||||
|
||||
### 让助手把会话图片保存到指定文件
|
||||
|
||||
可以让助手把当前会话中已经上传或生成的图片保存到指定绝对路径,缺少的父目录会自动创建。远程项目保存到对应 Host,方便后续在项目文件中使用;已有同名文件默认保留,明确允许覆盖后才替换。
|
||||
|
||||
保存支持 PNG、JPEG 和原格式 WebP,可按扩展名转换为 PNG 或 JPEG,不提供转为 WebP。工具使用当前会话已有图片,不负责下载任意图片网址。成果页已移除,历史图片和成果记录仍保留;保存图片无需先进入成果页,既有成果也不会自动复制到项目目录。远程保存自 Agent 0.15.2 起提供,本轮建议使用上述最新配套版本。
|
||||
|
||||
### 通过 Telegram 私聊使用通道项目
|
||||
|
||||
配置自己创建的 Telegram Bot、数字用户 ID 白名单和通道项目后,可以在 Telegram 私聊中发送文字问题或任务。GoodBuddy 沿用该项目的 Runtime、模型和工作目录处理请求,返回最终文字回复,并在桌面保留会话记录。
|
||||
|
||||
首次配置可以先留空发送者 ID,启用并保存通道。连接后私聊 Bot,发送 `/start` 和 `/whoami` 获取自己的数字用户 ID,填回白名单并再次保存后即可发送 AI 请求;仅测试连接不会开始接收消息。
|
||||
|
||||
当前支持单个 Bot 的普通私聊,桌面应用需要保持运行并能连接 Telegram。连接使用系统代理,支持断线恢复和停用取消;排除连接故障后,也可原样保存配置重新连接。目前不处理群聊、媒体输入或附件发送,图片等附件需回到桌面查看。
|
||||
|
||||
> **【待核对 C02|Telegram 实际联调】** 功能已提交,真实 Bot 与模型的完整收发验收仍待完成。对外发布前用专用演示 Bot 核对文字请求、回复、断线恢复与停用行为,不将本轮静态核查写成实际联调通过。
|
||||
|
||||
### 将本机文件导入本地或远程工作区
|
||||
|
||||
工作区文件面板的“新建文件”旁新增导入入口。选定目标目录后,可以批量选择本机文件,复制到本地项目或上传到远程 Host,随后在文件列表中查看。远程导入需要配套 Agent 0.15.4 或包含该功能的更新版本。
|
||||
|
||||
导入遇到同名文件时保留原文件,单个文件失败后继续处理其余文件。列表可以按名称、修改时间或可用的创建时间排序;排序针对当前目录已读取的条目,每目录最多 500 条。
|
||||
|
||||
### 桌面通知可以单独关闭
|
||||
|
||||
“设置 → 平台功能 → 通用设置”新增“桌面通知”开关,默认开启。关闭后,应用处于后台时的任务完成、失败或取消等系统提醒停止弹出,应用内通知仍然保留。设置保存后立即生效,重启后也会记住选择。
|
||||
|
||||
### 设备共享目录:登记设备,发布能力与知识说明
|
||||
|
||||
在应用中心启用“设备共享”,连接另行运行的 ShareServer 后,可以登记本机、浏览设备和发布目录,并发布或撤销能力、知识的元数据,供使用同一服务的人查看有哪些资源。
|
||||
|
||||
此功能默认关闭,当前提供目录管理。发布“知识”记录不会上传或同步正文,搜索、读取和下载标志仅表示声明;尚不提供远程执行或知识正文访问。设备登记也不表示设备在线,当前服务没有身份认证,应在可信环境中使用。
|
||||
|
||||
## 功能改进
|
||||
|
||||
### 请求直接使用当前 Runtime 与能力配置
|
||||
|
||||
聊天、任务和通道请求不再需要切换 Ask/Execute,也移除了原有的通用工具审批策略。选好 Runtime、模型与已启用能力后,即可提出请求;只需解释时可以直接说明,文件和工具操作使用执行账户的实际权限。
|
||||
|
||||
能力开关、工具分配、资源范围和取消操作继续生效,各 Runtime 的支持范围也仍有区别。监督回顾、摘要等专用文本分析任务保持无工具。已有会话内容保留,旧工作模式设置不再决定后续请求的行为。
|
||||
|
||||
### 项目与会话集中到一个菜单
|
||||
|
||||
左上角的双行按钮统一显示项目与当前会话,点击后浏览本地、远程和通道项目。可以分类搜索,悬停预览项目会话,也可以直接跨项目进入正在处理的工作;切回项目时恢复上次打开的会话。
|
||||
|
||||
菜单同时提供最近 10 个会话,以及待处理和运行中的工作;按已完成状态筛选时可以继续查找历史会话。项目设置、归档恢复和离开未保存草稿前的提示仍可使用。新建本地项目必须选择目录,远程目录选择器支持进入指向目录的符号链接。
|
||||
|
||||
### 会话搜索按需展开,快捷键提示集中在输入框
|
||||
|
||||
侧栏的搜索入口与“新建对话”放在同一行,点击后展开并自动聚焦,可查找会话标题或消息正文。切换会话保留关键词;点击关闭按钮,或在搜索控件行内按 Escape,可清空并收起搜索。展开和收起时,会话列表位置保持不变。
|
||||
|
||||
输入框的空白提示集中显示发送、换行、粘贴、新建对话及可用的快捷唤起按键,并按系统显示 Ctrl 或 Command。修改快捷键设置后提示随之更新;快捷唤起已关闭或注册失败时,不再显示不可用的按键。
|
||||
|
||||
侧栏底部的“应用”和“设置”保留文字并分开显示,应用菜单箭头与向上展开方向一致。会话列表独立滚动,短窗口中的入口仍可到达,滚动条和分隔线样式也作了统一。
|
||||
|
||||
### 知识库文档与来源集中浏览
|
||||
|
||||
知识库将内容来源与文档索引合并为统一列表。单文档来源合成一行,目录继续按组展示,导入失败、尚未生成文档的来源也保留恢复入口。搜索可以同时查找来源与文档,网址来源保留原网址,便于辨认同名资料。
|
||||
|
||||
列表突出查看入口,打开原文、分块管理、重新解析等操作收进更多菜单。解析结果页也整理了返回导航和图片操作,减少长名称、多个按钮挤在一行的情况。
|
||||
|
||||
任务历史改为按批次展示,可以展开处理步骤,查看进行中、失败与历史记录。筛选与计数跟随当前文档或来源,便于检查一次导入的处理情况。
|
||||
|
||||
### 图谱列表与画布各自浏览
|
||||
|
||||
故事线图谱侧栏可以按故事、经验、事件、实体和关系查看,列表与详情各自滚动。切换页签保留已有选中对象,在画布选中对象时,侧栏会切到对应类别。平铺与时间螺旋都使用中间画布的可用空间,视角等控件收进紧凑操作区,减少窗口较矮时内容被挤出屏幕的问题。
|
||||
|
||||
工作回顾与图谱共用选中的历史结果和范围说明,从总结切到图谱时继续查看同一份回顾。新回顾的输入区域可以收起,历史选择器和视角工具排列更紧凑;详情布局也适配较窄窗口。
|
||||
|
||||
### 手动回顾与智能心跳共用增量进度
|
||||
|
||||
原有的每日、每周计划现在集中到“自动监督”。新建或编辑计划时,可以选择全局或多个项目,设置时间、时区、回顾窗口和历史保留期限;暂停的计划在编辑保存后仍保持暂停。
|
||||
|
||||
手动“回顾”和心跳计划现在共用增量处理方式,默认只分析所选范围内尚待处理的变化。项目与全局共用来源处理进度:同一版本的来源已成功发布后,换一个范围回顾可以复用,减少重复提取。没有新来源时仍可补做积压的故事与经验整理,没有待处理工作才跳过模型调用。需要重看完整区间时,可从“更多 → 重新整理”确认发起;重新整理不会推进或重置增量进度。
|
||||
|
||||
启用监督者不会自动创建计划。定时工作需要桌面应用保持运行,关闭监督者会停止发起新的回顾,并保留计划和历史;从导航取消常驻只改变入口显示。尚无活动时,自动监督页会隐藏空的统计、建议和历史区域,先展示计划入口。
|
||||
|
||||
监督者设置中可以单独选择一条文本模型连接,用于回顾、故事和经验整理,以及建议生成;未指定时跟随默认文本模型。选定连接失效或缺少 API Key 时明确提示,不会自动换用其他模型。
|
||||
|
||||
监督分析会将入选内容交给配置的模型服务,可能产生费用。它以只读、无工具的方式分析记录,长时间范围可能分成多批;心跳不再额外生成一份独立报告。并发数和超时设置不等于总费用上限。
|
||||
|
||||
### 长回顾可以看进度、保留批次并继续处理
|
||||
|
||||
监督者的“活动记录”集中展示手动和自动执行情况。展开记录,可以查看当前阶段、已保存批次、剩余来源和失败原因,再按项目、会话、批次检查已保留事实与来源片段。
|
||||
|
||||
回顾会分页读取选定范围内的来源,将长内容拆成批次处理。成功批次保存后,后续阶段失败不会把这些内容一并丢弃;暂停或失败后可以继续。0.13.16 还取消了按固定累计时长自动暂停的行为,长回顾可以持续处理至完成,无需隔一段时间手动续跑。
|
||||
|
||||
需要调整处理方式时,可在“监督者 → 设置”中配置模型并发、单次模型超时、每次读取的来源条数、每批消息和文本量,以及单次响应容量。新建回顾会保存分批配置;来源已被修改或删除时,需要重新发起回顾。
|
||||
|
||||
> **【待补图 04|必补|回顾进度与来源批次】** 展开真实运行的活动记录,展示阶段、已保存批次和来源正文。若展示暂停后继续,须通过真实操作完成,不注入运行状态。图 01 的演示可复用同一份记录。计划文件:`assets/04-review-progress.png`。
|
||||
>
|
||||
> 建议图注:从活动记录查看处理阶段与已保存批次,展开核对来源。
|
||||
|
||||
> **【待核对 C01|暂停与取消配图】** `v0.15.0` 已包含 `5116613` 的暂停控件修复与取消入口,见末尾“回顾运行期间的控制”。截图须对应目标版本,展示实际暂停、继续和取消操作。
|
||||
|
||||
分页和批次记录便于核查处理范围,模型给出的总结、实体和关系仍需人工判断;来源处理完成不代表每个事实都被模型准确理解。
|
||||
|
||||
### 待办、任务与用量更便于浏览
|
||||
|
||||
魔法笔记的待办页可以拖动列表与详情之间的分隔条,调整后的宽度会保存。来源笔记标题支持独立折叠,收起一个分组不会清空正在查看的任务详情。分组折叠状态不跨重启保存,窄窗口下不提供横向拖宽。
|
||||
|
||||
笔记详情的记录索引与 AI 区开关集中在正文顶部的固定工具栏,图标带有悬停提示。任务中心展开列表后,可以直接选择“全部、进行中、暂停、已结束”;默认显示进行中的任务,历史任务仍可通过切换状态查看。
|
||||
|
||||
> **【待补图 05|建议|待办布局】** 用虚构笔记创建不同来源的待办,拖动分隔条并折叠一个来源组,保持另一项详情可见。计划文件:`assets/05-note-todos.png`。
|
||||
>
|
||||
> 建议图注:调整待办列表宽度,按来源笔记收起暂时不看的任务。
|
||||
|
||||
运行记录中的 Token 用量先显示项目或会话总计,展开后再看 Runtime 和模型明细,按模型分组的视图继续保留。“系统任务”页签单列监督回顾、笔记、知识库等后台用量,并补齐回顾、图片重新生成、知识图谱提取和远程嵌入等调用的用量记录;过去未记录的用量无法补算。页面打开时加载,可见期间每 15 秒刷新,也可点击页头手动刷新。这里汇总的是应用记录,并非服务商计费账单。
|
||||
|
||||
会话和任务耗时按实际运行状态累计,包含模型与工具执行,等待用户回答时暂停,运行期间持续更新。并行请求的时间会相加,因此它不等于项目经历的日历时长。旧历史不补算,缺少计时数据且累计为零时显示“不可用”,与真实的零时长区分。
|
||||
|
||||
### 长对话输入、切换与流式回复减少重复处理
|
||||
|
||||
任务耗时按运行状态累计,避免反复扫描历史消息和事件;隐藏的任务面板保留操作状态并减少不必要的更新。较长任务列表也减少了屏幕外卡片的绘制工作。
|
||||
|
||||
运行记录按需分页加载,筛选时读取对应范围,保存时只传递变化的记录;页面计数与汇总仍覆盖已保存历史。离开页面后释放多余已加载页,重新查看时可以继续加载,保留原有内容与顺序。实际响应改善取决于历史规模和设备,不以测试中的数据库指标代替整机提速幅度。
|
||||
|
||||
聊天输入和流式回复减少了无关活动面板的刷新;置顶或取消置顶会话时,也不再附带扫描附件引用。笔记启动检查复用已读取的正文信息,减少重复读取,原有内容恢复与清理流程保留。
|
||||
|
||||
长回答逐步生成时,已完成的 Markdown 区块可以复用,减少每次新增文字都重新处理整篇内容。切回近期看过的会话时也会复用缓存结果;会话侧栏与运行记录减少无关刷新,降低回复期间继续输入和切换会话的开销。
|
||||
|
||||
较长会话和大量会话的侧栏主要渲染屏幕附近的内容,历史记录仍然保留,可以继续加载和定位。切回缓存会话时恢复阅读位置;Markdown 解析结果也可复用,包含公式的长回复减少重复解析。
|
||||
|
||||
知识检索、故事图谱查询、会话搜索和会话列表等常用读取移到后台线程,减少查询期间对桌面主线程的占用。远程运行的连续事件按批保存,减少逐条写入的开销。改善幅度取决于设备、数据规模和内容复杂度,不作统一提速比例承诺。
|
||||
|
||||
文档解析、分块与摘要哈希计算也转到后台线程;笔记网格、工作区文件树和知识库文档表主要绘制可见区域,终端连续输出合并刷新。启动时按需加载部分组件,并在后台准备工具环境,需要这些工具时再等待准备完成。
|
||||
|
||||
### 设置页减少来回查找
|
||||
|
||||
长设置表单的分类标题与相关页签固定在顶部,请求 Header、Body 等低频选项收进高级设置。默认工作目录移到“平台功能 → 通用设置”,Skills 和 MCP 页面移除重复标题,保留导入与添加操作。
|
||||
|
||||
设置导航按用途分组,开关统一放在右侧。通用设置集中放置会话与通知、工作目录等选项;本地语音和嵌入模型页面提供“模型下载源”快捷入口,可打开统一弹窗,在 ModelScope 与 Hugging Face 之间切换。选择影响后续托管模型下载,已安装模型、ZIP 导入、Ollama 和应用更新不受影响。
|
||||
|
||||
外观设置新增透明磨砂效果,当前默认开启;标题栏和相关卡片使用半透明背景,可以看到其后的应用内容。可以关闭恢复实色显示,已有的外观选择继续保留。
|
||||
|
||||
内置浏览器 MCP 也改为默认启用,仍可在设置中关闭;之前明确保存的关闭状态会保留,不会修改系统默认浏览器。请助手通过配置工具应用本次请求的变更计划时,取消了额外一次原生确认;仍需先生成有效计划,再应用对应变更。
|
||||
|
||||
HTTP OCR 高级设置可以直接选择是否保留页眉、页脚、页码等内容,并恢复默认过滤方式。版面与生成参数的分组和间距也重新整理;具体选项效果仍取决于所连接的 OCR 服务。
|
||||
|
||||
成功和提示通知采用更紧凑的卡片,两秒后自动消失;错误提示继续保留,直至手动关闭。删除会话的确认框也去掉了重复警告,永久删除的含义不变。
|
||||
|
||||
## 问题修复
|
||||
|
||||
### 直连文本模型恢复手动压缩上下文
|
||||
|
||||
本地直连文本会话可以从输入区点击“压缩上下文”,即使关闭自动压缩或尚未达到触发阈值,也能手动执行。压缩使用配置的摘要连接,摘要随会话保存并用于后续对话,同时保留最近轮次和完整聊天历史;关闭自动压缩后,已有有效摘要仍可使用。
|
||||
|
||||
会话运行或正在压缩时需等待完成;没有可压缩的较早历史时会提示。这项修复适用于本地直连文本会话,不包含图像生成连接、远程通道会话或 SSH 项目。
|
||||
|
||||
### 长请求持续保留已授权的工具访问
|
||||
|
||||
修复桌面 Agent 请求持续超过十分钟后,工具授权提前失效的问题。请求仍在执行时,已启用并分配的知识库、笔记、浏览器等工具可继续使用;完成、取消或失败清理时仍会撤销该次授权。工具自身的调用超时、读写权限和开关设置保持不变。
|
||||
|
||||
### 取消与失败提示更清楚
|
||||
|
||||
取消和失败信息集中显示在消息状态中,减少与回答正文或工具详情的重复。主动取消使用中性提示,真实失败继续显示错误状态,已经生成的回答保留。
|
||||
|
||||
最后一条失败或取消消息有可恢复的原问题文字时,可以点击“重新编辑”将文字回填输入框,修改后自行发送;不会自动重发,也不承诺恢复附件。
|
||||
|
||||
取消或失败的轮次在后续上下文中明确标为中断,并保留已有回答,避免上一条未完成请求被并入新问题继续执行。OpenCode 复用会话在取消后的下一轮也会收到中断说明。
|
||||
|
||||
### 远程回答完成后,可以继续对话
|
||||
|
||||
远程请求现在等待模型交付确认后再结束。回答已经完成时,随后回收空闲 Runtime 失败不会再把结果改成“未知”。确实遇到结果未知的请求,用户显式发送新消息后会建立新的运行绑定并带入已有历史;自动恢复仍只重新连接原请求,不会擅自重复执行结果未知的任务。
|
||||
|
||||
最后一个活动任务结束后,远程空闲 Runtime 会被释放,下一轮对话再恢复历史。仍有任务或待答问题的共享进程会保留;更新后被替换的旧 Agent 会等待已有工作结束再退出,当前 Agent 保持常驻。
|
||||
|
||||
Host 更新还会清理已确认停用的旧 Agent 程序和归属明确的孤儿进程,保留会话历史、活动安装和状态不明的安装。这些完成状态与进程清理修复自 Agent 0.13.4 起提供;本轮基础配套为 Agent 0.15.4,仅升级桌面不会直接替换 Host 上已经运行的旧进程。
|
||||
|
||||
最新修复使远程 Runtime 在等待启动时仍可取消,并避免阻塞其他会话;本地 OpenCode 提交失败后也能及时结束等待并显示原因。远程部分需要包含 `bbf24c28` 改动的 Agent,不属于已核查的 Agent 0.15.4 标签。
|
||||
|
||||
### 通道交付与统计结果
|
||||
|
||||
- 修复生成图片成功后,通道因结果格式校验而误报失败的问题。Telegram 仍只发送文字提示,附件需到桌面查看。
|
||||
- 钉钉返回业务错误时不再被当作送达成功,可以进入已有重试流程。
|
||||
- 修正 OpenCode 缓存命中率偏高、完全命中时显示为空的问题;已有用量重新查询后使用完整输入量计算,不表示实际缓存命中率提高。
|
||||
- 活动汇总覆盖已加载的会话集合,不再只统计前 5,000 个会话。
|
||||
- PPTX 解析警告显示实际使用的 OCR 服务及失败的幻灯片、图片位置,便于排查;这项修复不改变外部 OCR 服务本身的识别结果。
|
||||
|
||||
### 启动、搜索与回顾结果
|
||||
|
||||
- 修复正常聊天或保存笔记后,重启反复显示历史迁移的问题。普通数据库结构升级也不再因正常空闲页额外整理整库;确需转换的旧数据仍会处理,启动页区分实际操作。
|
||||
- 修复本机直连模型文件搜索参数受限和大结果截断的问题。模型可使用 ripgrep 原生参数,保留原生结果与退出码,并分页续读长输出;工具使用当前执行账户权限。这项变更不属于远程 Agent 或其他 Runtime 的新增工具。
|
||||
- 修复早期监督回顾只取部分会话、消息和正文的问题,改为对入选来源分页、分批处理。事实与来源独立保存,只有必要阶段完成并成功保存结果后才标记完成。
|
||||
- 修复监督回顾中的旧实体引用、摘要合并校验及长摘要报错。遇到无效 JSON、结构或引用时,会尝试缩小当前批次后重试,剩余正文留待后续处理;成功保存的批次保留。
|
||||
- 监督模型遇到可识别的临时网络中断、限流或部分服务端错误时,清理失败请求后自动再试一次,等待期间可以取消。认证、配置、费用额度、超时、取消及容量错误不会触发这次重试;失败尝试已报告的用量仍会记录,重试可能增加模型费用。
|
||||
- 故事或经验整理失败时,已保存的回顾结果继续可用,可以单独重试整理。故事归类减少把各功能的提交与测试统归到“发布”的情况;经验应用记录也增加时间顺序与共同实体核对,归纳内容仍需结合来源判断。
|
||||
- 时间螺旋退出时释放图形资源;3D 初始化失败或图形上下文丢失时,在视图内提示并提供重试,也可切回平铺,避免整个监督页面无法使用。
|
||||
- 修复历史回顾入口跳到其他结果、切换后旧响应覆盖当前选择的问题。侧栏、活动记录和图谱按对应结果定位,人工确认与修订的内容保留。
|
||||
- 修复关闭跨项目关联后候选范围未正确限定、暂停发生在最终查询期间仍继续发布的问题。故事整理期间已移除的故事不会再收到新关联,刷新后的控件也不会因旧请求失效而一直禁用。
|
||||
|
||||
### 日常界面操作
|
||||
|
||||
- 多个会话同时回复时,侧栏列表不再随流式文本或工具进度反复换位。会话按最新消息的创建时间排序,每五秒检查一次;悬停、滚动后的短暂停留、菜单展开或键盘焦点位于列表时暂缓重排,方便点选。置顶会话继续优先显示。
|
||||
- 输入框的附件、语音与选项按钮统一尺寸、间距和悬停反馈,录音中和选项展开时仍保留对应状态提示。
|
||||
- 草稿和排队消息中的图片可以直接打开预览,发送前更容易核对附件。文本输入框移除重复问候,只保留快捷键提示。
|
||||
- 展开 Runtime 检查清单不再挤动聊天内容;文件列表和差异页的操作栏在滚动时保持可见。
|
||||
- 笔记保存后退出编辑;从快速笔记跳到指定记录时,等待正文显示后再定位,减少落在错误位置的情况。
|
||||
- 切回已查看的会话时,立即显示缓存的累计回复时长,再后台刷新;缓存限本次应用运行期间。
|
||||
- 长对话滚出屏幕后再返回,推理、工具输入输出、引用和子代理详情保留展开选择;该状态限历史面板存续期间,不跨应用重启保存。跨屏选择、复制与滚动定位也作了修复,跳到底部恢复平滑滚动。
|
||||
- Mermaid 大图放大后,顶部和左侧等边缘仍可通过滚动、拖动到达,窄窗口中的控件可以换行。
|
||||
- Git 分支选择器悬浮在工作区上方,不再把文件列表和提交历史向下挤动;项目选择菜单可以浮出侧栏,长名称与路径有更多显示空间。
|
||||
- 文件差异页将查看文件、刷新和复制路径集中到路径旁,窄面板中可以换行;只有未暂存改动时,省去重复的分区标题。浏览器“前往”按钮改为紧凑图标,保留悬停提示。
|
||||
- 聊天与浏览器、终端工作栏之间的分隔条恢复完整拖动区域;笔记缩略图与内容边缘对齐,键盘焦点轮廓保持可见。
|
||||
- 纯文本排队消息不再显示无效的“查看附件”按钮;浏览器页签关闭失败后重试成功,会清除残留错误。
|
||||
- 监督反馈显示会话或任务名称,固定与刷新按钮在窄侧栏中排列更紧凑。工作回顾和活动页使用可用宽度,运行阶段与已保存批次更醒目。
|
||||
|
||||
### 运行配置与界面修复
|
||||
|
||||
- OpenCode 原生终端减少重复准备步骤,缩短打开时的等待;临时 MCP 工具名缩短并在同一会话中尽量复用,减少工具名称过长导致的调用失败。
|
||||
- 本地 OpenCode 初始化工具连接时,遇到明确的暂时性传输故障,清理成功后最多重试一次,不重放问题或工具调用。迟到的旧连接结果不再替换当前连接;自定义 MCP 中一台服务加载工具失败时,其他正常服务仍可使用。
|
||||
- 修复 Continue 禁用自动更新后仍显示“Checking for updates”的问题,避免空闲终端显示误导性的更新状态。
|
||||
- 补齐 DS Web、远程 Runtime 和模型桥辅助进程启动时的遥测关闭配置;模型、MCP 和用户主动发起的联网请求保持可用。远程遥测配置与 Continue 更新检查修复由 Agent 0.15.0 配套提供,对更新 Host 后新启动的 Runtime 生效,已有进程不会热更新。
|
||||
- 修复知识库不同来源的操作相互阻塞,以及多文档来源中任务范围、筛选计数不一致的问题。
|
||||
- 修复深浅主题中危险操作按钮、禁用状态、控件边框和键盘焦点显示不一致的问题,包括放弃笔记草稿的确认框。
|
||||
- MCP 编辑器和图片预览可以在父弹窗上正常操作;项目弹窗的 Escape 只处理当前弹窗,帮助层覆盖浏览器时不再被原生页面遮挡。
|
||||
- HTML 全屏预览恢复键盘进入与 Escape 返回,继续保持原有沙箱限制。
|
||||
- 窄工作栏的添加按钮不再被滚动箭头遮挡,模型连接协议文字保持可读;DS 入口移除常驻说明,重新打开和停止服务操作继续保留。
|
||||
- 多页签工作栏的滚动箭头不再遮挡关闭按钮。DS 网页和服务操作名称更明确,停止确认与取消按钮同行显示,减少输入区跳动。
|
||||
|
||||
### 回顾运行期间的控制
|
||||
|
||||
回顾运行期间,范围、时间选择和刷新入口继续可用;修改选择不会改变已经启动的回顾。重复点击发起按钮会提示已有运行,避免同时创建重叠回顾。
|
||||
|
||||
活动记录增加取消正在运行回顾的入口,并修复继续回顾后无法再次暂停的问题。暂停保留进度供继续处理;取消会保留已成功保存的批次,但该次回顾不能再继续。取消清理结束前显示“正在取消”,未完成结果不会发布,之后可以新建回顾。
|
||||
|
||||
手动发起、继续与自动回顾共用执行限制;自动回顾在忙碌时等待,手动操作会提示当前已有运行。同一次回顾内部仍可按设置并行处理批次。
|
||||
> **编辑提示:** 发布前补齐生产截图,替换故事线概念图,并核对下载地址、发布日期和更新日志链接。以上提示属于编辑流程,不影响本文所述功能已完成的状态。
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# 0.13.8 起综合宣传稿核查记录
|
||||
|
||||
## 2026-10-09 宣传版重排
|
||||
|
||||
本轮按宣传重点重写正文,将故事图谱、Telegram 和 Obsidian 接入作为三条主线;底部输入区、Runtime、笔记、性能和稳定性改进合并到“其他改进”。当前版本内所述功能均按已完成能力表述,发布前的待办只保留生产配图、公开下载信息、发布日期和更新日志链接,不再把功能验收材料写成开发状态。
|
||||
|
||||
正文保留必要的使用范围和升级提示,提交依据、实现边界与历史变更继续留在本核查记录中。原正文中的概念图仍只作示意,发布前需要用目标版本的生产截图替换。索引已同步更新,未修改产品代码。
|
||||
|
||||
早期核查中关于 Telegram 真实联调的 C02 标记属于当时的验证记录,不再作为当前正文的功能状态描述;本轮按用户确认,将 Telegram 作为当前版本已完成能力介绍。发布前仍需补齐公开演示素材和发布信息。
|
||||
|
||||
## 2026-10-09 截至 3556df88 的增量核查
|
||||
|
||||
本轮固定范围 `47917094..3556df88`,共 5 个提交;全文范围为 `v0.13.7..3556df88`。`v0.15.13` 与 `agent-v0.15.6` 均指向 `3556df88`。本轮没有新增独立宣传功能,连续提交合并回已有的输入区 UI 与 Runtime 修复说明。
|
||||
|
||||
| 提交 | 入文内容 | 核查依据与边界 |
|
||||
| --- | --- | --- |
|
||||
| `69f64df2`、`4b4531ab` | 补强“底部输入区工具栏更紧凑” | 恢复 Composer 弹层与工具入口的集成,保留打开后聚焦第一个可用控件、外部点击或焦点离开关闭、Escape 返回触发按钮焦点的最终行为;测试夹具调整不单独宣传。 |
|
||||
| `adb58d92`、`3556df88` | 合入 Runtime 网络请求修复 | 本地模型请求可通过 Electron 的 `net.fetch` 发送,监督模型连接也沿用该请求路径并保持既有配置校验;不扩大为所有网络故障已解决或跨平台安装包验收完成。 |
|
||||
| `d7fb4299` | 仅留内部验证依据 | 使用真实 fetch 的监督模型覆盖测试,不写成当前轮次已执行的产品测试或真实模型验收。 |
|
||||
|
||||
本轮只更新宣传正文、核查记录和索引,未修改产品代码。未运行产品测试、构建、真实模型请求或拍摄新截图;生产配图、Telegram 真实联调和公开下载信息继续待核验。`deai-writing` 扫描结果沿用上一轮为阻断项 0、复核项 19;`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
|
||||
|
||||
## 2026-10-08 截至 47917094 的增量核查
|
||||
|
||||
本轮固定范围 `384683e8..47917094`,共 23 个提交;全文范围为 `v0.13.7..47917094`。Desktop/Agent 版本依据仍为 `0.15.13`/`0.15.6`,本轮没有新的版本准备提交。正文按用户场景合并了表格编辑、专家与 Runtime 控制入口;输入区、历史导航和存储测试相关的连续提交没有拆成多个宣传条目。
|
||||
|
||||
| 提交 | 入文内容 | 核查依据与边界 |
|
||||
| --- | --- | --- |
|
||||
| `3d063937` | 新增“在魔法笔记中插入和编辑表格” | Quill 原生 table Delta 经过编辑器、共享契约、IPC、存储和只读渲染链路;支持设置行列、行列增删和整表删除,表格上限 20 行/12 列。空单元格表格可保存并重新打开,空白富文本仍拒绝保存。 |
|
||||
| `13b58627`、`1a59c2d8`、`670b5985` | 新增“在当前会话中使用专家角色和 Runtime 控制”,并合并底部输入区 UI 改进 | 专家和 Runtime 控制收进 Composer 工具栏。选定专家或智能路由得到的角色说明加入当前 Runtime 的受信任提示词,沿用当前会话的模型、工具、流式输出和权限;普通对话不转为额外子任务,团队模式仍走独立团队路径。上下文来源共用一个入口,OpenCode/Continue 的控制项进入独立弹窗;点击外部区域或焦点离开时关闭,Escape 返回触发按钮焦点,运行控制弹窗移除多余分隔线。 |
|
||||
| `e2a5f836`、`47917094`、`4266ff5`、`4a2ea490` | 仅记录历史导航方案的收回和文档整理 | `e2a5f836` 发布了聊天历史导航文档,曾尝试取消向上滚动的 80 条增量限制;`47917094` 恢复批量历史加载。当前仍保留历史窗口和约 60 条 DOM 窗口化,不能宣传为已实现连续滚动或 minimap。用户指南和 README 截图整理不单列为功能。 |
|
||||
| `377b14fc`、`a406f596`、`7f0a73e6`、`2d68d104`、`277da413`、`e1360264`、`59a28209`、`9788b4cd`、`c90ad2af`、`95ece25c`、`c4ab83a7`、`7eb11302`、`5ba127e3`、`2a92f8f8`、`95f44195` | 仅留内部验证依据 | 存储进程取消、排空、崩溃恢复和测试夹具稳定化,不写入宣传正文,不据此扩大存储进程故障恢复或跨平台验收结论。 |
|
||||
|
||||
本轮只更新宣传正文、核查记录和索引,未修改产品代码。未运行产品测试、构建、真实模型请求或拍摄新截图;提交中已有的表格、聊天历史和存储测试记录只用于核对实现边界。生产配图、Telegram 真实联调和公开下载信息继续待核验。deai-writing 扫描结果为阻断项 0、复核项 19、读取错误 0;复核项集中在版本、素材、能力边界和必要否定条件,未机械删除。`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
|
||||
|
||||
## 2026-10-08 截至 384683e8 的增量核查
|
||||
|
||||
本轮固定范围 `e50ca6d1..384683e8`,共 22 个提交;全文范围为 `v0.13.7..384683e8`。版本资料已推进到 Desktop 0.15.13、Agent 0.15.6;HEAD 位于版本准备提交之后,最后一组提交主要是存储故障验收夹具和测试稳定化,不另列为产品功能。正文仍按新增功能、功能改进、问题修复组织,并将连续的架构与性能改动合并为一节。
|
||||
|
||||
| 提交 | 入文内容 | 核查依据与边界 |
|
||||
| --- | --- | --- |
|
||||
| `d80d42b0`、`8473189f`、`904bb691`、`1f85d0c0` | 合并为“存储与请求路径合并,减少主进程等待” | Desktop SQLite 的 Assistant、Knowledge、附件、Runtime 绑定和用量记录改由同一 storage utility process 负责;查询、文件操作和回顾来源读取共用类型化异步调用。同步保留事务、提交确认、取消、关闭排空和故障报告边界。移除无依据的产品侧 admission、子任务和解析 worker 队列上限,不宣传为全量并发或跨平台性能验收;存储进程故障后未确认写入仍需按域核对。 |
|
||||
| `13c29bad`、`c1e4bbf6`、`3e3a0216` | 更新魔法笔记标签节 | 标签管理器新增直接创建标签;保留空标签集合,重复标签错误保留原始原因。正文不扩大为自动打标签或取消每篇最多 10 个标签的限制。 |
|
||||
| `e0607a15`、`2be57492` | 合入回顾结果修复 | 支持短引用和已知历史键映射;删除会话后回顾可继续查看或处理,暂停、继续、重试及项目范围规则仍有效。 |
|
||||
| `56b8b10d` | 新增直连模型工具失败后的继续对话说明 | 工具执行失败不会阻塞后续直连模型消息,分页输出边界、工具权限、取消和超时规则保留。 |
|
||||
| `425accf4`、`4bb7573b`、`edf019aa` | 合入日常界面改进 | 直接进入设置页时预加载目标路由;工作区文件列表元数据和聊天输入区过渡更简洁。均不单列为新能力。 |
|
||||
| `b90ff50f`、`36e671a0`、`fbfafa15` | 更新版本与监督稳定性依据 | 监督任务交付和停顿修复并入既有监督修复范围;版本准备资料为 Desktop 0.15.12/0.15.13、Agent 0.15.5/0.15.6。 |
|
||||
| `42ad8a98`、`4c25e85b`、`7eb51bd5`、`9212fd43`、`384683e8` | 仅留内部验证依据 | 存储故障验收、权限夹具、容量断言和测试竞态稳定化,不写入宣传正文,也不据此宣称完整 C01–C07 或跨平台性能验收完成。 |
|
||||
|
||||
本轮未修改产品代码,仅更新宣传正文、核查记录和索引。未运行产品测试、构建、真实模型请求或拍摄新截图;本轮提交中的测试记录只作为静态核查依据。生产配图、Telegram 真实联调和公开下载信息继续待核验。deai-writing 扫描结果为阻断项 0、复核项 18、读取错误 0;复核项均为版本、素材、能力边界或模型结果限定,未机械删除。`git diff --check -- docs/pr-articles` 通过,仅提示既有 LF/CRLF 行尾策略。
|
||||
|
||||
## 2026-10-05 截至 e50ca6d1 的增量核查
|
||||
|
||||
本轮最终范围 `9527402..e50ca6d1`,共 60 个提交;全文范围为 `v0.13.7..e50ca6d1`。初次固定至 `bbf24c28` 的 58 个提交,收尾补查新产生的两项提交。本地桌面 `v0.15.11` 与 Agent `agent-v0.15.4` 同指向 `78afec77`,上一桌面 `v0.15.10` 指向 `ff61da28`。Telegram、自定义回顾日期、历史耗时缺失提示及最后一组运行修复在 0.15.11 标签之后,均已提交,不将其混入该标签,也不使用开发状态标签。以下取代旧节的当前状态描述,旧节仅作历史依据。
|
||||
|
||||
@@ -386,4 +386,4 @@ Agent 专题使用对应的 `agent-v…` tag。
|
||||
| 日期 | 目标 tag | 标题 | 状态 | 正文 | 发布链接 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-09-19 | 未确定(unreleased) | GoodBuddy 更新预览:画布笔记、文档处理与应用管理 | 草稿,截图待补 | [综合更新稿](./2026-09-19-unreleased/article.md) | 未发布 |
|
||||
| 2026-09-26(10-05 更新) | v0.15.11(78afec77)及之后截至 e50ca6d1;汇总 v0.13.8 起的变化 | GoodBuddy 更新:故事图谱、Obsidian接入、Runtime原生终端 | 草稿,已补 Telegram 私聊、统一执行、项目会话菜单、文件导入、自定义回顾日期、监督模型与会话图谱开关、临时失败重试、3D 和性能改进;Telegram 实际联调、生产配图及公开发布信息待核验 | [综合更新稿](./2026-09-26-unreleased/article.md) | 未发布 |
|
||||
| 2026-09-26(10-09 更新) | Desktop 0.15.13/Agent 0.15.6 及之后截至 3556df88;汇总 v0.13.8 起的变化 | GoodBuddy 更新:故事图谱、Telegram 与 Obsidian 接入 | 草稿,按故事图谱、Telegram、Obsidian 三条主线重排;其他功能合并为辅助改进,生产配图及公开发布信息待补 | [综合更新稿](./2026-09-26-unreleased/article.md) | 未发布 |
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
| 文档 | 职责 |
|
||||
| --- | --- |
|
||||
| [Electron UI 自动化测试规范](./electron-ui-automation.md) | 测试分层、当前源码启动、视觉与原生交互取证、真实调用计数、脱敏报告和清理 |
|
||||
| [桌面并发架构验收](./concurrent-desktop-acceptance.md) | 一次完整架构交付的多项目/会话/子任务负载、计时口径、正确性断言、平台与验收记录要求;当前候选仍有未完成项 |
|
||||
| [复发缺陷与防复发规则](./recurring-defects.md) | 已重复出现的缺陷类型、对应的共享机制和自动发现测试 |
|
||||
| [开发与构建](../../BUILD.md#质量验证) | 依赖、测试、类型检查、Lint 和构建命令 |
|
||||
| [UI 设计系统](../../UI-DESIGN.md#155-验收) | 全局视觉、交互和响应式验收要求 |
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# 桌面并发架构验收
|
||||
|
||||
## 状态与职责
|
||||
|
||||
2026-10-05,设计基线为 `2be57492`。本文定义一次完整架构交付的验证要求,测试脚本与结果尚未完成。
|
||||
目标架构见[桌面存储与并发执行](../architecture/desktop-storage-direction.md),规范和数值预算以
|
||||
[性能原则](../architecture/performance-principles.md)为唯一依据。既有 KISS 测量只作为基线证据,不能替代本矩阵。
|
||||
|
||||
验收对象是同一候选中的完整产品路径,不单独验收“接口改成 Promise”“Worker 可调用”或“单会话很快”。
|
||||
所有必测场景完成前,不标记 PERF-15/PERF-16 为完成;未测平台及指标明确列出,不以跳过视为通过。
|
||||
|
||||
## 发布门槛与目标差距
|
||||
|
||||
2026-10-06 起按两类判定,沿用既有“历史回归上限”与“性能预算”的分层,不新增判定体系:
|
||||
|
||||
| 类型 | 内容 | 发布判定 |
|
||||
| --- | --- | --- |
|
||||
| 正确性与稳定性 | A01–A10;请求排空、无静默丢弃、提交后 ACK、取消结算、故障不传染、资源回落、进程退出后可恢复 | **必须全部通过**,不能用“比旧版好”替代 |
|
||||
| 性能对照 | 同机、同数据、同负载,与改造前提交交替三轮;含单会话简单操作和后台吞吐 | **不得退化**:候选不差于基线的重复测量波动范围 |
|
||||
| 性能预算 | 性能原则第 1 节的绝对数值 | 未达到时记录为目标差距,不阻断本次发布 |
|
||||
|
||||
“不得退化”同时覆盖单会话路径。存储迁到独立进程后多一次 IPC,单个简单操作最可能变慢,只测并发场景会漏掉这种退化。
|
||||
后台吞吐、回顾发布和事件提交也要对照,界面流畅但后台明显变慢不算改善。
|
||||
|
||||
## 固定负载
|
||||
|
||||
以下为验收档位,不是已验证的容量承诺,也不为产品新增并发限制。当前产品若不能达到某档位,记录实际原因;不能用事先写入的内部上限代替测量。
|
||||
使用本地确定性模型与隔离目录,每个输入、事件和工具结果有稳定标识,能够逐项验证顺序及重放结果。
|
||||
不得用真实提供商的网络速度作为应用性能基准。
|
||||
本地临时库、日志和源码测试产物遵守仓库 [AGENTS 临时文件规则](../../AGENTS.md#local-temporary-files),
|
||||
使用仓库根目录 `temp/` 的任务子目录并在关闭资源后清理;可复用夹具提交到 `tests/` 或 `scripts/`,
|
||||
正式验收摘要记录在文档中,不依赖唯一一份临时结果。远程临时目录沿用 Host 专用规则。
|
||||
|
||||
| 场景 | 项目/并行会话/同时活动子任务 | 必须同时执行的用户动作 |
|
||||
| --- | --- | --- |
|
||||
| C01 日常并行 | 3/4/8,子任务为全场景总数 | 持续打字、跨项目切换、查看来源和工具详情、首次进入监督者 |
|
||||
| C02 并发压力 | 4/8/16,子任务为全场景总数 | 滚动长历史、搜索、切换缓存会话、取消其中一个父任务,其他会话继续 |
|
||||
| C03 混合存储 | C01 加 100 批次回顾发布、知识导入和检索 | 前台输入与保存,打开图谱,观察远程事件提交/ACK;各操作必须真实重叠 |
|
||||
| C04 运行时故障隔离 | C01 中挂起一个启动、模型路由准备或清理 | 查询其他任务、取消故障请求、继续健康 peer;覆盖本地及受影响的远程路径 |
|
||||
| C05 存储故障与饱和 | C01 中注入读 Worker 失败、队列达到上限、存储进程退出 | 页面报错与重试,已提交结果可重读,未确认写入不盲目重放,UI 仍可响应 |
|
||||
| C06 持续生命周期 | C02 连续 5 分钟,每轮完成、关闭并重开任务/会话 | 保持草稿、展开、焦点及滚动状态,观察队列、缓存、订阅和所属进程回落 |
|
||||
| C07 冷热进入 | 新进程首次打开及同进程暖返回,分别在空闲和 C01 下测量 | 输入框、监督概览、图谱、历史选择、来源正文分开记录可用时刻 |
|
||||
|
||||
默认数据为 300 个已保存会话、每个 20 条消息,另加 2,000 条消息的长会话;压力数据改为 10,000 条长会话消息。
|
||||
监督数据沿用可复现样本:20 个结果、40,000 条消息、8,000 事件/来源、400 故事、800 经验;消息及来源正文各 2,048 ASCII 字节。
|
||||
回顾发布为 100 批、每批 20 来源,知识检索保留既有 5,000/20,000/50,000 文本块样本。各场景只装入相关数据,不把互不相关的大样本无条件叠加。
|
||||
|
||||
活动流初始使用每会话每 16 ms 一个文本片段、每父会话每秒一个有界工具/子任务更新;工具输出固定为 4 KiB,实际数量、字节数和时间表写入 fixture。
|
||||
每个会话的首次响应、持续流、完成和清理都要经过生产路径。调度抖动时记录实际发送率及迟到分布,不能丢弃片段以保持设定速率。
|
||||
若现有 Runtime 会自然合并事件,分别记录生产与消费数量,按可还原内容验证等价。
|
||||
|
||||
## 计时和判定
|
||||
|
||||
| 指标 | 起止与用途 |
|
||||
| --- | --- |
|
||||
| 输入/点击反馈 | 原生输入事件至目标反馈后的帧机会;记录方法局限,不把两帧等待称为纯渲染时间 |
|
||||
| 内容可用 | 请求发起至所需正文/图谱/来源提交到界面;加载占位出现不能替代该指标 |
|
||||
| Main 阻塞及 p99 延迟 | 连续事件循环采样,排除夹具生成和数据库初始化;与 Renderer 长任务分别记录 |
|
||||
| 存储排队 | 入队至执行开始,记录操作类型、项目/会话归属及队列深度,不记录用户正文 |
|
||||
| 执行/提交 | 存储执行开始至结果或事务提交;完成响应不能早于承诺的提交点 |
|
||||
| 取消/清理 | 分别记录请求取消、执行结算、资源释放;UI 清除等待不等于清理完成 |
|
||||
| 吞吐和积压 | 固定 offered load 下完成数、字节数、队列高水位及输入停止后的排空时间 |
|
||||
| 内存与进程 | 全应用及各实际 PID 的工作集/堆;记录缓存条目、监听器、连接及 worker 数 |
|
||||
|
||||
标准机和 Renderer 4× 降速分别报告,UI 与 Main 的绝对预算引用性能原则第 1 节。
|
||||
Renderer 降速不等于模拟慢磁盘、慢 Main 或真实低配整机,不能从该结果推断它们合格。
|
||||
原有 `perf:check` 回归上限比规范宽松;即使上限检查通过,违反规范仍须报告失败或明确的未满足项。
|
||||
|
||||
存储排队、吞吐、内容就绪和取消清理不得被 UI 流畅掩盖。每份 fixture 在测试实现时固定最大等待/排空期限,
|
||||
使用既有产品期限或改造前重复测量支持的验收值,先记录依据再运行候选;禁止测完候选后放宽期限。
|
||||
没有期限、样本不足或未采集的必需指标不能记为通过。固定输入下吞吐不得低于基线的重复测量波动范围,
|
||||
队列不得持续增长,有限后台任务在持续前台负载下必须完成。容量溢出按设计等待或明确失败,不能静默丢事件。
|
||||
|
||||
每版本至少三轮交替运行,基线从独立 worktree 获得。交互延迟每轮每个指标至少 100 个有效样本;
|
||||
首次进入通过独立启动样本报告各次值,不用三次启动估计 p95。持续负载场景先预热 30 秒,测量至少 120 秒;
|
||||
C06 单独执行 5 分钟,足以观察多轮创建、完成后的资源趋势;需要更长时间时另行说明理由。记录 OS、CPU、内存、磁盘、Electron/Node、提交、数据校验值、并发、缓存状态和后台负载。
|
||||
|
||||
无新增数据的重复生命周期测试中,逻辑缓存/订阅/连接应回到约定存续范围,队列排空后为零,结束的所属子进程退出。
|
||||
RSS 未立即回落不能单独判为泄漏;需结合存活对象、资源计数和重复轮次趋势。不能通过杀掉健康共享 Runtime 达到清零。
|
||||
|
||||
## 正确性断言
|
||||
|
||||
| ID | 断言 | 对应架构要求 |
|
||||
| --- | --- | --- |
|
||||
| A01 | Main 不直接或间接打开业务 SQLite;正常、失败、饱和、维护及退出路径均无同步回退 | SA-01、SA-06 |
|
||||
| A02 | 同会话事件不丢、不重、不乱序;不同项目/会话/子任务的结果、取消和凭据不串用 | SA-03、SA-05 |
|
||||
| A03 | 远程事件、checkpoint 提交后才 ACK;崩溃后重放去重;发件箱与模型账本不重复执行 | SA-02、SA-05 |
|
||||
| A04 | 事务失败整体回滚,提交后立即读取可见;回顾结果与 checkpoint 同步发布 | SA-02、SA-05 |
|
||||
| A05 | 取消排队请求不执行,取消在途请求等待实际结算;迟到结果不污染新请求,健康 peer 不受影响 | SA-03、SA-06 |
|
||||
| A06 | 关闭前先停止新写入、排空必要写入、关闭连接;替换存储进程前观察旧进程退出 | SA-06 |
|
||||
| A07 | 多项目切换不丢草稿、展开、选区、焦点和滚动;后台任务继续,切回不全量重建历史 | SA-04、SA-05 |
|
||||
| A08 | UI DOM 随视口有界;缓存和队列有清理;未访问正文不随摘要列表整包返回 | SA-04 |
|
||||
| A09 | 已发布数据库升级保留用户内容、ID、顺序、引用、去重与落盘语义,不静默换空库 | SA-02、SA-05、SA-06 |
|
||||
| A10 | GoodBuddy 中转无非必要落盘;完整输出复用唯一正文,分页和释放正确,必要临时文件及历史残留按归属清理 | SA-04、SA-05、SA-08 |
|
||||
|
||||
故障注入覆盖提交前、提交后但响应前、初始化期间和退出期间。使用现有业务 ID 查询实际结果;
|
||||
不得为了让测试容易恢复而引入新的持久化 receipt 或盲目重放。强杀共享存储进程仅用于隔离测试中的服务故障场景。
|
||||
|
||||
## 输出与临时资源验收
|
||||
|
||||
按[源码调查](../architecture/runtime-storage.md#output-and-temporary-file-investigation)选取生产入口,
|
||||
在 C01/C02/C06 中记录 GoodBuddy 创建的文件数、写入字节、峰值保留字节、清理后残留及内存高水位。
|
||||
区分正式保存、必要启动文件和中转写入;结果只计 GoodBuddy 所有的文件,不把第三方内部缓存归为产品临时副本。
|
||||
沿用三轮前后对照,不以目录改名或文件移到数据库目录证明减少写入。
|
||||
|
||||
| 覆盖 | 必须验证的结果 |
|
||||
| --- | --- |
|
||||
| 普通 MCP、短命令输出、空 stderr、短搜索/子任务输出 | 中转不创建输出临时文件;需要保存的正常消息/事件仍持久化,MCP SDK 实际 transport 路径纳入检查 |
|
||||
| 超出预览的输出 | 从现有工具到下一次模型 `output_read` 的生产链逐字节还原,覆盖 UTF-8、JSON 转义、游标、EOF、stdout/stderr、失败与取消;只保留一份所属正文,无 Temp 全文再复制 |
|
||||
| 并发与生命周期 | 保留现有会话归属检查,不能读取其他会话输出;多路输出内存有界,释放调用资源不删除仍被历史/分支引用的内容 |
|
||||
| 数据等价 | 分别核对模型收到的内容、工具事件预览和历史内容;不能用最终回答等价替代工具全文,也不新增整份事件正文副本 |
|
||||
| 文档解析 | 不再新写/复制重复 `parsed.md`,manifest 内容、原文件、图片及附件采用/复制/重开仍正常;旧数据可读 |
|
||||
| 必要启动文件 | 配置/插件加载正常,创建中失败能清理所属半成品,子进程及句柄关闭后再删除;失败不误删预先存在的同名文件 |
|
||||
| 升级与再次启动 | 隔离夹具覆盖旧版失效目录、同版崩溃目录、仍活跃的子进程、归属不明目录和删除失败;只回收确认失效者,失败下次可重试,启动预算不退化 |
|
||||
|
||||
复用 `paged-output-store.test.ts`、`direct-model-process-service.test.ts`、
|
||||
`direct-model-ripgrep.test.ts`、`direct-model-subagent-service.test.ts` 和
|
||||
`model-runtime.test.ts` 中已有分页与模型下一轮请求断言,并扩展文档存储、附件及 Runtime 生命周期测试。
|
||||
远程路径单独验证事件映射和提交后 ACK,不声称本地分页测试覆盖远程全文。
|
||||
无需真实模型即可确定的字节、文件和清理断言使用固定输出;受影响的 Runtime 再按既有 Host 规则验证。
|
||||
|
||||
交付记录列出删除的旧 spool 创建入口、复用的正式内容入口、仍必要的文件及理由、实际残留与未能确认归属的旧目录。
|
||||
本次文档调查未执行上述验收,没有磁盘节省或性能改善的实测结论。
|
||||
|
||||
## 平台与生产路径
|
||||
|
||||
性能主对照使用固定参考机。Windows、macOS、Linux x64/arm64 的原生验证检查进程启动、SQLite 可用性、
|
||||
文件路径及关闭、升级和重启;没有硬件性能基线的平台只报告功能结果,不与参考机数字混算。
|
||||
受影响的远程事件持久化、ACK、binding 和 Runtime 生命周期在共享 Linux x64 Host 开发时验收,
|
||||
采用专用目录和既有固定身份;真实模型调用数单独报告,不混入无外部请求的性能基准。
|
||||
|
||||
正常 UI → Preload → Main → 存储进程 → SQLite 的完整路径必须至少覆盖 C01、C03、C07;
|
||||
仅组件 mock bridge、独立 SQL 或进程握手均不能替代。原生客户端 ledger 的创建、失败、不确定结果及关闭删除也必须覆盖。
|
||||
|
||||
生产构建、各平台包内入口及进程启动通过既有 CI 和[发布手册](../development/release-runbook.md)验证。
|
||||
本规范不授权发布准备期间本地打包或启动 portable 原始用户目录;源码测试使用隔离数据。
|
||||
|
||||
## 自动检查与交付记录
|
||||
|
||||
### 复用既有验收入口
|
||||
|
||||
扩展下表中的脚本和测试,不新增一套并行性能框架。现有能力与缺口均来自源码核对,不能把“已有测试文件”等同于已覆盖新语义。
|
||||
|
||||
| 场景/断言 | 既有入口 | 本次需要补齐的内容 |
|
||||
| --- | --- | --- |
|
||||
| C01/C02/C06/C07 | `build/run-app-perf.cjs`、`tests/support/app-perf-driver.mjs`、`build/run-startup-perf.cjs` | 多项目生产路由、并行流和子任务、冷热功能进入、资源回落;为 C06 显式设置匹配的运行期限,保留原场景期限 |
|
||||
| C03/A04 | `build/run-main-perf.cjs`、`build/run-knowledge-perf.cjs`、`build/run-write-perf.cjs` | 保留既有数据,新增存储客户端和真实重叠负载;与完整 App 结果对应,不把分开测量冒充同时执行 |
|
||||
| C04/A02/A03 | `remote-event-batcher.test.ts`、`ipc.test.ts`、`runtime-acp-backend.test.ts`、`runtime-controller.test.ts` | 异步提交延迟/拒绝时禁止提前通知及 ACK;覆盖交互、后台和恢复三个入口,open/prepare/close 清理时健康 peer 仍可操作 |
|
||||
| C05/C06/A05/A06 | `readonly-query-reader.test.ts`、`supervision-model-pool.test.ts`、`subagent-scheduler.test.ts`、`shutdown.test.ts` | 取消后真实容量、队列饱和、进程丢失和等待退出;升级预检 SQL 迁出,checkpoint 终止得到确认 |
|
||||
| A04/A07/A09 | `supervision-worker.test.ts`、`supervision-ui-reads.test.ts`、`supervisor-layout.electron.test.ts`、`message-expansion.electron.test.ts` | 保留事务、来源及交互断言;完整 App 扩展 UI 到真实存储链,不能只用组件模拟 bridge |
|
||||
| SA-07 | `eslint.config.js`、`build/check-perf-thresholds.cjs`、`tests/perf-thresholds.test.ts`、已有 Desktop/Agent workflow | 间接打开点、白名单基线、负载元数据、缺失指标及并发场景;预算与历史回归上限分别报告 |
|
||||
|
||||
既有部分脚本默认使用系统临时目录,复用时按 AGENTS 统一到仓库 `temp/`;新增的可重用夹具进入 `tests/` 或 `scripts/`。
|
||||
C06 使用独立场景时长参数,不能通过放宽所有测试超时处理。
|
||||
现有阈值检查未验证全部负载规模,需校验项目、会话、子任务、数据量及降速条件,缺少或不匹配时不能产生合格结果。
|
||||
|
||||
### 替代路径核验
|
||||
|
||||
A01/A03/A05/A06 还必须逐项确认:Main 升级预检与间接 ledger 打开已迁出,生产 `readWithFallback` 同步回退已删除,
|
||||
监督清单不再持有独立业务写入权限,远程 `flush` 等待提交且失败可传播,原生客户端先完成 ledger claim 再发模型请求,
|
||||
关闭 ledger 后才删所属目录。检查外部 live repository 工厂、重复客户端、重复队列和旧 shutdown 回调无生产调用。
|
||||
保留合法的仓储同步单测、原子事务内部读取和远端数据库实现;不以删除测试或已发布兼容代码实现“清零”。
|
||||
|
||||
### 最终记录
|
||||
|
||||
- 静态检查覆盖 `DatabaseSync`、同步文件 API、`openPrivateSqliteDatabase` 及桌面间接构造点;
|
||||
允许的存储宿主/测试/远端代码按执行环境列出,不能整片豁免 `src/main`。
|
||||
- 白名单和 App 上限与已提交基线比较;迁出后删除例外,不因换文件名增加例外。
|
||||
- 普通 CI 运行契约、顺序、取消、资源释放和生产路径测试;可复现性能执行环境运行上述关键并发基准,
|
||||
在合并/发布候选检查中提供必需结果。没有合适 runner 时,记录为验收缺口,不能默认通过。
|
||||
- 最终同一候选运行 `npm test`、`npm run typecheck`、`npm run lint`,并核对 native CI/Host 与性能报告对应的源码。
|
||||
- 验收记录列出每个 C/A 编号的结果、命令、日志位置、样本数、原始数据和未测项;单独报告低配、故障注入及真实模型结果。
|
||||
|
||||
截至本文编写,只有历史局部测量可引用,尚无本矩阵的全项通过记录。待实现完成后新增具体日期的验收报告,
|
||||
保留失败结果和复测理由,不把目标架构文档改写成未经验证的进度证明。
|
||||
@@ -0,0 +1,323 @@
|
||||
# 存储架构升级实施与验收记录
|
||||
|
||||
## 交付范围
|
||||
|
||||
本记录跟踪 [架构设计](../architecture/desktop-storage-direction.md) 的 SA-01 至 SA-08,并汇总
|
||||
[Assistant 消费者](../architecture/assistant-storage-consumers.md)、[Knowledge 迁移](../architecture/desktop-storage-knowledge.md)、
|
||||
[文件存储](../architecture/desktop-storage-files.md) 和 [Runtime 存储](../architecture/desktop-storage-runtime-integration.md) 的当前实现。
|
||||
同一候选已完成多项桌面数据库所有权迁移、异步生产调用、输出唯一存储和临时资源清理。
|
||||
内部工作按依赖并行执行,最终整体交付。接口、进程握手或分组测试通过均不代表产品完成。
|
||||
保留现有未提交文档及仓库规则修改,不自动提交或发布。
|
||||
|
||||
## 实施分工
|
||||
|
||||
| 工作包 | 实施内容及替代入口 | 验证要求 | 当前状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| S01 存储宿主 | 复用 reader 请求关联与 utilityProcess;宿主打开 Assistant/Knowledge,负责升级、查询分派、提交和退出 | 真实 Electron 进程、升级失败重试、取消结算、提交后读取、退出排空 | 已实现并接入生产组合;全量验收未完成 |
|
||||
| S02 Assistant 调用 | IPC、心跳、监督、任务、笔记、通道、审计和工具接口改用异步领域操作;回调事务留在宿主 | 先写后读、回顾原子发布、项目隔离、暂停/恢复、错误传播 | 已接入生产组合;完整并发验收未完成 |
|
||||
| S03 Knowledge 调用 | 数据库与 external repository 归宿主;解析、模型与凭据保留原所属;发布 DTO 替代事务回调 | 导入、检索、重建、embedding、删除和取消 | 已接入生产组合;完整 App 和并发验收未完成 |
|
||||
| S04 文件与关联 | 文档结果、附件引用、正文分页由同一存储宿主管理;删除重复 Markdown;保留分支引用 | 采用/复制/释放/重启,取消失败全文,删除项目引用回收 | 已接入生产组合;完整 App 和并发验收未完成 |
|
||||
| S05 绑定与账本 | 桌面远程绑定和原生客户端账本迁入宿主;远端数据库保持原所有权 | claim 后派发、提交后 ACK、响应不确定、关闭后删除目录 | 已接入生产组合;完整 App 和并发验收未完成 |
|
||||
| S06 输出与启动材料 | 短输出有界内存,全文唯一 backing;必要配置文件归属、部分创建清理、启动回收 | 字节等价、内存上限、无多余文件、活跃所有者保护 | 生产 backing 已接入;跨场景文件与生命周期验收未完成 |
|
||||
| S07 应用组合 | Main 初始化、事件通知、远程批处理、shutdown 接入统一存储;删除旧打开和同步回退 | 正常 UI 进入真实宿主,后台/恢复入口,退出失败可见 | 当前 Main 组合已接入;全路径验收未完成 |
|
||||
| S08 守护与验收 | 扩展现有 lint、性能脚本和 Electron 测试;复核旧调用及重复实现 | 全仓检查、C01–C07/A01–A10、真实模型及数据副本 | 待最终候选 |
|
||||
|
||||
S01 的契约供 S02–S06 复用,S07 统一组合。跨包共享文件由指定整合者修改;业务代理提供所需操作和测试。
|
||||
全量测试只对组合后的候选作最终判定。每次失败记录源码版本、原因、修复及复测,不放宽预算。
|
||||
|
||||
## 已发现问题对应关系
|
||||
|
||||
监督者重读/发布 Main 阻塞对应 S01/S02/S07;过大响应、窗口化和摘要缓存保留此前修复并在 C03/C07 回归。
|
||||
跨会话全局等待、取消/迟到路由、超限用量与 Telegram 交付保留已有修复,纳入故障和混合负载验收。
|
||||
输出短内容写后即删、全文引用缺失、重复解析 Markdown、Continue 部分创建和 helper 退出、Python 归档残留对应 S04/S06。
|
||||
Agent 旧包/远程 staging 的失败清理、项目删除附件回收、诊断解析结果归属及笔记原子临时残留需逐项核对现有实现后记录处理结果。
|
||||
有意保留的原生历史、安装缓存、用户导出、模型文件与 WAL 不作为垃圾删除。
|
||||
|
||||
## 真实数据与模型
|
||||
|
||||
已定位 portable:`dist/GoodBuddy-windows-x64/data`。该位置是原始数据,只读取统计并制作一致性备份,禁止直接启动候选写入。
|
||||
数据库存在活动 WAL 时使用 SQLite 一致性备份;备份与工作副本分开,测试材料位于仓库 `temp/goodbuddy-storage-upgrade/`。
|
||||
仅复制测试必需的数据库及所引用文件,记录校验值、schema、行数与完整性;不把凭据或正文写入报告。
|
||||
源库无 WAL 且确认关闭时才允许直接文件复制。升级后逐项比较稳定 ID、顺序、引用、事件还原与重放去重。
|
||||
|
||||
性能采用本地确定性模型,真实模型用于生产功能链:普通会话及工具、子任务、监督发布、本地/远程 Runtime。
|
||||
外部调用有界,记录实际请求数;真实数据内容不会为了性能测试默认发送到外部模型。
|
||||
远程验证沿用共享 Linux x64 Host、现有凭据和专用测试目录,结束确认所属进程退出。
|
||||
|
||||
## 完成条件
|
||||
|
||||
最终运行 `npm test`、`npm run typecheck`、`npm run lint` 和差异检查;构建入口变化验证开发构建。
|
||||
按 [并发验收规范](./concurrent-desktop-acceptance.md) 固定负载和三轮前后对照,包含 30 分钟生命周期。
|
||||
分别报告界面延迟、Main 阻塞、存储等待/提交、吞吐、取消、内存和文件增长,禁止将单测通过替代性能结论。
|
||||
实测超预算、未覆盖平台或外部环境阻断均保留为未满足项,不能宣称全部已解决。
|
||||
|
||||
当前记录确认 S01-S07 已有生产组合和定向证据;解析 Worker 的无依据队列拒绝已移除。
|
||||
完整架构验收、平台包验证和 C01-C07/A01-A10 仍未完成。
|
||||
|
||||
## 2026-10-05 Portable 真实副本验证
|
||||
|
||||
本轮已执行当前源码的 `DesktopStorageClient → utilityProcess → DesktopStorageOwner`,
|
||||
使用生产升级、读取、发布和关闭入口。脚本为
|
||||
`tests/support/portable-storage-upgrade.mjs` 与 `portable-storage-upgrade-fixture.ts`。
|
||||
成功运行始于 `2026-10-05T12:03:32.624Z`,总耗时 265.074 秒;
|
||||
基于提交 `4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a` 的未提交工作树。
|
||||
环境为 Windows `10.0.26200`、Intel Core Ultra X7 358H、33,873,752,064 字节内存、
|
||||
Node `24.21.0`、Electron `44.5.1`。这次未启动完整桌面 UI。
|
||||
|
||||
### 原件、备份与清理
|
||||
|
||||
原件位于 `dist/GoodBuddy-windows-x64/data`。成功运行没有在该目录打开 SQLite 连接,
|
||||
也没有启动以该目录为用户数据目录的候选程序。先复制主文件与 WAL 到隔离 capture 目录,
|
||||
确认源文件的大小、修改时间和 SHA-256 未变,再从 capture 执行 SQLite online backup,
|
||||
由备份生成独立工作副本。本轮四个源 WAL 均为 0 字节;不能据此声称已覆盖持续写入的活动 WAL。
|
||||
各数据库分别备份,文件集合不构成跨数据库事务快照。
|
||||
|
||||
四个源主文件及各自 WAL/SHM 共 12 个文件的前后大小、修改时间和 SHA-256 全部一致。
|
||||
升级后的四个备份主文件 SHA-256 也与升级前一致。73 个附件引用均有可复制内容,
|
||||
包括目录附件,合计 12,720,927 字节;50 个笔记文件合计 431,056 字节。
|
||||
附件及笔记在复制时校验内容摘要,生产写入与重开后再次确认原件及工作副本内容未变。
|
||||
日志只保存表名、计数、摘要和断言结果;没有复制设置或凭据,没有把正文写入报告,外部模型调用为 0。
|
||||
|
||||
被取消的旧探针曾继续占用 `run/work` 的 Assistant 文件。环境中的 `node` 实际由
|
||||
`GoodBuddy.exe` 承载,按可执行文件名查询 Node/Electron 会漏掉该进程。
|
||||
确认其命令行为本任务旧探针后,终止所属进程树并删除残留文件。
|
||||
成功运行自动删除 `fresh-run` 的 capture、backup、work、WAL/SHM 与 Electron 临时目录;
|
||||
旧 `run` 也已删除。保留的 `temp/goodbuddy-storage-upgrade/summary.json` 为无正文统计证据。
|
||||
原取消轮没有保存完整源文件前后报告,因此上述原件不变结论限定于本次有记录的验证区间。
|
||||
|
||||
### 数据与正确性
|
||||
|
||||
| 数据库 | 源主文件/备份字节数 | schema 前 → 后 | 实际数据规模 | 完成写入练习后的主文件字节数 |
|
||||
| --- | ---: | --- | --- | ---: |
|
||||
| Assistant | 2,429,247,488 | 61 → 62 | 11 项目、312 会话、4,836 消息、2,353 任务、1,755,171 事件、233 成果、23,690 活动记录 | 2,432,491,520 |
|
||||
| Knowledge | 1,568,768 | 13 → 13 | 2 知识库、2 文档、27 分块、27 embedding | 1,568,768 |
|
||||
| Attachments | 98,304 | 1 → 1 | 73 附件、73 引用 | 98,304 |
|
||||
| Runtime Bindings | 143,360 | 0 → 0 | 53 绑定 | 143,360 |
|
||||
|
||||
本轮实际覆盖的 Assistant 61 → 62 迁移新增监督来源/结果索引;数据已有历史差量存储,
|
||||
没有重跑 schema 33/34 的正文压缩迁移。
|
||||
|
||||
在追加合成记录之前,对四库全部已有用户表逐行计算规范化摘要,比较行数与表摘要。
|
||||
比较包含稳定 ID、顺序字段、时间、引用和远程来源字段;子任务事件按任务及事件 ID 顺序,
|
||||
调用生产 `restoreSubagentPayload` 还原后比较。未变的不可变过程块复用内容摘要,
|
||||
过程块数量与顺序仍参与每条事件的摘要。所有已有表的 `changed` 与 `added` 列表均为空,
|
||||
没有需要豁免的启动恢复差异。Assistant 完整扫描前/后分别耗时 80.393/107.989 秒。
|
||||
|
||||
备份、升级副本及完成写入练习后的四库 `integrity_check` 均为 `ok`,
|
||||
`foreign_key_check` 均为 0 条。真实监督数据包含 3 个结果、434 个事件、758 个来源;
|
||||
读取其中一个图谱并关闭/重开,内容摘要一致。
|
||||
新增远程事件的首次写入、同内容重放去重、冲突拒绝、已提交序号读取,以及重开后的重放去重均通过。
|
||||
100 批合成监督发布每批包含 20 个来源及 20 个事件;最后一批的事件、来源和 20 条关联均存在,
|
||||
关联端点有效,关闭/重开后图谱摘要一致。
|
||||
|
||||
Assistant 空闲页从 1,643 变为 1,619,Knowledge 维持 90 页;本轮没有证明空间回收收益。
|
||||
表中 Assistant 最终大小包含后续合成发布与去重记录,不能当成仅升级造成的增长。
|
||||
最终工作 WAL 的独立字节数没有写入该轮报告,退出后相关副本已清理。
|
||||
|
||||
### 耗时与边界
|
||||
|
||||
| 项目 | 单轮观测 |
|
||||
| --- | --- |
|
||||
| 首次生产存储就绪/关闭后再次就绪 | 557.778/308.364 ms |
|
||||
| 后续练习初始化/再次就绪 | 333.349/286.487 ms |
|
||||
| 100 次发布与 100 组图谱+结果列表读取重叠执行 | 32,363.016 ms |
|
||||
| 发布 p50/p95/max | 10.830/15.269/27.026 ms |
|
||||
| 读取组 p50/p95/max | 334.647/355.728/368.961 ms |
|
||||
| 夹具 Main 事件循环延迟 p99/max | 16.482/17.121 ms,共 2,117 个采样 |
|
||||
| 结束时 pending/Main RSS | 0/93,634,560 字节 |
|
||||
|
||||
这些数值来自单轮、两个有界生产者的闭环负载,不能替代固定 offered load、三轮前后对照、
|
||||
多项目混合负载或 C01–C07/A01–A10。pending 高水位只在请求完成后采样,观测值 1 不是全程峰值。
|
||||
未测 UI 响应、Renderer、存储进程内的等待与提交分解、30 分钟生命周期、真实模型及远程 Host。
|
||||
Main 延迟采样也不能替代单次阻塞测量,当前证据不足以宣称所有性能预算达标。
|
||||
|
||||
探针设有 15 分钟总期限、每个 Electron 练习 180 秒期限、完整扫描 600 秒检查,
|
||||
扫描每 100,000 行报告进度,练习每 10 秒报告阶段;通过后台运行与有界轮询执行。
|
||||
前一版重复序列化累计过程的扫描超过期限,修正为复用块摘要后才完成全量比较,未减少事件数。
|
||||
另外修复了 `publishedGraph` 的 `unknown` 使用:用共享 `supervisionGraphViewSchema` 校验响应,
|
||||
没有用类型断言跳过检查。依赖目录缺失曾阻断续跑,经用户授权按 lockfile 恢复后完成执行。
|
||||
|
||||
### 检查结果
|
||||
|
||||
`npm run typecheck` 全仓通过,用时 14.312 秒。两个探针文件的 ESLint 通过。
|
||||
定向运行 `desktop-storage.test.ts`、`desktop-storage-files.test.ts`、
|
||||
`desktop-storage-runtime-operations.test.ts` 与 `assistant-storage-upgrade.test.ts`,
|
||||
4 文件、31 测试全部通过,Vitest 报告 8.56 秒;包含真实 Electron 存储及历史迁移合成回归。
|
||||
结束前执行 `--verify-source`,再次确认 12 个源 DB/WAL/SHM 指纹和笔记摘要与本轮基线一致。
|
||||
`git diff --check` 无空白错误。中文审校扫描无阻断项,复核项中的测试边界与实际统计表述保留。
|
||||
|
||||
全仓 `npm run lint` 最后仍有一个范围外错误:
|
||||
`tests/support/story-graph-focus-driver.mjs:122` 的 `console` 未定义;没有修改该探针。
|
||||
全仓 `npm test` 使用 300 秒总期限启动,结束时没有留下最终汇总或退出结果,不能标记为通过。
|
||||
已输出的失败涉及 `MarkdownRenderer.cache.test.tsx` 的 1 个微基准、
|
||||
`image-tool-mcp.test.ts` 的 3 个测试及 `obsidian-electron.test.ts` 的 1 个测试。
|
||||
这些失败保留给对应实现负责者,不以本轮存储验证通过覆盖全仓失败。
|
||||
检查产生的本任务临时目录也在所属进程退出后清理;没有提交代码。
|
||||
|
||||
## 2026-10-05 C01-C07 并发桌面验收(未完成)
|
||||
|
||||
本轮使用现有 `build/run-app-perf.cjs` 与
|
||||
`tests/support/app-perf-driver.mjs`,新增最小并发模式夹具
|
||||
`tests/support/app-perf-concurrent.mjs`。夹具通过真实 UI、Preload、Main、存储宿主和本地回环确定性模型运行;没有启动 portable 原始目录,也没有外部模型请求。基线使用独立 worktree 的提交
|
||||
`4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a`,候选为同一提交的当前未提交工作树。日志和原始 JSON 位于 `temp/goodbuddy-concurrent-upgrade/`。
|
||||
|
||||
本轮没有达到验收规范要求的三轮交替、每项三轮各 100 个有效样本,也没有完成全部 C01-C07。因此不能标记 PERF-15/PERF-16 或本架构验收通过。
|
||||
|
||||
| 场景 | 实际结果 | 证据与主要指标 |
|
||||
| --- | --- | --- |
|
||||
| C01 3/4/8 | 未通过 | 当前候选首轮测量 1,898 个按键样本,p95 输入 16.8 ms、p95 帧 17.0 ms,但 Main 最大事件循环延迟 64.4 ms;请求未排空,驱动超时。基线首轮在第一批真实发送前等待发送按钮超时。 |
|
||||
| C02 4/8/16 | 未通过 | 两个 30 秒测量周期完成;周期 1 有 494 个按键样本,p95 输入 17.4 ms、p95 帧 17.2 ms、4.2% 抖动、Main p99 18.4 ms、最大 25.3 ms。第三周期在请求排空前超时;标准机输入和帧 p95 均超预算。 |
|
||||
| C03 回顾发布+知识重叠 | 未执行 | 现有 App 夹具没有真实重叠的 100 批回顾发布、知识导入/检索和图谱读取生产动作。 |
|
||||
| C04 故障隔离 | 未执行 | 没有挂起启动/模型路由/清理并验证取消及健康 peer 的夹具。 |
|
||||
| C05 存储故障与饱和 | 未通过 | C06 运行期间生产存储路径实际多次返回 `STORAGE_CAPACITY`;错误来自 `ReadonlyQueryReader.call` 的会话队列读取,已保留在 `soak-current/execution.log`。专门读 Worker 失败、存储进程退出和提交前后故障注入尚未完成。 |
|
||||
| C06 30 分钟生命周期 | 未完成 | 已启动完整期限为 1,800,000 ms 的后台运行,实际约 34.5 分钟后因驱动超时退出;首个 30 秒测量有 494 个按键样本,p95 输入 16.6 ms、p95 帧 16.9 ms、Main p99 17.5 ms。由于请求持续在途且存储容量错误,未完成规范要求的完整生命周期、关闭重开和排空,不能算通过。 |
|
||||
| C07 冷/暖进入 | 未通过 | 两个周期完成;周期 1 有 442 个按键样本,p95 输入 17.8 ms、p95 帧 50.0 ms、8.2% 抖动、Main p99 17.7 ms。夹具尚未分别测量监督概览、图谱、历史选择和来源正文的冷/暖内容可用时刻。 |
|
||||
|
||||
资源证据包括每个场景的 Browser、Tab、GPU、Utility PID、CPU 和工作集快照。当前候选的 C02 周期 1 Tab 工作集约 210 MB、Browser 约 200 MB;C07 周期 1 Tab 约 210 MB、Browser 约 212 MB。夹具已记录 3/4 和 4/8 项目/会话档位及 8/16 子任务 offered load,但生产事件中没有形成稳定的 8/16 个同时运行子任务,不能把 offered load 当成实际吞吐。周期性 4 KiB 工具更新、存储排队/执行拆分、缓存/订阅计数、临时文件字节数、C03-C05 故障动作和完整 supervisor 冷/暖内容指标均为未采集项。
|
||||
|
||||
本轮没有修改核心存储或调度实现。`node --check build/run-app-perf.cjs` 和
|
||||
`node --check tests/support/app-perf-concurrent.mjs` 通过;未运行全仓 `npm test`,
|
||||
因此本节只记录局部复测,不改变 C01-C07 的未完成状态。
|
||||
|
||||
### 2026-10-05 C01/C02/C07 实际证据复核与局部修复
|
||||
|
||||
复核了上述原始 JSON、`soak-current/execution.log` 及生产源码。容量错误的实际调用链为
|
||||
`pumpConversationQueue` → `DesktopStorageClient.listConversationQueueItems` →
|
||||
`ReadonlyQueryReader.call`;同一压力段还可见 `tasks:execution-stats`、工作区变化和概览读取竞争同一
|
||||
有界读容量。C01 候选首轮为 1,898 个输入样本,输入 p95 16.8 ms、帧 p95 17.0 ms、Main 最大
|
||||
64.4 ms;C02 周期 1 为 494 个样本,输入 p95 17.4 ms、帧 p95 17.2 ms、Main p99 18.4 ms、
|
||||
最大 25.3 ms;C07 周期 1 为 442 个样本,输入 p95 17.8 ms、帧 p95 50.0 ms、Main p99
|
||||
17.7 ms。C02/C07 的长帧主要出现在持续事件输入和显示刷新期间,采样中没有长任务脚本时间,
|
||||
但有长动画帧的渲染/样式布局时间;因此未减少 offered load、输入样本或阈值。
|
||||
|
||||
本轮应用了三个最小局部调整:隐藏窗口时丢弃已过时的会话显示刷新并在重新可见时重读;侧栏隐藏时
|
||||
不挂载项目切换器、活动菜单及其项目行;队列泵遇到 `STORAGE_CAPACITY` 时按会话只保留一个
|
||||
50 ms 后重试,避免同一队列因容量瞬时不足进入错误风暴。正常事件、持久化和队列项目均保留。
|
||||
定向 `ipc`、刷新和项目切换回归为 248 passed、1 skipped;类型检查和相关 lint 通过。
|
||||
|
||||
重新构建后以相同 C02 档位(4 项目/8 会话/16 子任务、每轮 30 秒输入)执行了四个周期,
|
||||
原始报告位于 `temp/goodbuddy-concurrent-upgrade/profile-1MGfH7/artifacts/result.json`。修复后四轮
|
||||
输入 p95 为 16.9/16.9/17.0/17.3 ms,帧 p95 为 16.9/16.9/17.0/17.1 ms,Main
|
||||
最大事件循环延迟为 46.5/31.1/45.8/292.8 ms;最后一轮仍有 1 个 51 ms 长任务,驱动因
|
||||
请求未排空而超时。日志仍观察到少量其他后台读取的 `STORAGE_CAPACITY`,所以本轮只能证明显示
|
||||
刷新和队列重试的局部改善,不能标记 C02 或容量验收通过。相对原 C02 周期 1,前三轮输入/帧
|
||||
p95 已回到约 17 ms,但 Main 尾延迟和完整排空仍未达预算;C01/C07 尚未取得修复后的同口径复测。
|
||||
|
||||
## 2026-10-05 C03/C04/C07 补充运行(仍未满足)
|
||||
|
||||
本轮只扩展现有 `build/run-app-perf.cjs`、`tests/support/app-perf-driver.mjs` 的
|
||||
并发夹具,并通过已有 Preload/IPC/存储宿主/SQLite 生产入口执行。模型仍为本地
|
||||
回环确定性模型,数据目录和运行目录均由 `run-app-perf` 隔离在
|
||||
`temp/goodbuddy-concurrent-upgrade/`。构建命令为 `npm run build:bundle`;本轮没有
|
||||
修改核心存储或调度实现,也没有提交。
|
||||
|
||||
| 场景 | 实际结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| C03 混合存储 | 未满足 | `C03-current-r4/result.json` 记录知识导入耗时 4,307.193 ms,100 个重叠批次,100 次历史读取、100 次知识检索、100 次图谱读取,重叠窗口 3,784.373 ms。当前隔离数据没有已持久化监督结果,回顾发布批次为 0,因此没有把图谱读取冒充回顾发布;运行期间日志出现 `STORAGE_CAPACITY`。结果状态保留为 `not-fulfilled`。 |
|
||||
| C04 故障隔离 | 未满足 | 复核发现原夹具在请求观察器登记前允许点击取消,且 HTTP `request.close` 被误记为模型中止;现已改为等待请求登记、使用响应 `close` 判断异常中止,并等待健康 peer 真正进入和完成。`C04-final-local-r2/result.json`(本地确定性、Windows x64、Electron 44.5.1)记录取消后请求结算、查询和 Runtime status 响应、清理完成,健康 peer 用时 37,142 ms,但 `healthyPeerUnaffected: false`,且循环账本 4 次请求中没有 `fault-hung` 条目,因此不能声称挂起模型路径已完成。外部模型请求数为 0;未授权执行真实模型。 |
|
||||
| C07 冷/暖进入 | 未满足 | `C07-current-r1/result.json` 记录冷进入:输入 0.591 ms、历史 41.889 ms、监督概览 2.532 ms、图谱 2.244 ms、知识快照 2.555 ms;暖返回:输入 18.844 ms、历史 219.868 ms、图谱 10.757 ms、知识快照 2.062 ms。暖监督概览因 `STORAGE_CAPACITY` 无可用时刻;来源正文没有持久化来源,仍未测得。夹具状态为 `measured-with-gaps`,整体未满足。 |
|
||||
|
||||
所有补充运行均保存原始 `result.json` 和执行日志;模型边界仍为本地回环,外部请求数为 0。运行中观察到的存储容量错误及一次存储 Worker 退出均保留为当前候选的未满足项,不能以夹具已采集样本或构建成功覆盖。三轮交替、每项三轮 100 个有效交互样本、C03 回顾发布、C04 健康 peer 完成和 C07 来源正文时刻仍未完成。
|
||||
|
||||
## 2026-10-06 Portable 证据轻量复核
|
||||
|
||||
本次只读复核针对原始 `dist/GoodBuddy-windows-x64/data`、保留的
|
||||
`temp/goodbuddy-storage-upgrade/summary.json` 和当前仓库状态执行;没有启动 portable 原始目录,
|
||||
没有复制或写入原始数据库,也没有提交。保留证据的成功运行时间仍为
|
||||
`2026-10-05T12:03:32.624Z`,记录提交为 `4bb7573b10ecc46f746fbe04cf6b1e2c05647d1a`,
|
||||
且记录的工作树为未提交状态。当前 HEAD 为
|
||||
`edf019aaa728f094a433a2601d149d0ec9b28a21`,工作树仍有大量未提交修改,因此 portable 运行早于当前
|
||||
源码状态,不能把它当作当前 HEAD 的运行结果。
|
||||
|
||||
保留验证器 `node tests/support/portable-storage-upgrade.mjs --verify-source` 本次未通过:
|
||||
当前 `assistant.sqlite` 与保留基线的 SHA-256 和修改时间不同,当前 `knowledge.sqlite` 的修改时间不同,
|
||||
且保留基线中的四个 0 字节 WAL 与四个 SHM 当前已不存在。当前四个主库的字节数仍分别与记录一致;
|
||||
`conversation-attachments.sqlite` 和 `remote-runtime-bindings.sqlite` 的 SHA-256/修改时间仍一致,
|
||||
`knowledge.sqlite` 的 SHA-256 仍一致。当前 `notes` 仍为 50 个文件、431,056 字节,内容指纹与保留证据一致。
|
||||
因此原件相对本轮 portable 执行后的“当时未变”结论有后续漂移,不能宣称当前原始数据仍与 portable 运行基线完全相同。
|
||||
|
||||
对当前四个原始主库执行只读 SQLite `PRAGMA integrity_check` 均为 `ok`,`foreign_key_check` 均为 0 条,
|
||||
版本分别为 Assistant 62、Knowledge 13、Attachments 1、Runtime Bindings 0。这只证明当前文件的轻量结构完整性,
|
||||
不恢复 portable 运行与当前源码的一致性,也不替代一次新的隔离副本迁移验证。
|
||||
|
||||
清理复核确认 portable 验证使用的 `temp/goodbuddy-storage-upgrade/fresh-run`、`run` 目录均不存在,
|
||||
保留目录中只有日志、JSON 结果和源基线等证据文件;进程命令行未发现 portable 数据目录、验证脚本或
|
||||
`goodbuddy-storage-upgrade` 相关进程。机器上发现的已安装 GoodBuddy 进程使用独立用户数据目录,未计入本次
|
||||
portable 验证进程。上述结果只更新本实施记录,未修改原始目录或其他实现文件。
|
||||
|
||||
## 2026-10-06 简化与前后对照验收
|
||||
|
||||
### 简化
|
||||
|
||||
按“出现实测问题才加机制”复核并发修复期间新增的机制,删除以下实现(共约 400 行):
|
||||
|
||||
| 删除项 | 原因 | 替代 |
|
||||
| --- | --- | --- |
|
||||
| 自定义分帧传输 `desktop-storage-wire.ts` | 同机实测原生 `utilityProcess.postMessage` 传 32 MB 文本 126–139 ms、5 万行 71–85 ms,Main 最大延迟在 16 ms 计时精度内;自定义分帧分别为 249–271 ms、448–496 ms | 原生结构化克隆 |
|
||||
| 读/写/后台三类准入预算、交互保留槽 | 容量错误是“满了就拒绝”造成的,分类只是缓解 | 单一在途上限 32,超出按顺序等待,不再拒绝 |
|
||||
| 同键读取合并、存储进程侧重复准入、整值读取串行 | 客户端已限流,宿主再做一遍没有新约束 | 删除 |
|
||||
| 队列泵 `STORAGE_CAPACITY` 50 ms 重试 | 等待替代拒绝后不再出现容量错误 | 删除 |
|
||||
|
||||
另把存储进程启动移到建窗口之前,与窗口创建重叠。取消、提交后才确认、进程丢失时不重放、关闭前排空的语义不变;真实 Electron 存储夹具(含 16 MiB 视频、2,560 万字符历史)通过。
|
||||
|
||||
### 前后对照
|
||||
|
||||
基线为独立 worktree 的 `edf019aa`,候选为同一提交加当前工作树;同机交替运行,本地确定性模型,无外部请求。判定口径见[并发验收的发布门槛](./concurrent-desktop-acceptance.md#发布门槛与目标差距)。
|
||||
|
||||
| 场景 | 正确性门槛 | 对照结果(基线 → 候选,3 轮) |
|
||||
| --- | --- | --- |
|
||||
| 单会话 11 个场景 | — | 输入、切换、长会话打开、流式均在轮间波动内;Main 延迟 p99 在流式场景约 20.6 → 16.8 ms;启动至输入框可用中位数约慢 33 ms(1,300 → 1,333 ms),与轮间波动同量级 |
|
||||
| C01(3/4/8) | 两版均 3/3 通过:全部排空、0 错误 | 排空耗时一致(约 37 s/17.5 s);Main p99 约 21 → 16.7 ms |
|
||||
| C02(4/8/16) | 两版完成的轮次均通过 | 排空耗时一致(约 23 s/53 s);Main p99 约 22 → 16.7 ms |
|
||||
| C03 混合读取 | 两版均无回顾结果可发布,标未满足 | 100 批重叠读取窗口约 1,010 → 912 ms;基线知识导入在该夹具中报错,候选无错误 |
|
||||
| C04 故障隔离 | 两版均 3/3 通过:挂起请求取消结算,健康会话完成 | 取消观测约 135 → 101 ms,健康会话耗时一致 |
|
||||
| C06 5 分钟生命周期 | 两版均通过 | 每轮排空耗时一致;4 轮后堆 18.2 → 17.8 MB;另有候选 30 分钟运行 21 轮全部排空、0 错误,堆每轮约增 0.3 MB,与每轮新增 8 条持久化会话一致,结束时驱动退出出错,未记为通过 |
|
||||
| C07 冷/暖进入 | 无持久化来源,来源正文未测 | 冷进入各阶段 1–7 ms,两版一致;暖返回监督概览 38–45 → 25–38 ms |
|
||||
|
||||
旧版记录曾按产品子任务并发 3 判定 16 个子任务的测量;该数字没有用户设置或故障测量依据,已从生产默认配置移除。后续验收按实际 offered load 和实际运行数记录,不再用内部上限替代吞吐结果。夹具修正:排空等待改为 180 s(两版相同)、子任务按 `childTaskId` 统计、输入框焦点未就绪时重试输入。
|
||||
|
||||
结论:**正确性门槛在 C01/C02/C04/C06 两版都通过,候选在单会话和并发场景均未退化,Main 尾延迟有改善。** 剩余未满足项:C03 回顾发布与 C07 来源正文需要先有持久化回顾数据的夹具;C05 存储故障注入只有真实 Electron 存储夹具中的进程丢失、发送失败、取消覆盖,未做 App 级故障矩阵。输入 p95 约 8–9 ms,帧 p95 多数 8.5 ms,部分轮出现 16.5/58 ms,两版相同,记为目标差距。
|
||||
|
||||
### 真实数据与真实模型
|
||||
|
||||
portable 新副本(`--fresh`)当前源码复验通过:Assistant 库 175 万条事件、4,848 条消息,schema 62,所有已有表规范化摘要无变化,`integrity_check` 为 ok,外键违规 0;73 个附件、50 个笔记前后一致,原件 DB/WAL/SHM 指纹未变,副本已删除。100 批发布与读取重叠时,发布 p95 19 ms,夹具 Main 延迟 p99 16.4 ms。
|
||||
|
||||
真实模型输出分页首次走通:使用当前默认 `openai-responses` 配置,模型调用 1 次 `process_execute`、2 次 `output_read`,逐页游标连续至 EOF,恢复两个随机标记,**4 次外部请求**。之前两次失败的原因是该手动用例未注入输出正文存储(生产在 `index.ts` 注入),工具在模型分页前就报错;已改为注入与其他单测相同的文件存储。
|
||||
|
||||
### 全仓检查
|
||||
|
||||
`npm run typecheck`、`npm run lint` 通过。`npm test` 三次全量结果:1 失败(`workspace-import` 图片加载超时)、9 失败、1 失败(`local-runtime-reuse` 压缩返回 false),每次失败的用例不同,单独复跑均通过(`local-runtime-reuse` 3/3、`workspace-import` 4/4、其余 3 个文件 481/481)。判定为满载时的时序敏感用例,未放宽断言或超时,但不能记为全仓一次通过。
|
||||
|
||||
## 2026-10-06 有界真实模型复验
|
||||
|
||||
本节记录当前未提交源码的最终有界复验。没有启动 portable 原始数据目录,也没有把其中的会话、附件、笔记、日志或凭据发送给外部模型。所有发送给模型的监督输入均为脚本内合成内容;取消和输出分页场景也只使用脚本生成的短提示、临时工作区和随机标记。请求计数来自生产 Runtime 的 fetch 层或本地计数 relay,不记录请求正文、响应正文、请求头、端点或密钥。
|
||||
|
||||
| 路径 | 实际外部 HTTP 请求 | 结果 | 范围与边界 |
|
||||
| --- | ---: | --- | --- |
|
||||
| 生产存储宿主监督链 | 4 | 通过,4 次 HTTP 200 | `supervision-storage-live.cjs` 使用当前运行时设置读取凭据,在隔离 Electron、Assistant/Knowledge 存储宿主、生产监督工厂和 Heartbeat 入口中运行合成项目;故事、经验、建议发布成功,`no_change` 重跑新增 0 次,设置未改变,pending 为 0 |
|
||||
| 生产监督审核工厂 | 2 | 通过,完成 | `supervision-audit-validation.ts live` 使用当前 `ModelAgentRuntime`、生产监督服务和隔离 SQLite;1 条合成消息完成提取与故事归属,重跑新增 0 次,SQLite 完整性与外键检查通过 |
|
||||
| 直连 Runtime 取消 | 3 | 通过 | 现有 `cancelled-turn-live.test.ts` 的两个 direct-model 取消后续答复用本地计数 relay 执行;取消轮和后续请求均完成,取消内容没有并入下一轮,3 次请求均由 relay 计数 |
|
||||
| 直连 Runtime 输出分页/工具请求 | 2 | 未通过覆盖断言 | 现有 `runtime-e2e.manual.test.ts` 的生产源测试到达模型两次,但模型没有发出 `process_execute` 或 `output_read`,因此没有证明真实模型工具请求和分页闭环;该次仅计入实际请求,不把失败运行写成通过 |
|
||||
| 本地 OpenCode | 0 | 未执行 | 当前源码 checkout 没有 `.runtime-resources/win32-x64/opencode.exe`;没有改用已发布 portable 或其他缓存二进制 |
|
||||
| 本地 Continue | 0 | 未执行 | 当前源码 checkout 没有 `node_modules/@continuedev/cli/dist/cn.js`;没有改用已发布包 |
|
||||
| 共享 Linux Host | 0 | 未执行 | `192.168.0.23` 的 SSH 连接可达,但 BatchMode 连接在认证前由 Host 关闭;没有凭据交换或模型请求 |
|
||||
|
||||
本节合计观察到 **11 次** 外部模型 HTTP 请求:监督存储 4 次、监督审核 2 次、取消 3 次、输出分页失败尝试 2 次。没有重试被计入,也没有远程或 OpenCode/Continue 本地请求。监督链使用的运行时设置仅在隔离进程内解密,原设置字节保持不变。
|
||||
|
||||
验证结束后,隔离数据库、Electron profile、临时工作区、凭据副本、计数 relay 和生成的 probe 目录均已删除;进程检查没有发现属于这些运行的残留进程。该结果证明了当前源码的生产存储监督与直连取消路径,以及一次明确失败的真实模型工具分页尝试;不证明本地 OpenCode、Continue、远程 Linux Host、真实模型工具分页成功、完整桌面 UI、portable 数据副本或 C01–C07/A01–A10 验收已经完成。
|
||||
|
||||
## 2026-10-07 C03/C07 继续验收(当前 HEAD 1f85d0c0)
|
||||
|
||||
本次只修改了 `tests/support/app-perf-concurrent.mjs`、`tests/support/app-perf-driver.mjs` 和 `build/run-app-perf.cjs` 的验收夹具。运行目录为 `temp/goodbuddy-acceptance-continue/`,使用 `npm run build:bundle` 生成当前源码开发构建;每次运行都由 `build/run-app-perf.cjs` 启动隔离 Electron profile、存储 utility process 和 127.0.0.1 回环确定性模型。模型请求总数分别保留在原始 JSON 中,均为回环请求,未发送外部模型请求。
|
||||
|
||||
### C03 混合存储
|
||||
|
||||
命令为 `GB_PERF_MODE=C03 node build/run-app-perf.cjs`,最终证据为 `temp/goodbuddy-acceptance-continue/C03-final4/result.json`。夹具先通过生产 `conversations.saveLocal` IPC 保存 99 个会话、每个 20 条消息,再通过生产 `knowledge.createLibrary` 和 `knowledge.importPaths` 导入知识源;监督页通过真实导航和“回顾”按钮启动生产 `supervision.run`,确定性模型走生产 Runtime 流式接口。回顾提交完成后,结果通过生产 overview、activity 和 batches IPC 读取,首批分页返回的每组证据数为 20,提交进度为 100 批。
|
||||
|
||||
生产重叠窗口包含 100 次历史读取、100 次 Knowledge search 和 100 次监督 overview,Knowledge 导入耗时 7,190.102 ms,重叠窗口 6,701.095 ms。结果 `status=measured`,运行器最终状态为 `passed`;没有使用直接 SQL 写入或预置 review 表。C03 的通过条件仅断言回顾结果存在、100 批提交、抽取分页每批 20 条证据、导入已执行以及三类生产读取各 100 次并发生在导入窗口内。
|
||||
|
||||
### C07 冷热进入
|
||||
|
||||
命令为 `GB_PERF_MODE=C07 node build/run-app-perf.cjs`,最终证据为 `temp/goodbuddy-acceptance-continue/C07-passed/result.json`。冷进入和暖返回分别测量输入框、历史、监督概览、图谱和 Knowledge snapshot 的可用时刻;随后通过生产监督页触发一次确定性回顾,刷新监督页并打开图谱事件。夹具点击真实图谱事件的来源链接,等待 `.supervisor-workspace__source pre` 出现并读取其 DOM 文本长度;本次 `textLength=51`、`rendered=true`,因此来源正文是 UI 内容可用证据,不是 API 返回时间的替代。
|
||||
|
||||
两次运行均为 Windows x64、Electron 44.5.1、当前源码 dirty worktree,外部模型请求为 0。C03/C07 的测量状态通过;其余并发矩阵、三轮交替对照、C05 故障注入、完整预算判定和 A01–A10 仍按上文状态保留未完成,不能由本节扩展为 PERF-15/PERF-16 全项通过。
|
||||
@@ -1019,34 +1019,38 @@ Renderer 主线程降速 4 倍(Main 不降速,每次 reload 后重新应用
|
||||
|
||||
### PERF-15 存储接口异步化
|
||||
|
||||
2026-10-05 范围澄清:目标职责与迁移边界见[桌面存储方向](../architecture/desktop-storage-direction.md)。
|
||||
当前先实施[监督者及执行路径的 KISS 局部修复](../review/kiss-local-fixes-2026-10-05.md),
|
||||
完成、验证并提交后再讨论全面迁移。局部 Worker 复用不等于 PERF-15/16 已完成。
|
||||
2026-10-05 决策更新:[KISS 局部修复](../review/kiss-local-fixes-2026-10-05.md)已以 `b90ff50f` 提交。
|
||||
用户随后确定 PERF-15/PERF-16 及必要守护按一个完整架构候选交付,不拆成多轮局部产品交付。
|
||||
职责与 KISS 边界以[桌面存储与并发执行](../architecture/desktop-storage-direction.md)为准,
|
||||
多项目、多会话、多子任务的共同验收见[并发规范](../quality/concurrent-desktop-acceptance.md)。
|
||||
当前候选已完成存储宿主及主要生产组合,完整迁移验收尚未完成;以下编号保留为职责索引,不表示分别验收的发布阶段。实现状态汇总见[存储升级实施记录](../quality/storage-upgrade-implementation-2026-10-05.md)。
|
||||
|
||||
- **优先级 / 状态:** P1 / 进行中(热点读走 worker;高频写降为 NORMAL、checkpoint 移出 Main、启动恢复按索引,见 7.2.2;接口异步化未开始)
|
||||
- **范围:** 在原进程内把 `AssistantDatabase`、`KnowledgeDatabase` 等对外接口改为异步,
|
||||
调用方全部 `await`;会话列表只返回摘要,搜索下推到 SQL,活动记录改为增量追加。
|
||||
远程 Runtime 实时事件改用已有的 `appendRemoteTaskEventsBatch` 批量写入,不再逐条同步事务。
|
||||
Story Graph 检索先补测耗时,扫描规模设上限,匹配尽量下推到 SQL。
|
||||
- **验收:** 行为与测试不变;`PERF-11` Main event-loop 延迟不变差。
|
||||
- **优先级 / 状态:** P1 / 进行中(存储域接口和主要生产调用已异步化;完整调用清理、异常路径和 C01-C07/A01-A10 验收仍未完成)
|
||||
- **范围:** 为桌面业务存储建立异步领域接口,保留完整事务和先写后读顺序;包含 Assistant、
|
||||
Knowledge、附件、桌面 Runtime 绑定和本机原生客户端账本。跨边界调用方等待实际完成;同步缓存读取
|
||||
与内部纯计算无需变成 IPC。已完成的摘要、SQL 搜索、活动增量和远程事件批处理继续复用。
|
||||
- **验收:** 与 PERF-16 一起通过 SA-01~SA-07;只增加 Promise 或保留 Main 同步执行不计完成。
|
||||
|
||||
### PERF-16 数据进程
|
||||
|
||||
- **优先级 / 状态:** P1 / 待开始
|
||||
- **实施条件:** `PERF-15` 完成,调用方已全部异步。
|
||||
- **优先级 / 状态:** P1 / 进行中(`utilityProcess` 宿主已接入 Assistant、Knowledge、文件和 Runtime 域;全量生产路径与并发验收仍未完成)
|
||||
- **接口条件:** 每条跨进程业务操作具备异步调用及事务边界;与 PERF-15 在同一候选完成和验收,不单独交付半迁移状态。
|
||||
- **范围:** 目标为统一管理业务 SQLite 的 `utilityProcess`,承接会话、知识库、Story Graph、
|
||||
远程 Runtime 事件持久化和活动记录的业务事务;Main 只做校验与转发。重查询由受管只读 Worker
|
||||
分担,解析、OCR 与 Runtime 执行保持独立,每个数据库明确一个写入所有者;不合并现有数据库。
|
||||
职责、生命周期及 VS Code 对照以[桌面存储方向](../architecture/desktop-storage-direction.md)为准。
|
||||
- **验收:** 大知识库检索、会话列表和文档导入期间 Main event-loop 延迟达到批次目标;取消、
|
||||
崩溃恢复和迁移语义不回退。
|
||||
- **验收:** Main 无目标业务 SQLite 直接或间接访问、无同步回退;多项目/会话/子任务、重查询、
|
||||
导入及发布重叠时满足性能原则。完整矩阵、取消、故障、旧库和平台要求按并发验收执行。
|
||||
|
||||
### PERF-17 架构守护
|
||||
|
||||
- **优先级 / 状态:** P2 / 待开始
|
||||
- **范围:** lint 禁止 `src/main` 使用同步文件 API 和在数据进程外引用 `DatabaseSync`;禁止
|
||||
组件直接订阅 `window.goodbuddy.*.on*`;限制 `App.tsx` 规模;`PERF-11` 关键指标设置回归阈值。
|
||||
- **验收:** 违反规则的改动在 lint 或基准阶段失败。
|
||||
- **优先级 / 状态:** P2 / 部分实施(已有 lint 与历史回归上限;间接访问、单调收紧及并发性能检查待补)
|
||||
- **范围:** 按实际执行环境检查 Main 同步文件操作、直接和间接 SQLite 打开;存储宿主、受管 Worker、
|
||||
测试及远端 Agent 的合法连接按[并发验收的自动检查规则](../quality/concurrent-desktop-acceptance.md#自动检查与交付记录)列明,
|
||||
不按目录一概禁止。禁止组件直接订阅 `window.goodbuddy.*.on*`,限制 `App.tsx` 规模,
|
||||
为 `PERF-11` 及并发场景建立符合性能原则的检查。
|
||||
- **验收:** 违反规则的改动在 lint 或基准阶段失败;本次完整架构所需守护随同一候选交付。
|
||||
历史性能回归上限不能替代规范预算,无 CI/性能环境结果时不标记验收通过。
|
||||
|
||||
## 8. 体验与可访问性
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
@@ -132,6 +132,7 @@ export default defineConfig({
|
||||
input: {
|
||||
index: resolve('src/main/index.ts'),
|
||||
'assistant-storage-worker': resolve('src/main/assistant-storage-worker.ts'),
|
||||
'desktop-storage-entry': resolve('src/main/desktop-storage-entry.ts'),
|
||||
'readonly-query-worker': resolve('src/main/readonly-query-worker.ts'),
|
||||
'document-parse-worker': resolve('src/main/document-parse-worker.ts'),
|
||||
'wechat-sidecar': resolve(
|
||||
|
||||
+80
-32
@@ -12,9 +12,8 @@ import tseslint from 'typescript-eslint'
|
||||
// assigns it (data process, async fs, domain store, layout shell).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PERF-16: SQLite belongs to a dedicated data process (utilityProcess); Main
|
||||
// only routes messages. Files that imported DatabaseSync from 'node:sqlite'
|
||||
// (including `import type`) on 2026-10-02. Shrink only.
|
||||
// Concrete SQLite implementations, executed by storage owners or the remote Agent.
|
||||
// Type imports do not grant permission to execute SQLite.
|
||||
const databaseSyncAllowlist = [
|
||||
'src/agent-daemon/agent-model-gateway.ts',
|
||||
'src/agent-daemon/event-journal.ts',
|
||||
@@ -24,19 +23,83 @@ const databaseSyncAllowlist = [
|
||||
'src/main/agent/runtime-session-binding-store.ts',
|
||||
'src/main/assistant/assistant-database.ts',
|
||||
'src/main/assistant/assistant-storage-upgrade.ts',
|
||||
'src/main/assistant/story-graph-reader.ts',
|
||||
'src/main/assistant/subagent-progress-storage.ts',
|
||||
'src/main/assistant/supervision-experiences.ts',
|
||||
'src/main/assistant/supervision-review-store.ts',
|
||||
'src/main/assistant/supervision-stories.ts',
|
||||
'src/main/assistant/supervision-suggestions.ts',
|
||||
'src/main/assistant/supervision-timeline.ts',
|
||||
'src/main/conversation-attachment-storage.ts',
|
||||
'src/main/knowledge/external/external-knowledge-store.ts',
|
||||
'src/main/knowledge/knowledge-database.ts',
|
||||
'src/shared/node/private-sqlite-database.ts'
|
||||
]
|
||||
|
||||
// Explicit execution contexts, not a src/main or *worker* naming exemption.
|
||||
const sqliteExecutionContexts = [
|
||||
'src/agent-daemon/agent-owned-acp-prompt.ts',
|
||||
'src/agent-daemon/daemon.ts',
|
||||
'src/agent-daemon/direct-linux-stdio-process-owner.ts',
|
||||
'src/agent-daemon/index.ts',
|
||||
'src/agent-daemon/runtime-composition.ts',
|
||||
'src/main/desktop-storage-owner.ts',
|
||||
'src/main/desktop-storage-files.ts',
|
||||
'src/main/desktop-storage-runtime-operations.ts',
|
||||
'src/main/desktop-storage-entry.ts',
|
||||
'src/main/desktop-storage-runtime-host-fixture.ts',
|
||||
'src/main/readonly-query-worker.ts',
|
||||
'src/main/assistant-storage-worker.ts',
|
||||
'src/main/agent/private-sqlite-database.ts'
|
||||
]
|
||||
|
||||
// Match known module entry points, including the legacy opener re-export. This
|
||||
// guards imports rather than trying to infer arbitrary transitive call graphs.
|
||||
const sqliteValueImports = {
|
||||
'node:sqlite': ['DatabaseSync'],
|
||||
'private-sqlite-database': ['openPrivateSqliteDatabase', 'PreparedPrivateSqliteDatabaseFile'],
|
||||
'assistant-database': ['AssistantDatabase'],
|
||||
'knowledge-database': ['KnowledgeDatabase'],
|
||||
'external-knowledge-store': ['ExternalKnowledgeStore'],
|
||||
'conversation-attachment-storage': ['ConversationAttachmentStorage'],
|
||||
'runtime-session-binding-store': ['SqliteRuntimeSessionBindingStore'],
|
||||
'agent-model-gateway': ['AgentModelCallLedger'],
|
||||
'event-journal': ['EventJournal'],
|
||||
'runtime-owner-registry': ['RuntimeOwnerRegistry'],
|
||||
'semantic-prompt-store': ['SemanticPromptStore'],
|
||||
'assistant-storage-upgrade': ['getPendingAssistantStorageUpgrade', 'upgradeAssistantStorage'],
|
||||
'desktop-storage-owner': ['DesktopStorageOwner'],
|
||||
'desktop-storage-files': ['openDesktopStorageFiles'],
|
||||
'desktop-storage-runtime-operations': ['DesktopStorageRuntimeOwner']
|
||||
}
|
||||
const sqliteExecutionRule = {
|
||||
meta: { type: 'problem', schema: [], messages: {
|
||||
owner: 'SQLite value access belongs in an explicit storage owner, worker or remote Agent context (SA-07). Main must use the async storage interface; type imports are allowed.'
|
||||
} },
|
||||
create(context) {
|
||||
function check(node, source, specifiers) {
|
||||
if (node.importKind === 'type' || node.exportKind === 'type') return
|
||||
const path = source?.type === 'TemplateLiteral' && source.expressions.length === 0
|
||||
? source.quasis[0].value.cooked : source?.value
|
||||
if (typeof path !== 'string') return
|
||||
const name = path.split('/').at(-1).replace(/\.(?:[cm]?[jt]s)$/, '')
|
||||
const restricted = /(?:^|\/)agent-daemon(?:\/index(?:\.[cm]?[jt]s)?)?$/.test(path)
|
||||
? ['AgentModelCallLedger', 'EventJournal', 'RuntimeOwnerRegistry', 'SemanticPromptStore']
|
||||
: sqliteValueImports[name]
|
||||
if (!restricted) return
|
||||
if (!specifiers || specifiers.length === 0 || specifiers.some(specifier => {
|
||||
if (specifier.importKind === 'type' || specifier.exportKind === 'type') return false
|
||||
const imported = specifier.imported ?? specifier.local
|
||||
return !['ImportSpecifier', 'ExportSpecifier'].includes(specifier.type)
|
||||
|| restricted.includes(imported?.name ?? imported?.value)
|
||||
})) context.report({ node, messageId: 'owner' })
|
||||
}
|
||||
return {
|
||||
ImportDeclaration: node => check(node, node.source, node.specifiers),
|
||||
ExportNamedDeclaration: node => check(node, node.source, node.specifiers),
|
||||
ExportAllDeclaration: node => check(node, node.source),
|
||||
ImportExpression: node => check(node, node.source),
|
||||
CallExpression: node => {
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'require') check(node, node.arguments[0])
|
||||
},
|
||||
TSImportEqualsDeclaration: node => check(node, node.moduleReference.expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PERF-15/PERF-16 rule 1: Main must not block its event loop on file IO.
|
||||
// Synchronous fs functions; also caught as `fs.xxxSync` member access.
|
||||
const syncFsFunctions = [
|
||||
@@ -48,15 +111,9 @@ const syncFsFunctions = [
|
||||
'symlinkSync', 'truncateSync', 'unlinkSync', 'utimesSync', 'writeFileSync', 'writeSync', 'writevSync'
|
||||
]
|
||||
const syncFsMessage = 'Synchronous fs APIs block the Main event loop (PERF-17). Use node:fs/promises, or move the work to the data process (PERF-16).'
|
||||
const databaseSyncRestriction = [{
|
||||
name: 'node:sqlite',
|
||||
importNames: ['DatabaseSync'],
|
||||
message: 'DatabaseSync may only be used by the data process (PERF-16); other code must go through its async interface (PERF-15). The allowlist in eslint.config.js must only shrink.'
|
||||
}]
|
||||
const syncFsRestriction = ['node:fs', 'fs', 'original-fs'].map(name => ({ name, importNames: syncFsFunctions, message: syncFsMessage }))
|
||||
// Files under src/main (non-test) that used synchronous fs APIs on 2026-10-02. Shrink only.
|
||||
const mainSyncFsAllowlist = [
|
||||
'src/main/agent/paged-output-store.ts',
|
||||
'src/main/assistant/assistant-database.ts',
|
||||
'src/main/assistant/assistant-storage-upgrade.ts',
|
||||
'src/main/conversation-attachment-storage.ts',
|
||||
@@ -98,6 +155,7 @@ export default tseslint.config(
|
||||
'docs/**/vendor/**',
|
||||
'node_modules/**',
|
||||
'out/**',
|
||||
'temp/**',
|
||||
'shareserver/**'
|
||||
]
|
||||
},
|
||||
@@ -170,26 +228,16 @@ export default tseslint.config(
|
||||
}
|
||||
},
|
||||
// --- Architecture ratchets (PERF-17), see allowlists at the top. ---------
|
||||
// Rules a (DatabaseSync) and b (sync fs) share `no-restricted-imports`, and a
|
||||
// later flat-config block replaces an earlier block's options for the same
|
||||
// rule. The blocks below therefore partition the files so that each file
|
||||
// gets exactly the restrictions it is subject to.
|
||||
// DatabaseSync outside the data process (all of src, both value and type imports).
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
ignores: ['**/*.test.{ts,tsx}', ...databaseSyncAllowlist],
|
||||
rules: { 'no-restricted-imports': ['error', { paths: databaseSyncRestriction }] }
|
||||
files: ['src/**/*.{ts,tsx,js,mjs,cjs}'],
|
||||
ignores: ['**/*.test.{ts,tsx,js}', ...databaseSyncAllowlist, ...sqliteExecutionContexts],
|
||||
plugins: { architecture: { rules: { 'sqlite-execution-context': sqliteExecutionRule } } },
|
||||
rules: { 'architecture/sqlite-execution-context': 'error' }
|
||||
},
|
||||
// Synchronous fs in Main: files not allowlisted for either rule get both restrictions.
|
||||
// Synchronous fs in Main.
|
||||
{
|
||||
files: ['src/main/**/*.ts'],
|
||||
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist, ...databaseSyncAllowlist],
|
||||
rules: { 'no-restricted-imports': ['error', { paths: [...databaseSyncRestriction, ...syncFsRestriction] }] }
|
||||
},
|
||||
// Main files allowlisted for DatabaseSync but not for sync fs.
|
||||
{
|
||||
files: databaseSyncAllowlist.filter(file => file.startsWith('src/main/')),
|
||||
ignores: mainSyncFsAllowlist,
|
||||
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist],
|
||||
rules: { 'no-restricted-imports': ['error', { paths: syncFsRestriction }] }
|
||||
},
|
||||
// `fs.readFileSync(...)` style access through a namespace/default import.
|
||||
|
||||
Generated
+1559
-20
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.15.12",
|
||||
"version": "0.16.2",
|
||||
"private": true,
|
||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||
"desktopName": "GoodBuddy",
|
||||
@@ -20,6 +20,9 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"docs:dev": "vitepress dev sites/docs --config sites/docs/.vitepress/config.mts",
|
||||
"docs:build": "vitepress build sites/docs --outDir sites/docs-dist --config sites/docs/.vitepress/config.mts",
|
||||
"docs:preview": "vitepress preview sites/docs-dist",
|
||||
"start": "electron-vite preview",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.agent.json && tsc --noEmit -p tsconfig.web.json",
|
||||
"lint": "eslint .",
|
||||
@@ -419,6 +422,7 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^7.3.6",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# GoodBuddy Agent 0.15.6 更新内容
|
||||
|
||||
## 问题修复
|
||||
|
||||
- **配合桌面存储 owner 迁移**:远程 Runtime、工具、图片和历史路径继续使用既有 Agent 会话、权限和回放语义;本版包含与 Desktop 0.15.13 配套的 Runtime 调整。
|
||||
- **工具失败后继续请求**:直连模型在工具执行失败后可继续下一轮,分页输出和下一轮请求保持有界;取消、资源清理和错误上下文继续保留。
|
||||
|
||||
## 升级须知
|
||||
|
||||
先升级 **Desktop 0.15.13**,再下载此包并更新 Host。runtime/acp 6、wire 2.0、Node 24.19.0、OpenCode 1.18.29 和 Continue 1.5.47 保持不变。已有 Agent 进程不会热更新,请先处理活动任务。
|
||||
|
||||
# What's New in GoodBuddy Agent 0.15.6
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Support the desktop storage-owner migration**: Remote Runtime, tool, image, and history paths retain existing Agent session, permission, and replay semantics; this package contains the matching Runtime changes for Desktop 0.15.13.
|
||||
- **Continue after tool failures**: Direct-model requests can continue after tool execution failures while paged output and next-turn requests remain bounded. Cancellation, resource cleanup, and error context remain intact.
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
Upgrade to **Desktop 0.15.13** before downloading this package and updating the Host. runtime/acp 6, wire 2.0, Node 24.19.0, OpenCode 1.18.29, and Continue 1.5.47 remain unchanged. Running Agent processes are not hot-updated; handle active tasks first.
|
||||
@@ -1,6 +1,170 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.16.2",
|
||||
"releasedAt": "2026-10-10",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**支持全部时间的监督回顾**:监督者现在可以选择从最早保存的工作记录到当前时间的全部时间范围,适合查看长期积累的工作进展。",
|
||||
"**只在稳定后纳入监督回顾**:正在生成的消息和运行中的任务会等到完成后再进入回顾,避免回顾内容随着实时输出变化。"
|
||||
],
|
||||
"fixes": [
|
||||
"**让 Magic Notes 操作更容易理解**:笔记入口和操作按钮增加清晰的文字标签,更多操作在需要时显示,减少误解和界面干扰。",
|
||||
"**修复终端输入法布局**:在终端中输入中文或其他组合文字时,输入候选和组合区域会保持在终端边界内,不再撑开或遮挡周围界面。"
|
||||
],
|
||||
"notices": [
|
||||
"本版本首次启动会将数据库升级到 schema 63。升级前请关闭应用并备份完整数据目录、相邻 `notes/` 和设置文件;Agent 0.15.6 无需更新。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Review all time in Supervisor**: Supervisor now offers an all-time range from the earliest saved work records through the current time, making it easier to review long-running progress.",
|
||||
"**Review sources only after they settle**: Messages that are still streaming and tasks that are still running wait until completion before entering a review, so review content does not change underneath an active run."
|
||||
],
|
||||
"fixes": [
|
||||
"**Make Magic Notes actions easier to understand**: Note entry points and actions now use clearer text labels, while secondary actions appear when needed to reduce confusion and visual noise.",
|
||||
"**Contain terminal IME layout**: Chinese and other composition input in the terminal now stays inside the terminal bounds instead of expanding into or obscuring surrounding UI."
|
||||
],
|
||||
"notices": [
|
||||
"First launch upgrades the database to schema 63. Close the app and back up the complete data directory, adjacent `notes/`, and settings files before upgrading; Agent 0.15.6 does not need to be updated."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.16.1",
|
||||
"releasedAt": "2026-10-10",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**让消息自然延伸到输入框边缘**:修复聊天消息在输入框上方提前被实色区域截断的问题,现在消息会沿着输入框轮廓自然淡出,输入区和消息区的过渡更平滑。",
|
||||
"**改善输入框与消息区的主题适配**:调整浅色和深色主题下的渐变、背景和焦点边界,减少输入框上方出现粗边或明显横线的问题;滚动到底部按钮也会保持合适的可见位置。"
|
||||
],
|
||||
"notices": [
|
||||
"本版本不新增数据库 schema,Agent 0.15.6 无需更新。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**Let messages fade naturally to the composer edge**: Chat messages no longer stop at a solid band above the composer. They now fade along the composer outline for a smoother transition between the message area and input area.",
|
||||
"**Improve composer and message-area theme behavior**: Light and dark theme gradients, backgrounds, and focus boundaries are refined to reduce thick edges and visible horizontal bands above the composer; the scroll-to-bottom control also remains positioned for clear access."
|
||||
],
|
||||
"notices": [
|
||||
"This release adds no database schema version. Agent 0.15.6 does not need to be updated."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.16.0",
|
||||
"releasedAt": "2026-10-09",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**在输入框中选择专家或团队角色**:发送消息前可以直接在输入框顶部选择专家或团队角色,选择入口更接近实际发送操作。每个会话独立保存自己的选择,切换会话时不会误用其他会话的角色。",
|
||||
"**保留历史消息使用的角色**:已发送的回复会保留请求当时使用的专家或团队角色,即使之后重命名或删除该角色,历史记录仍能准确显示当时的使用情况。"
|
||||
],
|
||||
"fixes": [],
|
||||
"notices": [
|
||||
"本版本不新增数据库 schema,Agent 0.15.6 无需更新。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Choose an expert or team role from the composer**: Select an expert or team role directly at the top of the composer before sending a message. Each conversation keeps its own selection, so switching conversations does not accidentally carry over another conversation’s role.",
|
||||
"**Preserve the role used by historical messages**: Sent replies retain the expert or team role selected when the request was made. Later renaming or removing that role does not make existing history lose the context of how it was produced."
|
||||
],
|
||||
"fixes": [],
|
||||
"notices": [
|
||||
"This release adds no database schema version. Agent 0.15.6 does not need to be updated."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.15.14",
|
||||
"releasedAt": "2026-10-09",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**恢复历史监督回顾的继续操作**:对于旧版本遗留的“来源已变化或已移除”失败回顾,现在可以直接从监督活动中继续,而不必重新开始整个回顾。",
|
||||
"**让变化中的来源能够有界地重新纳入回顾**:来源在回顾期间发生变化时,系统会重新冻结并处理有限次数;持续变化的来源会被省略,避免回顾无法结束。已删除、归档或不再可用的来源会按现有规则安全省略,同时保留其余来源的进度。",
|
||||
"**改进监督活动状态**:监督活动继续保留暂停、失败、取消和已保存批次的状态,继续操作不会绕过项目范围和来源处理规则。",
|
||||
"**让设置分类更易识别**:设置页的分类现在显示对应图标,帮助用户更快找到外观、模型、Runtime、通道和其他设置。"
|
||||
],
|
||||
"notices": [
|
||||
"本版本不新增数据库 schema,继续使用 schema 62。监督回顾可能产生模型调用费用;持续变化的来源会被有限次数地重新处理,之后自动省略。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**Resume historical Supervisor reviews**: Reviews left in the legacy “source changed or was removed” failure state can now be continued directly from Supervisor Activity instead of restarting the entire review.",
|
||||
"**Re-review changing sources within a bound**: When a source changes during a review, GoodBuddy re-freezes and processes it for a limited number of attempts. A source that keeps changing is then omitted so the review can finish. Deleted, archived, or unavailable sources continue to be omitted under the existing rules while surviving progress is retained.",
|
||||
"**Improve Supervisor activity state**: Paused, failed, cancelled, and partially saved review states remain available, and resuming a review continues to follow the project-scope and source-processing rules.",
|
||||
"**Make settings categories easier to scan**: Settings categories now show matching icons, helping users find Appearance, Models, Runtime, Channels, and other settings more quickly."
|
||||
],
|
||||
"notices": [
|
||||
"This release adds no database schema version and remains on schema 62. Supervisor reviews may use model calls; continuously changing sources are reprocessed a limited number of times and then omitted."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.15.13",
|
||||
"releasedAt": "2026-10-07",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**把桌面业务 SQLite 交给独立存储进程**:生产路径现在由一个 storage utility process 统一拥有业务数据库,Assistant、Knowledge、附件、运行绑定和账本等域通过类型化异步操作访问。保留原有事务、顺序、提交后确认和数据路径;进程异常、关闭排空和就绪失败会显式反馈,不使用第二个 Main 同步写入者。",
|
||||
"**让长输出和工具失败后的对话继续可用**:直连模型在工具执行失败后可继续请求,分页输出保持有界并保留下一轮所需正文;桌面和远程 Runtime 的工具、图片、知识和历史绑定继续沿用原有权限和会话隔离。Agent 0.15.6 包含配套 Runtime 调整,需与 Desktop 一起升级。"
|
||||
],
|
||||
"fixes": [
|
||||
"**减少存储迁移中的重复工作与等待**:监督、知识、附件和运行时访问复用同一存储 owner,读取 Worker 仍使用有界快照;监督来源、图谱、摘要和文件操作不再分别创建业务数据库 owner。utility IPC 对请求、取消、容量、进程退出和提交结果有明确边界,未完成的全量并发验收仍不宣称通过。",
|
||||
"**改进 Magic Notes 标签和项目设置入口**:标签管理保留空的全局标签、重复标签错误保留原始原因;直接打开设置路由时预加载对应设置,文件列表元数据更简洁,聊天 Composer 过渡更平滑。",
|
||||
"**放宽监督者可识别的来源别名**:历史来源使用短引用或旧主键时,生产监督流程可通过已知映射解析,不因非 UUID 的历史标识直接拒绝有效来源。删除会话、暂停、重试和项目范围规则继续生效。",
|
||||
"**移除未被产品支持的并发限制假设**:存储和并发代码不再依赖未经支持的队列容量或 Worker 队列上限;明确的资源边界、取消、排空和错误传播仍保留,未将目标架构文档中的性能预算写成已达标结论。"
|
||||
],
|
||||
"notices": [
|
||||
"本轮不新增数据库 schema 版本,沿用 schema 62;升级前仍建议关闭应用并备份完整数据目录、相邻 `notes/` 和设置文件。存储 owner 迁移不改变数据库路径、表、ID、事务语义或远程 Agent 数据库。",
|
||||
"远程 Runtime 路径有生产代码变化,请先升级 Desktop 0.15.13,再下载 Agent 0.15.6 并更新 Host;已有进程不会热更新。runtime/acp 6、wire 2.0、Electron、Node、OpenCode 和 Continue 固定版本不变。",
|
||||
"并发验收规范要求的 C01–C07 全矩阵、跨平台存储进程安装和低端机性能对照仍是后续验收项。本轮 focused storage/监督测试、类型检查和 lint 证据不替代这些完整验收。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Move desktop business SQLite ownership to a utility process**: Production paths now use one storage utility process as the business-database owner, with typed asynchronous operations for Assistant, Knowledge, attachments, runtime bindings, and ledgers. Existing transactions, ordering, commit-before-confirmation, and data paths remain; process failure, drain-on-close, and readiness errors are explicit, with no second synchronous Main writer.",
|
||||
"**Keep conversations usable after long output and tool failures**: Direct models can continue after tool execution failures, while paged output remains bounded and retains the content needed by the next request. Desktop and remote Runtimes retain existing tool, image, knowledge, history, permission, and session isolation; Agent 0.15.6 contains the matching Runtime changes and should be upgraded with Desktop."
|
||||
],
|
||||
"fixes": [
|
||||
"**Reduce duplicate work and waits during storage migration**: Supervisor, Knowledge, attachments, and Runtime access reuse one storage owner, while read Workers retain bounded snapshots; review sources, graphs, summaries, and file operations no longer create separate business-database owners. Utility IPC defines request, cancellation, capacity, process-exit, and committed-result boundaries; the full concurrency matrix is not claimed complete.",
|
||||
"**Improve Magic Notes tags and direct settings entry**: Tag management preserves empty global tags and original causes for duplicate-tag errors; direct settings routes preload their target settings, file-list metadata is simpler, and the transition above the chat composer is smoother.",
|
||||
"**Accept more historical Supervisor source aliases**: Production review flows resolve short references and known legacy keys through the existing mapping instead of rejecting valid historical sources solely because they are not UUIDs. Conversation deletion, pause, retry, and project-scope rules remain active.",
|
||||
"**Remove unsupported assumptions about concurrency limits**: Storage and concurrency code no longer relies on unsupported queue-capacity or Worker-queue limits. Explicit resource bounds, cancellation, draining, and error propagation remain, and target-architecture performance budgets are not presented as achieved."
|
||||
],
|
||||
"notices": [
|
||||
"This release adds no database schema version and remains at schema 62; close the app and back up the complete data directory, adjacent `notes/`, and settings files before upgrading. Storage ownership does not change database paths, tables, IDs, transaction semantics, or remote Agent databases.",
|
||||
"The remote Runtime path changed in production code. Upgrade to Desktop 0.15.13 first, then download Agent 0.15.6 and update the Host; running processes are not hot-updated. runtime/acp 6, wire 2.0, and pinned Electron, Node, OpenCode, and Continue versions remain unchanged.",
|
||||
"The C01–C07 concurrency matrix, cross-platform storage-process installation, and low-end performance comparisons remain follow-up acceptance work. Focused storage/Supervisor tests, type checks, and lint do not replace that complete validation."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.15.12",
|
||||
"releasedAt": "2026-10-05",
|
||||
|
||||
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { parseEnv } from 'node:util'
|
||||
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
|
||||
import { extractExperiences } from '../src/main/assistant/supervision-experiences.ts'
|
||||
import { extractExperiences } from '../src/main/assistant/experience-extraction-service.ts'
|
||||
|
||||
const [prepared, envPath, directory, maxCallsArg] = process.argv.slice(2)
|
||||
const config = parseEnv(readFileSync(envPath, 'utf8'))
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/* global process, console, setTimeout, clearTimeout */
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const script = fileURLToPath(import.meta.url)
|
||||
const root = resolve(dirname(script), '..')
|
||||
const base = join(root, 'temp', 'goodbuddy-final-tests-final')
|
||||
const stateFile = join(base, 'status.json')
|
||||
const lockFile = join(base, 'runner.lock')
|
||||
const npm = join(root, 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
||||
const [mode = 'status', selection = 'all', ...filters] = process.argv.slice(2)
|
||||
const alive = (pid) => {
|
||||
try { process.kill(pid, 0); return true } catch { return false }
|
||||
}
|
||||
const readState = () => JSON.parse(readFileSync(stateFile, 'utf8'))
|
||||
const killTree = (pid) => {
|
||||
if (!pid || !alive(pid)) return
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, timeout: 15000 })
|
||||
} else {
|
||||
try { process.kill(-pid, 'SIGKILL') } catch { /* Already exited. */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'start') {
|
||||
if (!['all', 'test', 'typecheck', 'lint'].includes(selection)) throw new Error('Unknown validation selection')
|
||||
if (!statSync(join(root, 'temp')).isDirectory()) throw new Error('Repository temp directory is required')
|
||||
mkdirSync(base, { recursive: true })
|
||||
if (existsSync(lockFile)) {
|
||||
const pid = Number(readFileSync(lockFile, 'utf8'))
|
||||
if (alive(pid)) throw new Error(`Validation runner already active: ${pid}`)
|
||||
rmSync(lockFile)
|
||||
}
|
||||
const lock = openSync(lockFile, 'wx')
|
||||
writeFileSync(lock, String(process.pid))
|
||||
closeSync(lock)
|
||||
const log = openSync(join(base, 'runner.log'), 'a')
|
||||
try {
|
||||
const child = spawn(process.execPath, [script, 'worker', selection, ...filters], {
|
||||
cwd: root, detached: true, windowsHide: true, stdio: ['ignore', log, log]
|
||||
})
|
||||
await new Promise((resolveSpawn, reject) => {
|
||||
child.once('spawn', resolveSpawn)
|
||||
child.once('error', reject)
|
||||
})
|
||||
writeFileSync(lockFile, String(child.pid))
|
||||
child.unref()
|
||||
console.log(JSON.stringify({ runnerPid: child.pid, selection, logs: base }))
|
||||
} catch (error) {
|
||||
rmSync(lockFile, { force: true })
|
||||
throw error
|
||||
} finally { closeSync(log) }
|
||||
} else if (mode === 'status') {
|
||||
const state = readState()
|
||||
console.log(JSON.stringify({ ...state, runnerAlive: alive(state.pid) }, null, 2))
|
||||
} else if (mode === 'stop') {
|
||||
const state = readState()
|
||||
if (state.status === 'running') {
|
||||
killTree(state.childPid)
|
||||
killTree(state.pid)
|
||||
state.status = 'stopped'
|
||||
state.endedAt = new Date().toISOString()
|
||||
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||
rmSync(lockFile, { force: true })
|
||||
}
|
||||
} else if (mode === 'worker') {
|
||||
const run = join(base, new Date().toISOString().replace(/[:.]/g, '-'))
|
||||
const tmp = join(run, 'tmp')
|
||||
mkdirSync(tmp, { recursive: true })
|
||||
if (!statSync(tmp).isDirectory()) throw new Error('Child temp directory was not created')
|
||||
const state = { pid: process.pid, status: 'running', startedAt: new Date().toISOString(), run, results: [] }
|
||||
const save = () => writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||
let child
|
||||
let interrupted = false
|
||||
const interrupt = (reason) => {
|
||||
interrupted = true
|
||||
state.reason = reason
|
||||
killTree(child?.pid)
|
||||
}
|
||||
process.on('SIGINT', () => interrupt('SIGINT'))
|
||||
process.on('SIGTERM', () => interrupt('SIGTERM'))
|
||||
process.on('exit', () => killTree(child?.pid))
|
||||
const deadline = setTimeout(() => interrupt('30-minute overall deadline exceeded'), 30 * 60 * 1000)
|
||||
save()
|
||||
try {
|
||||
for (const job of selection === 'all' ? ['test', 'typecheck', 'lint'] : [selection]) {
|
||||
if (interrupted) break
|
||||
const started = Date.now()
|
||||
const report = join(run, 'tests.json')
|
||||
const args = job === 'test'
|
||||
? ['test', '--', ...filters, '--reporter=default', '--reporter=json', `--outputFile.json=${report}`]
|
||||
: ['run', job]
|
||||
const logPath = join(run, `${job}.log`)
|
||||
const log = openSync(logPath, 'w')
|
||||
try {
|
||||
child = spawn(process.execPath, [npm, ...args], {
|
||||
cwd: root, windowsHide: true, detached: process.platform !== 'win32',
|
||||
env: { ...process.env, TEMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMPDIR: join(tmpdir(), 'goodbuddy-final-tests-final'), NO_COLOR: '1' },
|
||||
stdio: ['ignore', log, log]
|
||||
})
|
||||
state.active = job
|
||||
state.childPid = child.pid
|
||||
save()
|
||||
const result = await new Promise((resolveExit, reject) => {
|
||||
child.once('error', reject)
|
||||
child.once('exit', (code, signal) => resolveExit({ code, signal }))
|
||||
})
|
||||
const entry = { job, ...result, seconds: (Date.now() - started) / 1000, log: logPath }
|
||||
if (job === 'test' && existsSync(report)) {
|
||||
const data = JSON.parse(readFileSync(report, 'utf8'))
|
||||
entry.totals = Object.fromEntries(Object.entries(data).filter(([key]) => key.startsWith('num') || key === 'success'))
|
||||
entry.failedFiles = data.testResults.filter((file) => file.status === 'failed').map((file) => ({
|
||||
file: file.name.replace(root, '').replaceAll('\\', '/'),
|
||||
tests: file.assertionResults.filter((test) => test.status === 'failed').map((test) => test.fullName)
|
||||
}))
|
||||
}
|
||||
state.results.push(entry)
|
||||
} finally {
|
||||
killTree(child?.pid)
|
||||
child = undefined
|
||||
closeSync(log)
|
||||
delete state.childPid
|
||||
delete state.active
|
||||
save()
|
||||
}
|
||||
}
|
||||
state.status = interrupted ? 'interrupted' : 'completed'
|
||||
} catch (error) {
|
||||
state.status = 'error'
|
||||
state.reason = error.message
|
||||
} finally {
|
||||
clearTimeout(deadline)
|
||||
killTree(child?.pid)
|
||||
try { rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }) }
|
||||
catch (error) { state.cleanupError = error.code }
|
||||
state.endedAt = new Date().toISOString()
|
||||
save()
|
||||
rmSync(lockFile, { force: true })
|
||||
}
|
||||
} else {
|
||||
throw new Error('Usage: node scripts/final-regression.mjs start [all|test|typecheck|lint] [test filters...] | status | stop')
|
||||
}
|
||||
@@ -86,7 +86,7 @@ try {
|
||||
'CANDIDATES:', JSON.stringify(request.candidates), 'Return only JSON.'].join('\n\n')),
|
||||
{ supervisionRunId, heartbeatRunId: run.id })
|
||||
})
|
||||
const plan = heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
|
||||
const plan = await heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
|
||||
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
|
||||
let tick = 0
|
||||
const due = async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { parseEnv } from 'node:util'
|
||||
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
|
||||
import { assignStories } from '../src/main/assistant/supervision-stories.ts'
|
||||
import { assignStories } from '../src/main/assistant/story-assignment-service.ts'
|
||||
|
||||
const [prepared, envPath, directory, mode, maxCallsArg] = process.argv.slice(2)
|
||||
const config = parseEnv(readFileSync(envPath, 'utf8'))
|
||||
|
||||
@@ -12,6 +12,8 @@ import { SupervisionModelPool } from '../src/main/assistant/supervision-model-po
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
|
||||
import { supervisionReviewSettingsSchema } from '../src/shared/supervision-review-contracts'
|
||||
import { ReadonlyQueryReader } from '../src/main/readonly-query-reader'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [mode, parent, envPath] = process.argv.slice(2)
|
||||
@@ -25,7 +27,8 @@ async function main() {
|
||||
bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
|
||||
const db = new AssistantDatabase(path)
|
||||
db.initialize(directory)
|
||||
db.enableReadonlyWorker(worker)
|
||||
const reader = new ReadonlyQueryReader('assistant', path, worker)
|
||||
const storage = asyncSupervisionStorage(db, undefined, reader).supervision
|
||||
const sql = (db as unknown as { requireDatabase(): DatabaseSync }).requireDatabase()
|
||||
const pool = new SupervisionModelPool()
|
||||
const report: Record<string, unknown> = { mode, directory, externalCalls: 0 }
|
||||
@@ -49,7 +52,7 @@ async function main() {
|
||||
const counts = () => ['supervision_entities', 'supervision_events', 'supervision_event_entities', 'supervision_sources'].map(table =>
|
||||
[table, Number(sql.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()!.n)])
|
||||
const before = counts()
|
||||
await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
|
||||
await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
|
||||
const legacy = sql.prepare(`SELECT e.id FROM supervision_entities e WHERE e.confirmation_state != 'revoked' AND EXISTS (
|
||||
SELECT 1 FROM supervision_event_entities ee JOIN supervision_events ev ON ev.id = ee.event_id
|
||||
WHERE ee.entity_id = e.id AND ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?)
|
||||
@@ -61,14 +64,14 @@ async function main() {
|
||||
const legacyMs = performance.now() - legacyStart
|
||||
const start = performance.now()
|
||||
for (const project of projects) {
|
||||
const candidates = await db.supervisionCandidates(request, { projectId: project.id, crossProject: false })
|
||||
const candidates = await storage.supervisionCandidates(request, { projectId: project.id, crossProject: false })
|
||||
assert.equal(candidates.length, 100)
|
||||
assert(candidates.every(item => item.label.startsWith(project.id + ':')))
|
||||
assert.deepEqual(candidates.map(item => item.id), expected.get(project.id))
|
||||
}
|
||||
const isolatedMs = performance.now() - start
|
||||
const crossStart = performance.now()
|
||||
const candidates = await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
|
||||
const candidates = await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
|
||||
assert.equal(candidates.length, 100)
|
||||
rounds.push({ legacyMs, isolatedMs, crossProjectMs: performance.now() - crossStart })
|
||||
}
|
||||
@@ -106,7 +109,7 @@ async function main() {
|
||||
calls++
|
||||
return fetch(input, init)
|
||||
} })
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
|
||||
const service = createProductionSupervisorService(storage, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
|
||||
supervisionReview: supervisionReviewSettingsSchema.parse({ crossProject: false, experienceMinEvents: 200 }) }), async () => runtime, pool)
|
||||
const result = await service.run(request)
|
||||
assert.equal(result.status, 'completed')
|
||||
@@ -133,6 +136,7 @@ async function main() {
|
||||
} finally {
|
||||
report.externalCalls = calls
|
||||
report.elapsedMs = performance.now() - started
|
||||
await reader.close()
|
||||
db.close()
|
||||
pool.dispose()
|
||||
report.databaseBytes = (await stat(path)).size
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [directory, envPath, mode] = process.argv.slice(2)
|
||||
@@ -68,7 +69,7 @@ async function main() {
|
||||
const candidates = db.listSupervisionCandidates(state.request)
|
||||
writeFileSync(join(directory, 'candidates.private.json'), JSON.stringify(candidates))
|
||||
metrics.originalCandidates = candidates.length
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
|
||||
const originalStore = db.supervisionReviewStore.bind(db)
|
||||
const save = store.save.bind(store)
|
||||
store.save = (...args) => { save(...args); service.pause(incident.run.id) }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import type { AgentRuntime } from '../src/main/agent/runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [sourcePath, destinationPath, runId] = process.argv.slice(2)
|
||||
@@ -37,7 +38,7 @@ async function main() {
|
||||
const leaves = store.batches(runId, 20)
|
||||
assert.equal(leaves.length, 6)
|
||||
assert.equal(store.progress(runId).remainingSources, 0)
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1 }),
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1 }),
|
||||
async () => ({ runtimeId: 'offline-replay', capability: 'chat', run } as AgentRuntime), pool)
|
||||
const result = await service.resume(runId)
|
||||
assert.equal(result.status, 'completed')
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [sourceArg, outputArg, envArg, mode] = process.argv.slice(2)
|
||||
@@ -79,7 +80,7 @@ async function main() {
|
||||
title: 'Private selected conversation', updatedAt: Date.parse(scope.lastAt) },
|
||||
messages: messages.map(message => ({ id: message.id, content: message.content, role: message.role,
|
||||
createdAt: Date.parse(message.created_at), state: 'complete' })) }])
|
||||
const service = createProductionSupervisorService(db, async () => ({ heartbeatEnabled: true,
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ heartbeatEnabled: true,
|
||||
supervisorModelConcurrency: 2, supervisorOrganizeTimeoutSeconds: 180,
|
||||
supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 20, executionSeconds: 600 }
|
||||
}), async () => runtime, pool)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* global require, process, __dirname, console, setTimeout, clearTimeout */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
// node scripts/supervision-storage-live.cjs <current-runtime-settings.json>
|
||||
const { build } = require('esbuild')
|
||||
const { spawn } = require('node:child_process')
|
||||
const { mkdir, mkdtemp, rm } = require('node:fs/promises')
|
||||
const { join, resolve } = require('node:path')
|
||||
async function main() {
|
||||
const repo = resolve(__dirname, '..')
|
||||
if (!process.argv[2]) throw new Error('Runtime settings path required')
|
||||
const parent = join(repo, 'temp', 'supervision-storage-live')
|
||||
await mkdir(parent, { recursive: true })
|
||||
const root = await mkdtemp(join(parent, 'probe-'))
|
||||
try {
|
||||
for (const [entry, output, format] of [
|
||||
['scripts/supervision-storage-live.ts', 'main.mjs', 'esm'],
|
||||
['src/main/desktop-storage-entry.ts', 'desktop-storage-entry.mjs', 'esm'],
|
||||
['src/main/readonly-query-worker.ts', 'readonly-query-worker.cjs', 'cjs'],
|
||||
['src/main/assistant-storage-worker.ts', 'assistant-storage-worker.cjs', 'cjs']
|
||||
]) await build({ entryPoints: [join(repo, entry)], outfile: join(root, output), bundle: true,
|
||||
platform: 'node', format, packages: 'external', logLevel: 'silent' })
|
||||
const env = { ...process.env, GB_SUPERVISION_STORAGE_ROOT: root, GB_SUPERVISION_SETTINGS: resolve(process.argv[2]), TEMP: root, TMP: root, TMPDIR: root }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
const child = spawn(require('electron'), [join(root, 'main.mjs')], { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
child.stdout.on('data', data => process.stdout.write(data))
|
||||
// The probe prints only bounded metrics; native stderr is not a credential-safe report.
|
||||
child.stderr.resume()
|
||||
const timer = setTimeout(() => child.kill(), 300_000)
|
||||
try { process.exitCode = (await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })) ?? 1 }
|
||||
finally { clearTimeout(timer) }
|
||||
} finally { await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }
|
||||
}
|
||||
main().catch(() => { console.error('Supervision storage probe failed before completion'); process.exitCode = 1 })
|
||||
@@ -0,0 +1,134 @@
|
||||
// Run through supervision-storage-live.cjs. Only synthetic evidence is sent to the model.
|
||||
import assert from 'node:assert/strict'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { readFile, writeFile, rm } from 'node:fs/promises'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { RuntimeSettingsStore } from '../src/main/runtime-settings-store'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { DesktopStorageClient } from '../src/main/desktop-storage-client'
|
||||
import { createSupervisionDomainPorts } from '../src/main/assistant/supervision-domain-ports'
|
||||
import { createProductionSupervisorService, createProductionSuggestionPhraser } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { HeartbeatService } from '../src/main/assistant/heartbeat-service'
|
||||
import { deriveSuggestions } from '../src/main/assistant/supervision-suggester'
|
||||
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
|
||||
|
||||
const root = process.env.GB_SUPERVISION_STORAGE_ROOT!
|
||||
const settingsPath = process.env.GB_SUPERVISION_SETTINGS!
|
||||
const calls: Array<{ status?: number; inputBytes: number; ms?: number }> = []
|
||||
const report: Record<string, unknown> = { syntheticOnly: true, maximumHttpCalls: 4, calls }
|
||||
async function run() {
|
||||
report.stage = 'preflight'
|
||||
assert(root && settingsPath)
|
||||
const profileDirectory = join(root, 'electron')
|
||||
// Electron must see the copied encryption state before its ready event.
|
||||
mkdirSync(profileDirectory)
|
||||
app.setPath('userData', profileDirectory)
|
||||
if (process.platform === 'win32') {
|
||||
const state = JSON.parse(readFileSync(join(dirname(settingsPath), 'Local State'), 'utf8'))
|
||||
writeFileSync(join(profileDirectory, 'Local State'), JSON.stringify({ os_crypt: state.os_crypt }))
|
||||
}
|
||||
await app.whenReady()
|
||||
const original = await readFile(settingsPath)
|
||||
const snapshot = join(root, 'settings.snapshot.json')
|
||||
await writeFile(snapshot, original)
|
||||
const settings = await new RuntimeSettingsStore(snapshot, {
|
||||
isAvailable: () => safeStorage.isEncryptionAvailable(),
|
||||
encrypt: () => { throw new Error('Probe does not write credentials') },
|
||||
decrypt: value => safeStorage.decryptString(value)
|
||||
}, {}).getResolvedSettings().finally(() => rm(snapshot, { force: true }))
|
||||
const application = JSON.parse(await readFile(join(dirname(settingsPath), 'application-settings.json'), 'utf8'))
|
||||
const selectedId = application.supervisorModelProfileId ?? settings.defaultModelProfileId
|
||||
const selected = settings.modelProfiles.find(profile => profile.id === selectedId)
|
||||
report.selection = { profileCount: settings.modelProfiles.length, explicitSupervisor: Boolean(application.supervisorModelProfileId),
|
||||
found: Boolean(selected), protocol: selected?.protocol, authentication: selected?.authentication, credentialAvailable: Boolean(selected?.apiKey) }
|
||||
assert(selected && selected.protocol !== 'openai-images-generations', 'No configured supervisor text model')
|
||||
assert(selected.authentication === 'none' || selected.apiKey, 'No configured credential')
|
||||
report.model = selected.modelName
|
||||
report.protocol = selected.protocol
|
||||
const storage = new DesktopStorageClient({ assistantPath: join(root, 'assistant.sqlite'), knowledgePath: join(root, 'knowledge.sqlite'),
|
||||
userDataPath: root, defaultRootPath: root, entryPath: join(root, 'desktop-storage-entry.mjs'),
|
||||
readerWorkerPath: join(root, 'readonly-query-worker.cjs'), upgradeWorkerPath: join(root, 'assistant-storage-worker.cjs') })
|
||||
const pool = new SupervisionModelPool()
|
||||
const runtime = () => new ModelAgentRuntime({ protocol: selected.protocol, authentication: selected.authentication,
|
||||
apiKey: selected.apiKey, baseUrl: selected.baseUrl, model: selected.modelName,
|
||||
requestHeaders: selected.requestHeaders, requestBody: selected.requestBody,
|
||||
maximumOutputTokens: 3000, defaultWorkspace: root,
|
||||
toolProvider: { listTools: async () => [], getApproval: () => { throw new Error('Probe tools disabled') },
|
||||
callTool: async () => { throw new Error('Probe tools disabled') }, releaseConversation: async () => {}, dispose: async () => {} },
|
||||
fetcher: async (input, init) => {
|
||||
assert(calls.length < 4, 'Probe HTTP capacity reached')
|
||||
const body = JSON.parse(String(init?.body))
|
||||
assert.equal(body.tools?.length ?? 0, 0)
|
||||
const call: (typeof calls)[number] = { inputBytes: Buffer.byteLength(String(init?.body)) }
|
||||
calls.push(call)
|
||||
console.log(JSON.stringify({ event: 'model-request', number: calls.length }))
|
||||
const start = performance.now()
|
||||
const response = await fetch(input, { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(60_000)]) })
|
||||
call.status = response.status
|
||||
call.ms = Math.round(performance.now() - start)
|
||||
return response
|
||||
} })
|
||||
try {
|
||||
report.stage = 'storage-ready'
|
||||
await storage.ready
|
||||
const ports = createSupervisionDomainPorts(storage)
|
||||
const project = (await storage.call('assistant', 'listProjects', []))[0]!
|
||||
const now = Date.now()
|
||||
const messages = [
|
||||
'Synthetic project Aurora maintains a weather sensor calibration module. Decision: calibrate against a reference sensor before each field deployment. This replaced the earlier plan to calibrate only after deployment.',
|
||||
'Milestone: the pre-deployment reference calibration detected a two-degree offset, which we corrected before deployment. The field measurements then matched the reference. Lesson: verify calibration before deployment to prevent biased field data.',
|
||||
'Open question for the calibration module: should routine recalibration happen monthly or quarterly? No interval has been decided.'
|
||||
].map((content, index) => ({ id: randomUUID(), role: 'user' as const, content, state: 'complete' as const, createdAt: now + index }))
|
||||
await storage.call('assistant', 'saveLocalConversations', [[{
|
||||
header: { id: randomUUID(), projectId: project.id, title: 'Synthetic Aurora calibration', updatedAt: now + 2 }, messages
|
||||
}]])
|
||||
const request: SupervisionRunRequest = { trigger: 'heartbeat', scope: { kind: 'global' },
|
||||
timeRange: { from: new Date(now - 1000).toISOString(), to: new Date(now + 1000).toISOString() } }
|
||||
const getSettings = async () => ({ supervisorModelProfileId: selectedId, supervisorOrganizeTimeoutSeconds: 60,
|
||||
supervisorModelConcurrency: 1, supervisionReview: { pageSize: 10, batchCharacters: 8000, batchMessages: 10,
|
||||
executionSeconds: 300, experienceMinEvents: 2, storyThreadEvents: 20 } })
|
||||
const supervisor = createProductionSupervisorService(ports.supervision, getSettings, async () => runtime(), pool)
|
||||
const phrase = createProductionSuggestionPhraser(ports.supervision, getSettings, async () => runtime(), pool)
|
||||
const heartbeat = new HeartbeatService(ports.heartbeat, {
|
||||
review: async ({ run }) => {
|
||||
const result = await supervisor.run(request, run.id)
|
||||
assert.equal(result.status, 'completed')
|
||||
assert.equal(result.coverage?.complete, true)
|
||||
assert.equal(result.coverage?.stories?.status, 'completed')
|
||||
report.coverage = result.coverage
|
||||
return { status: result.status, runId: result.runId }
|
||||
},
|
||||
suggest: async ({ run, supervisionRunId }) => deriveSuggestions(ports.suggestions, phrase, { supervisionRunId, heartbeatRunId: run.id })
|
||||
})
|
||||
const config = await heartbeat.create({ name: 'Synthetic probe', scope: request.scope, timezone: 'UTC',
|
||||
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
|
||||
const first = await heartbeat.runNow({ id: config.id, idempotencyKey: 'first' })
|
||||
report.stage = 'verify-publication'
|
||||
const activity = (await storage.call('assistant', 'listSupervisionActivity', [10, 0, config.id]))[0]!
|
||||
assert.equal(activity.status, 'completed')
|
||||
assert.equal(activity.suggestionStatus, 'completed')
|
||||
const stories = await ports.stories.list(request.scope)
|
||||
const experiences = await ports.experiences.list()
|
||||
const suggestions = await ports.suggestions.countForHeartbeat(first.id)
|
||||
assert(stories.length > 0 && experiences.length > 0 && suggestions > 0)
|
||||
const before = calls.length
|
||||
const quiet = await supervisor.run(request)
|
||||
assert.equal(quiet.status, 'no_change')
|
||||
assert.equal(calls.length, before)
|
||||
report.counts = { stories: stories.length, experiences: experiences.length, suggestions }
|
||||
report.noChangeCalls = calls.length - before
|
||||
report.usage = (await storage.call('assistant', 'getTokenUsageSummary', [])).systemTotals
|
||||
report.settingsUnchanged = (await readFile(settingsPath)).equals(original)
|
||||
assert.equal(report.settingsUnchanged, true)
|
||||
assert.equal(storage.pendingCount, 0)
|
||||
report.passed = true
|
||||
} finally { pool.dispose(); await storage.close() }
|
||||
}
|
||||
void run().catch(error => {
|
||||
report.passed = false
|
||||
report.errorType = error instanceof Error ? error.name : 'Error'
|
||||
// Do not print provider errors, headers, credentials, or settings.
|
||||
}).finally(() => { console.log(JSON.stringify(report)); app.exit(report.passed ? 0 : 1) })
|
||||
+43
-4
@@ -1,6 +1,6 @@
|
||||
# GoodBuddy 静态官网
|
||||
|
||||
`sites` 是无需构建步骤或额外依赖的中英文静态官网源码,可直接托管整个目录。
|
||||
`sites` 包含中英文官网和使用 VitePress 默认主题的用户手册。官网可直接预览;手册正文使用 Markdown 维护,发布前需要构建。
|
||||
|
||||
正式站点地址:<https://mesalogo.github.io/goodbuddy/>
|
||||
|
||||
@@ -16,7 +16,7 @@ x64 / arm64 安装包与独立龙芯 loong64 预览版,并说明兼容性与
|
||||
首屏示意显示所选 Runtime,不展示 Ask / Execute 模式或一般工具审批。工具说明以
|
||||
已启用能力和 MCP 服务分配为准;站点校验会检查中英文页面中的旧模式文案与控件。
|
||||
FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准备和远端使用条件。
|
||||
英文页面位于 `en.html`,用平台与架构覆盖描述同一产品事实,不使用中文信创
|
||||
英文页面位于 `en.html`,用户手册由 `docs/content/` 中按功能拆分的 Markdown 内容构建到 `docs-dist/`。英文页面用平台与架构覆盖描述同一产品事实,不使用中文信创
|
||||
政策语汇;页面会标明独立的 LoongArch 实验预览,但三个平台的下载按钮仍始终
|
||||
前往 GitHub 最新正式 Release。
|
||||
首屏产品界面默认正面展示,在精确指针设备上使用克制的 3D 倾斜、
|
||||
@@ -26,7 +26,7 @@ FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准
|
||||
## 部署
|
||||
|
||||
`.github/workflows/pages.yml` 会在 `main` 分支中的官网文件发生变化后,
|
||||
校验并部署整个 `sites` 目录。工作流也支持在 GitHub Actions 中手动运行。
|
||||
安装依赖、构建手册、校验并部署整个 `sites` 目录。工作流也支持在 GitHub Actions 中手动运行。
|
||||
|
||||
首次部署前,需要在 GitHub 仓库的 **Settings > Pages** 中将 **Source**
|
||||
设为 **GitHub Actions**。站点使用项目 Pages 地址,不需要 `CNAME` 文件
|
||||
@@ -42,17 +42,41 @@ FAQ 使用原生 details/summary,说明模型费用、数据流向、组件准
|
||||
|
||||
## 本地预览
|
||||
|
||||
使用 Node.js 24,在仓库根目录安装依赖后运行 `npm run docs:dev`。访问终端显示的地址,在手册 base 下打开 `zh/` 或 `en/`,默认路径为 `/docs-dist/zh/` 和 `/docs-dist/en/`。Markdown 修改会触发热更新。
|
||||
|
||||
预览构建产物时,先按下方命令使用本地 base 构建,再运行 `npm run docs:preview`;同样以终端显示的地址和端口为准,在 base 下打开 `zh/` 或 `en/`。
|
||||
|
||||
### 维护手册
|
||||
|
||||
- 章节顺序、双语标题和稳定 slug 维护在 `docs/manifest.ts`;VitePress 配置从该清单生成侧栏。
|
||||
- 中文正文放在 `docs/content/zh/`,英文正文放在 `docs/content/en/`。每个语言目录都必须包含 manifest 中的十二个 slug:`start`、`concepts`、`connections`、`runtimes`、`workbar`、`knowledge`、`notes`、`tasks`、`supervision`、`remote`、`privacy`、`troubleshooting`。
|
||||
- 每个章节文件以一个 `#` 标题开始,正文使用 `##` 和 `###`。除 `start.md` 的 `##` 章节标题外,其余文件的 `#` 标题应与 manifest 对应语言的标题一致。标题尽量使用普通文字。
|
||||
- VitePress 根目录为 `docs/`,`srcDir` 为 `content`。新增章节时添加两份 Markdown 并更新 manifest,同时更新校验脚本中的必需章节清单。
|
||||
- `zh/start.md` 和 `en/start.md` 分别重写为 `zh/index.md` 和 `en/index.md`,入口为 base 下的 `zh/` 和 `en/`;其余章节路径为 `zh/<slug>.html` 和 `en/<slug>.html`。
|
||||
- 配置维护在 `docs/.vitepress/config.mts`,使用默认主题的本地搜索、双语侧栏和原生语言切换,不维护自定义主题或布局。
|
||||
- 生产 base 为 `/goodbuddy/docs-dist/`,开发与预览默认使用 `/docs-dist/`,可用 `DOCS_BASE` 覆盖。构建和预览的 base 必须一致。开始页下载链接使用正式官网绝对地址:`https://mesalogo.github.io/goodbuddy/#download` 和 `https://mesalogo.github.io/goodbuddy/en.html#download`。
|
||||
- `docs-dist/` 是构建产物,已忽略,不手工编辑或提交。
|
||||
|
||||
整站预览前先构建,以下命令在仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
$env:DOCS_BASE='/docs-dist/'
|
||||
npm run docs:build
|
||||
Remove-Item Env:DOCS_BASE
|
||||
```
|
||||
|
||||
在仓库根目录运行:
|
||||
|
||||
```powershell
|
||||
python -m http.server 4173 --bind 127.0.0.1 --directory sites
|
||||
```
|
||||
|
||||
然后访问 <http://localhost:4173/>。也可以直接用浏览器打开 `sites/index.html`。
|
||||
然后访问官网 <http://127.0.0.1:4173/> 或 <http://127.0.0.1:4173/en.html>,中文手册 <http://127.0.0.1:4173/docs-dist/zh/>,英文手册 <http://127.0.0.1:4173/docs-dist/en/>。手册需要通过 HTTP 服务预览。
|
||||
|
||||
## 校验
|
||||
|
||||
```powershell
|
||||
npm run docs:build
|
||||
node sites/scripts/validate.mjs
|
||||
node --check sites/app.js
|
||||
node --check sites/language.js
|
||||
@@ -68,6 +92,7 @@ node --test sites/scripts/app.test.mjs sites/scripts/release-index.test.mjs
|
||||
不可变 URL,并按发布生成器的实际命名绑定版本、平台、架构和格式。移动
|
||||
导航行为测试同时覆盖现代 MediaQueryList 监听与旧版 Safari 的 `addListener`
|
||||
回退。英文下载入口固定指向 GitHub Release。
|
||||
手册校验覆盖全部 24 个构建章节的服务端渲染标题与正文、双语侧栏、语言切换和本地资源,并检查 Markdown 标题与 manifest 一致。上述命令检查生产 base;若检查本地构建,运行校验脚本时也设置 `DOCS_BASE='/docs-dist/'`。
|
||||
|
||||
## 下载入口
|
||||
|
||||
@@ -173,10 +198,24 @@ SIL OFL 1.1 许可证位于 `assets/fonts/inter-OFL.txt`。
|
||||
焦点,切换回桌面宽度也会解除隔离。媒体查询监听兼容现代浏览器和使用
|
||||
`MediaQueryList.addListener` 的旧版 Safari。
|
||||
|
||||
## 手册开发
|
||||
|
||||
手册正文维护在 `docs/content/zh/` 与 `docs/content/en/`,章节清单维护在
|
||||
`docs/manifest.ts`,主题、语言、侧栏和本地搜索配置维护在 `docs/.vitepress/config.mts`。本地开发运行:
|
||||
|
||||
```powershell
|
||||
npm run docs:dev
|
||||
```
|
||||
|
||||
发布前清除本地 `DOCS_BASE` 覆盖并运行 `npm run docs:build`。VitePress 将中英文各十二个章节及资源生成到 `docs-dist/`,入口为 `docs-dist/zh/index.html` 和 `docs-dist/en/index.html`。Pages 工作流会在校验前自动构建;不要直接编辑构建产物。
|
||||
|
||||
## 文件
|
||||
|
||||
- `index.html`:页面结构与简体中文内容
|
||||
- `en.html`:不包含信创适配文案的英文页面
|
||||
- `docs/content/zh/`、`docs/content/en/`:按功能拆分的中英文手册 Markdown 正文
|
||||
- `docs/manifest.ts`:手册章节顺序、标题和中英文映射
|
||||
- `docs/.vitepress/config.mts`:VitePress 默认主题、语言、路由、本地搜索和构建配置,输出到 `docs-dist/`
|
||||
- `styles.css`:语义令牌、浅深主题、焦点与响应式布局
|
||||
- `app.js`:主题、移动导航、当前章节和中文下载索引
|
||||
- `language.js`:浏览器语言自动选择与手动语言偏好
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { chapters } from '../manifest'
|
||||
|
||||
const base = process.env.DOCS_BASE ?? '/goodbuddy/docs-dist/'
|
||||
|
||||
const sidebar = (language: 'zh' | 'en') => [
|
||||
{
|
||||
text: language === 'zh' ? '用户手册' : 'User guide',
|
||||
items: chapters.map(({ slug, title }) => ({
|
||||
text: title[language],
|
||||
link: `/${language}/${slug === 'start' ? '' : `${slug}.html`}`,
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
export default defineConfig({
|
||||
title: 'GoodBuddy',
|
||||
description: 'GoodBuddy user guide',
|
||||
base,
|
||||
srcDir: 'content',
|
||||
rewrites: {
|
||||
'zh/start.md': 'zh/index.md',
|
||||
'en/start.md': 'en/index.md',
|
||||
},
|
||||
locales: {
|
||||
root: { label: 'English', lang: 'en', link: '/en/' },
|
||||
zh: { label: '简体中文', lang: 'zh-CN', link: '/zh/' },
|
||||
},
|
||||
themeConfig: {
|
||||
siteTitle: 'GoodBuddy',
|
||||
search: { provider: 'local' },
|
||||
outline: 'deep',
|
||||
sidebar: {
|
||||
'/zh/': sidebar('zh'),
|
||||
'/en/': sidebar('en'),
|
||||
},
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/mesalogo/goodbuddy' }],
|
||||
locales: {
|
||||
root: {
|
||||
langMenuLabel: '语言',
|
||||
nav: [
|
||||
{ text: 'Home', link: 'https://mesalogo.github.io/goodbuddy/en.html' },
|
||||
{ text: 'GitHub', link: 'https://github.com/mesalogo/goodbuddy' },
|
||||
],
|
||||
},
|
||||
zh: {
|
||||
langMenuLabel: 'Languages',
|
||||
nav: [
|
||||
{ text: '官网首页', link: 'https://mesalogo.github.io/goodbuddy/' },
|
||||
{ text: 'GitHub', link: 'https://github.com/mesalogo/goodbuddy' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
# Core concepts
|
||||
|
||||
GoodBuddy connects conversations, model requests, and file work through a small set of related objects. Knowing these objects makes Runtime selection, knowledge scope, and execution history easier to understand.
|
||||
|
||||
## Projects and conversations
|
||||
|
||||
A Project is a working scope with a project directory, project Runtime choice, conversations, and tasks. A Conversation is a continuing chat record containing messages, tool activity, and citations. A conversation may have no Task or several Tasks; linking a Task does not change the conversation type.
|
||||
|
||||
## Model connections and Runtimes
|
||||
|
||||
A model connection is a reusable service configuration with a URL, protocol, model name, and credentials. A Runtime is the way a request runs: Direct models are called by GoodBuddy, while OpenCode and Continue use their native agent clients and DeepSeek Harness uses GoodBuddy's compatible managed path. See [Choose a Runtime](./runtimes.html).
|
||||
|
||||
A Runtime uses the selected capabilities and current account permissions. A project can define the default choice, and a conversation can inherit it or select its own. If a connection is deleted or incompatible, the picker explains the fallback or failure instead of silently choosing another credentialed model.
|
||||
|
||||
## Capabilities and execution spaces
|
||||
|
||||
A capability is something a request may use, such as knowledge retrieval, workspace files, a terminal, a browser, or an MCP service. Availability depends on the Runtime, project settings, conversation scope, and execution space. An execution space is where files, terminals, managed processes, and the Runtime actually run: a local project directory or a remote SSH project directory.
|
||||
|
||||
## Task, Job, and Run
|
||||
|
||||
A Task is work that the user explicitly creates or confirms, and it has exactly one associated Conversation. A Job is an internal step or scheduled occurrence within a Task. A Run is one execution attempt and record for a Job or subjob. Ordinary chat requests, tool calls, and Runs do not become Tasks automatically. The UI primarily exposes Tasks; Job and Run details appear through activity and execution records when available.
|
||||
|
||||
## No Ask / Execute mode
|
||||
|
||||
GoodBuddy has no Ask / Execute work mode and does not require a mode switch before using an enabled capability. A normal request uses the selected Runtime, enabled capabilities, and current account permissions. If a Runtime lacks a capability, GoodBuddy reports that limitation. Summaries, note analysis, and supervision reviews remain dedicated text requests without tools.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Configure model connections
|
||||
|
||||
A model connection stores a service URL, protocol, model name, authentication method, and optional request customization. Projects and conversations can reuse it, while credentials remain managed by GoodBuddy's main process. A usable connection does not mean every Runtime supports every field.
|
||||
|
||||
## Create and test a connection
|
||||
|
||||
Open Settings > Model connections > LLM. Create a connection with a name, HTTP or HTTPS URL, protocol, model ID, and API key. Save it and run Test connection. A successful test proves that the basic connection works; image generation, tools, and native Runtime clients still need their own real request path.
|
||||
|
||||
Connections can be copied. Copy creates a new connection and leaves the original unchanged. A saved key is copied through the protected credential path and is never returned to the page or error text. Choose the connection in a project or conversation picker, or choose the Runtime default.
|
||||
|
||||
## Custom headers and body
|
||||
|
||||
The Advanced settings area has two JSON editors. Headers must be an object whose values are strings, and the body must be a serializable top-level JSON object. Syntax, type, size, and reserved-field errors block saving while keeping the original text for correction.
|
||||
|
||||
Runtime, protocol, and authentication fields take precedence. Custom values cannot replace model, message, tool, streaming, or authentication fields. Body values merge only at the top level. Keep API keys and other secrets in the dedicated credential field. Direct models and Continue support both customizations; local OpenCode and DeepSeek Harness support headers but not custom body; managed remote OpenCode supports both.
|
||||
|
||||
## Scope and diagnostics
|
||||
|
||||
Connections are Settings resources; projects and conversations store the selection relationship. Vector connections are for knowledge indexing and do not appear in Runtime model pickers. For diagnosis, test the basic connection first, then confirm the Runtime and model used by the real request. If custom values appear to cause failure, clear them temporarily. A connection test is not a generation guarantee.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Build a knowledge base
|
||||
|
||||
A knowledge base brings local sources or a configured external knowledge service into retrieval. For a first setup, create a base, add sources, wait until it is searchable, and use it in the current conversation.
|
||||
|
||||
## Local workflow
|
||||
|
||||
Create a local base, add files, directories, or web sources, and wait for parsing and indexing. In the composer, explicitly select the knowledge scope and choose Model retrieves when needed or Retrieve before every request. Open citations to inspect the matching chunk, surrounding context, and source.
|
||||
|
||||
Knowledge scope is stored per Conversation. New conversations start with no selected base, and creating a base does not re-enable other bases you turned off. Each request uses only its explicit scope. The first mode lets the model decide whether to use read-only search; the second searches before the Runtime starts and shows zero results or degradation. Image generation does not run the pre-search.
|
||||
|
||||
Local retrieval can use full-text, Chinese phrase, vector, and graph channels. A citation is bounded evidence for the request, not proof that every answer sentence came from it. Check parsing, full-text, vector, and graph status separately. Vector failure can leave full-text and Chinese retrieval available, with the degraded channel shown. A failed rebuild keeps the last ready index.
|
||||
|
||||
## External knowledge bases
|
||||
|
||||
GoodBuddy can manage Dify, FastGPT, and RAGFlow instances and bind a remote base into the local list. Retrieval sends only the current query, remote ID, and saved settings. GoodBuddy checks returned chunks before showing citations.
|
||||
|
||||
External bindings are read-only. GoodBuddy does not create, edit, upload, or delete remote bases, documents, chunks, or graphs, and does not bulk-sync them. Removing a local binding does not delete remote data. Authentication, timeout, target, and provider failures remain visible and do not silently switch instances.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Use Magic Notes
|
||||
|
||||
Magic Notes is for meeting records, research drafts, ideas, and long-term plans. Notes are application-level data. The full workspace supports rich text, todos, and paged canvases, while the workbar provides quick browsing and appending.
|
||||
|
||||
## Capture conversation content
|
||||
|
||||
Choose Add to note from an assistant response, or Save current conversation to note from the conversation menu. Review the preview, source, and destination, then explicitly save. A full capture contains completed user and assistant text, preserving Markdown, line breaks, and code. Reasoning, tool logs, system prompts, binary attachments, and images are excluded.
|
||||
|
||||
Saved entries keep structured source information. Open a source to return to the conversation or locate the message. If the source is later removed, the note body remains and the source is marked unavailable. A failed save keeps the preview and destination for retry; cancelling writes nothing.
|
||||
|
||||
## Quick panel and full workspace
|
||||
|
||||
Open + > Notes in the workbar to search notes, view summaries, and append text. The panel and full workspace share storage, so successful writes appear in both. Unsaved drafts stay in app memory and are protected when changing project, conversation, or workbar visibility. Leaving the panel asks whether to keep or discard a changed draft.
|
||||
|
||||
Tags are manual. A note can have up to 10 case-insensitive tags; filter by tags or rename, merge, and delete them in tag management. Deleting a tag does not delete notes.
|
||||
|
||||
## Canvas and AI comments
|
||||
|
||||
Paged canvases support templates, text, handwriting, highlights, images, and PDF backgrounds. Canvas saves are manual. Zoom changes viewing only; there is no infinite canvas or separate PNG download.
|
||||
|
||||
Request AI comments manually, or enable analysis after save. Comments use the default model's image input and the first 1 to 8 configured pages. Visual-only content requires an image-capable model; a text-only model uses a labelled text fallback. Analysis failure never undoes saved content and does not append again automatically.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Data and privacy
|
||||
|
||||
GoodBuddy is a local-first desktop app and does not require a GoodBuddy account. Whether data leaves the computer depends on the model connection, Runtime, knowledge type, remote project, and browser features you use.
|
||||
|
||||
## Data flow
|
||||
|
||||
Local conversations, notes, tasks, settings, and knowledge indexes are stored in the application-data directory. Direct models send requests to the selected service; OpenCode, Continue, and Harness call models according to their Runtime configuration. Installing GoodBuddy or opening an empty conversation does not upload history.
|
||||
|
||||
With a local knowledge base, sources and indexes stay local and the Runtime receives bounded evidence and citations selected for the request. With Dify, FastGPT, or RAGFlow, the query goes to the configured instance. GoodBuddy does not upload the full conversation or local attachments or write to the external base. Returned bounded chunks and citations are stored with the local conversation.
|
||||
|
||||
## Credentials and boundaries
|
||||
|
||||
API keys, SSH credentials, and authentication data are managed by Main and protected storage; the Renderer does not read them directly. Custom headers and bodies are ordinary settings, not a place for secrets. Tools, MCP, terminals, and plugins use current-user permissions and enabled capabilities. Ask / Execute is not a separate privacy boundary.
|
||||
|
||||
Remote projects send bounded model configuration to an authenticated Agent, where requests run with SSH account permissions. The Agent is not bundled with the desktop package. Reconnect restores confirmed Agent output; uncertain model calls are not replayed automatically.
|
||||
|
||||
## Preview and clearing data
|
||||
|
||||
Conversation HTML preview handles completed assistant HTML only. Scripts, network requests, forms, nested pages, and window operations are disabled; turning preview off still shows source. Clear local data is an explicit confirmed action and does not delete provider records, remote hosts, remote bases, exported files, or files saved in a workspace.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Remote projects
|
||||
|
||||
Remote projects run Workspace, terminals, and supported Runtimes on an SSH host. A remote path is never treated as local. Files, processes, network access, and model calls are bounded by the SSH account and prepared environment. The GoodBuddy Agent is not included in the desktop installer.
|
||||
|
||||
## Prepare an SSH host
|
||||
|
||||
Add a Host in remote-project settings with its address, port, user, and authentication method. Before authentication, inspect and explicitly accept the Host Key algorithm and SHA-256 fingerprint. A changed fingerprint requires a separate replacement confirmation. GoodBuddy can use the system SSH Agent or supported certificates; root and SSH Agent Forwarding are not required.
|
||||
|
||||
Saving a Host does not mean the environment is ready. Use the Host card to prepare or update the signed Agent and Runtime, then wait for checks. A version-match badge is only a version fact. Failed, cancelled, or disconnected preparation keeps the Host and offers retry.
|
||||
|
||||
## Create a project and use its space
|
||||
|
||||
Choose a verified Host, enter or select a bounded remote workspace directory, and choose a supported Runtime such as OpenCode or Continue. The project stores the Host ID, remote path, and Runtime choice. Switching projects changes local selection only; the Agent is attached when Workspace or Runtime is first used.
|
||||
|
||||
Remote Workspace provides bounded directories, previews, Git, and import. A terminal uses the remote shell. Operations use the SSH account permissions. Local Skills, stdio MCP servers, and tool environments are not uploaded automatically; remote capabilities are those reported by the Agent.
|
||||
|
||||
## Failures and boundaries
|
||||
|
||||
Check network, port, Host Key, credentials, directory permissions, Agent version, Runtime version, and project permissions separately. If the Agent is unavailable, GoodBuddy does not fall back to an SSH stdio Runtime or secretly run the request locally.
|
||||
|
||||
An SSH relay disconnect does not necessarily stop an accepted request. Reconnect restores only output recorded and confirmed by the Agent. An uncertain model call is not replayed automatically because it could duplicate cost or side effects. Stop, Agent shutdown, and remote process failure are shown as cancellation, failure, or unknown outcome.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Choose a Runtime
|
||||
|
||||
A Runtime selects the execution path for a request. A connection answers which model service to call, while the Runtime organizes the request, tools, and execution space. Each request retains visible status, tool activity, cancellation, and failure information.
|
||||
|
||||
## The four choices
|
||||
|
||||
| Runtime | Best for | Model configuration |
|
||||
| --- | --- | --- |
|
||||
| Direct models | Chat, retrieval, image generation, and GoodBuddy capabilities | GoodBuddy model connections |
|
||||
| OpenCode | Coding agents, files, commands, and Skills | GoodBuddy connection or OpenCode config |
|
||||
| Continue | Rules, prompt presets, and coding work | GoodBuddy connection or Continue config |
|
||||
| DeepSeek Harness | A fixed host and compatible plugin capabilities | GoodBuddy-managed compatible connection |
|
||||
|
||||
Use Direct models for ordinary questions and retrieval. Choose OpenCode or Continue for continuing project-file and command work. Choose DeepSeek Harness when its fixed host capabilities fit. Tools and native questions differ by Runtime.
|
||||
|
||||
## Selection and capabilities
|
||||
|
||||
A project selects a concrete Runtime. A conversation follows the project by default, while the composer can select its own choice. The picker shows defaults, individual connections, and Runtime-owned configuration with their source. Requests use the Runtime, enabled capabilities, knowledge scope, execution space, and current account permissions.
|
||||
|
||||
GoodBuddy has no Ask / Execute mode and no separate general tool-approval switch. Unsupported capabilities are reported clearly. Local Runtimes run locally; remote projects use the Agent and managed Runtime on the SSH host. The Agent is not bundled with the desktop app; see [Remote projects](./remote.html).
|
||||
|
||||
The workbar shows Runtime, connection, tool state, cancellation, errors, and saved results. Closing a panel does not cancel background work, and stopping a request affects that request only.
|
||||
@@ -0,0 +1,30 @@
|
||||
# GoodBuddy User Guide
|
||||
|
||||
This guide is for a first GoodBuddy session. No GoodBuddy account is required. Conversations, projects, and notes are local by default; model providers, remote hosts, and external knowledge services receive data only when you configure and use them.
|
||||
|
||||
## Get started
|
||||
|
||||
### Install GoodBuddy
|
||||
|
||||
Choose a platform, processor architecture, and package from the [home page](https://mesalogo.github.io/goodbuddy/en.html#download). Windows provides an installer and a portable ZIP. Linux provides AppImage, DEB, and RPM. LoongArch uses a separate experimental preview channel. Start the app after installation; the first launch does not require sign-in.
|
||||
|
||||
### Initial setup
|
||||
|
||||
1. Open Settings, confirm the local data location, and prepare a text model connection.
|
||||
2. In Model connections, enter the service URL, protocol, model name, and credentials, then run a connection test. See [Configure model connections](./connections.html) for the field rules.
|
||||
3. Create a project and choose its project directory and Runtime. A project groups conversations, workspace access, and tasks.
|
||||
4. Open or create a conversation and send a simple question first. Once that works, try files, knowledge, or remote operations.
|
||||
|
||||
### Projects and conversations
|
||||
|
||||
A project is a working scope that can contain many conversations. A conversation stores user messages, assistant replies, tool activity, and citations; it can also be associated with multiple tasks. New conversations usually inherit the project Runtime choice, while the composer can select a conversation-specific choice.
|
||||
|
||||
When changing projects or working directories, check the workspace and terminal scope in the [assistant workbar](./workbar.html). Before sending a request, confirm the Runtime, model, and knowledge scope so content goes to the intended service.
|
||||
|
||||
### Local data
|
||||
|
||||
Conversations, notes, task records, knowledge indexes, and settings are stored in the system application-data directory. Clear local data is an explicit Settings action with confirmation. Uninstalling the app does not delete exported files, remote directories, or provider-side records. See [Data and privacy](./privacy.html) for the data boundaries.
|
||||
|
||||
### Continue reading
|
||||
|
||||
Read [Core concepts](./concepts.html) for the relationship between projects, conversations, and Runtimes. Then configure a [knowledge base](./knowledge.html), create [tasks](./tasks.html), or set up a [remote project](./remote.html).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Supervision
|
||||
|
||||
Supervision reviews completed work, highlights changes and issues, and records supported results in the story graph. It does not send messages, approve tools, expand file scope, or change execution permissions. The Supervisor app is off by default.
|
||||
|
||||
## Enablement and scope
|
||||
|
||||
Enable Supervisor in the app center, then choose the default text model or an existing text connection in Settings. The first review requires an explicit project, conversation, and time range. Automatic supervision can target Global or selected projects; changing the active project does not change a saved scope.
|
||||
|
||||
## Review and story graph
|
||||
|
||||
Work review reads conversations, Tasks, notes, and sources within the selected scope and produces sourced summaries, changes, and unresolved items. With no valid new change it records no change and does not call the model. Review is incremental by default; use Reorganize for a historical interval.
|
||||
|
||||
The story graph presents events, entities, relationships, and changes over time. Select an object to inspect its sources. Conversations, notes, and knowledge bases remain the owners of source text. You can continue a discussion after checking evidence or preview a write to a local knowledge base. External knowledge remains read-only.
|
||||
|
||||
## Follow, pin, and intervene
|
||||
|
||||
The Supervision panel can follow the active conversation or pin to a conversation or Task. A pinned target stays selected across page changes and reports an error if it becomes unavailable. Suggestions create editable drafts or explicit next actions; they enter a conversation only after you choose to send them.
|
||||
|
||||
Supervisor does not execute suggestions and cannot send messages, approve tools, change capability switches, or expand workspace access. Its model calls are internal text requests without tools.
|
||||
|
||||
## Heartbeat and failures
|
||||
|
||||
Smart Heartbeat is the scheduled wake-up mechanism. It checks incremental changes within its scope and generates suggestions or updates memory according to the plan. The app and each plan have separate enabled states; disabling the app prevents new heartbeat runs, while an already started run may finish.
|
||||
|
||||
Pause, cancel, and failure keep the last successful result and saved batches. The run shows covered and omitted scope and can be retried when valid. A failed suggestion stage does not undo a published graph or review. Supervisor being off by default does not affect ordinary chat or Tasks.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Tasks
|
||||
|
||||
A Task is work that you explicitly create or confirm. It has exactly one associated Conversation, while one Conversation can have several Tasks. Ordinary chat requests and tool calls do not appear in Task Center automatically.
|
||||
|
||||
## Immediate and scheduled work
|
||||
|
||||
Choose New task in Task Center or the conversation task area. Enter the request; the name can be blank and GoodBuddy will derive one. Immediate execution is the default. You can choose a one-time, daily, or weekly schedule and select the current conversation, another conversation in the project, or a new conversation. The request, conversation, and schedule are submitted together.
|
||||
|
||||
A Task uses the associated conversation's Runtime, capabilities, knowledge scope, and workspace when it runs. It has no Ask / Execute mode and does not gain permission because it runs in the background. A new conversation does not copy source history.
|
||||
|
||||
## Task, Job, and Run
|
||||
|
||||
Task is the user-facing object. An immediate run, schedule occurrence, or retry may create internal Jobs and Runs, but they do not become new Task entries. Task Center aggregates status, progress, schedule, and attention items; Activity and Runtime panels provide detail.
|
||||
|
||||
A recurring schedule remains one Task. Pause, resume, run now, and cancel act on the Task. Deleting it normally stops future scheduling while keeping the Conversation and messages. Deleting one queued occurrence does not delete the Task.
|
||||
|
||||
## Queue and Heartbeat
|
||||
|
||||
Normal and Task messages use the same Conversation queue. A due occurrence waits while a reply is running. You can remove an unstarted item or choose Run now and interrupt. Later items can continue after cancellation or failure. Smart Heartbeat is not a Task Center item; it belongs to [Supervision](./supervision.html) and wakes incremental reviews and suggestions.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Troubleshooting
|
||||
|
||||
First confirm the current project, conversation, Runtime, model, and local or remote execution space. Check request status and activity before repeating a send or rebuilding all data.
|
||||
|
||||
## The model connection test fails
|
||||
|
||||
Check protocol, URL, model ID, API key, proxy, certificate, and provider limits. Clear custom headers and body temporarily and test again. If the basic test succeeds but generation fails, confirm the Runtime and model capability; a connection test is not a generation guarantee.
|
||||
|
||||
## The reply does not use tools or files
|
||||
|
||||
Check Runtime support, execution space, workspace directory, enabled capability or MCP service, and account permissions. Open Workspace or Runtime in the workbar. For remote projects check SSH, Agent readiness, remote path, and Agent version. GoodBuddy has no Ask / Execute mode.
|
||||
|
||||
## Knowledge retrieval returns nothing
|
||||
|
||||
Check the conversation scope, parsing and full-text status, vector compatibility, and OCR for image-only PDFs. Use Test retrieval to see actual channels and degradation. Test with source words first, then rebuild a failed document or index. External bases also require valid authentication, remote ID, timeout, and provider status.
|
||||
|
||||
## A note, Task, or remote project failed
|
||||
|
||||
For notes and canvases, confirm that Save was explicitly used; AI comment failure does not undo saved content. For Tasks, check enabled state, pause state, associated conversation, app availability, and the latest error. Heartbeat is off by default. For remote work, check network, Host Key, credentials, permissions, and Agent preparation. Uncertain remote calls are not replayed automatically.
|
||||
|
||||
## HTML preview is empty or shows source
|
||||
|
||||
Check that the reply is complete, recognizable HTML or an `html`/`htm` block, and HTML rendering is enabled. Streaming replies, user messages, and reasoning remain source text. Scripts, network, forms, and window operations are intentionally disabled in the preview.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Assistant workbar
|
||||
|
||||
The assistant workbar is the application-level workspace on the right side of the main window. It is not attached to one chat message. Chat, knowledge, task, and supervision pages can open it to show tasks, workspace, Runtime, terminals, browser tabs, notes, and supervision status.
|
||||
|
||||
## Panels and the add menu
|
||||
|
||||
Task Center and Workspace are always-present base panels. Use the + menu to open Runtime, Supervision, Terminal, Processes, Browser, Resources, Documents, or Notes. Terminals and browsers can have multiple instances; Notes is single-instance. Background events update badges or notifications without taking over the current panel.
|
||||
|
||||
An open panel shows its capability, instance, and target. When a capability is unavailable, the panel explains why and provides a configuration path instead of silently hiding it. Terminals, logs, and large results may offer right, bottom, or separate-window docking.
|
||||
|
||||
## Follow and pin
|
||||
|
||||
Panels that can bind to a target have two scope choices: follow the active context, or pin to a conversation, Task, project, Runtime, terminal, workspace, or browser session. Follow mode changes with the project or conversation. Pinned mode stays on its target. If a pinned target disappears, the panel reports that it is unavailable and offers a repair path rather than switching silently.
|
||||
|
||||
The active context is only a default. Opening Workspace does not expand file access, and choosing a temporary directory does not silently change project settings. GoodBuddy still validates the project, execution space, and capability before an operation.
|
||||
|
||||
## Execution-space boundaries
|
||||
|
||||
Workspace shows the current project directory or a local or remote directory that you explicitly choose. A new terminal binds to the project execution space and directory; with no project it uses the local home directory. Browser manages only GoodBuddy-controlled browser tabs, not a browser already open on your system. Processes shows processes created or managed by GoodBuddy, not every system process.
|
||||
|
||||
Workspace supports file previews, Git status, and bounded directory operations. A remote workspace uses the Agent on the SSH host and does not pretend that a remote path is local. Terminal input, file operations, browser activity, and Runtime execution each keep their own scope; selecting one panel does not grant another panel more access.
|
||||
|
||||
## A practical combination
|
||||
|
||||
For project work, let Task Center follow the current project and pin Supervision to one conversation. Use Workspace to inspect files and create a terminal for commands. When reviewing activity, check the project, conversation, Runtime, and local or remote label at the top of the panel. Open a Task for its complete user-facing relation; Job and Run are not separate workbar panels.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: GoodBuddy User Guide
|
||||
layout: home
|
||||
---
|
||||
|
||||
# GoodBuddy User Guide
|
||||
|
||||
Choose a language to open the user guide:
|
||||
|
||||
- [English](/en/)
|
||||
- [简体中文](/zh/)
|
||||
@@ -0,0 +1,25 @@
|
||||
# 核心概念
|
||||
|
||||
GoodBuddy 把聊天、模型调用和文件操作放在几个相互关联的对象中。理解这些对象后,选择 Runtime、限制知识范围和查看执行记录会更容易。
|
||||
|
||||
## Project 与 Conversation
|
||||
|
||||
Project 是项目范围,包含项目目录、项目级 Runtime 选择、会话和任务。Conversation 是一条持续的聊天记录,保存消息、工具活动和引用。一条会话可以没有 Task,也可以关联多个 Task;关联 Task 不会改变会话类型。
|
||||
|
||||
## 模型连接与 Runtime
|
||||
|
||||
模型连接是可复用的服务配置,通常包含 URL、协议、模型名和凭据。Runtime 是执行请求的方式:直连模型由 GoodBuddy 调用模型服务,OpenCode 和 Continue 使用各自的 Agent 客户端,DeepSeek Harness 使用 GoodBuddy 管理的兼容执行路径。Runtime 的具体选择见[选择 Runtime](./runtimes.html)。
|
||||
|
||||
Runtime 会使用当前选择的能力和当前账号权限。项目可以设置默认 Runtime,会话可以继承项目设置或单独选择。模型连接被删除或不兼容时,选择器会说明回退或失败原因,不会悄悄换成另一个有凭据的模型。
|
||||
|
||||
## Capability 与执行空间
|
||||
|
||||
Capability 是一次请求可以使用的能力,例如知识检索、工作区文件、终端、浏览器或 MCP 服务。能力是否可用取决于 Runtime、项目设置、会话范围和执行空间。执行空间是文件、终端、受管进程和 Runtime 实际运行的位置,可以是本机项目目录,也可以是远程 SSH 项目目录。
|
||||
|
||||
## Task、Job 与 Run
|
||||
|
||||
Task 是用户明确创建或确认的工作单位,并且只关联一条 Conversation。Job 是 Task 内部的一次步骤或一次调度触发,Run 是 Job 或子任务的一次执行尝试和记录。普通聊天请求、工具调用和 Run 不会自动变成 Task;用户界面主要展示 Task,Job 和 Run 的细节在活动和执行记录中按需查看。
|
||||
|
||||
## 没有 Ask / Execute 模式
|
||||
|
||||
GoodBuddy 不提供 Ask / Execute 工作模式,也不会要求先切换模式才能调用已启用的能力。普通请求直接使用所选 Runtime、已启用能力和当前账号权限。某个 Runtime 不支持某项能力时,界面会报告能力缺失;内部摘要、笔记分析和监督回顾仍是专用的无工具文本请求。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 配置模型连接
|
||||
|
||||
模型连接保存服务地址、协议、模型名称、认证方式和可选的请求定制。连接可以被多个项目和会话复用,但凭据仍由 GoodBuddy 的主进程管理。连接本身可用,不代表每个 Runtime 都支持它的全部字段。
|
||||
|
||||
## 新建和测试
|
||||
|
||||
进入“设置 → 模型连接 → LLM”,新建连接并填写名称、HTTP 或 HTTPS 地址、协议、模型 ID 和 API Key。保存后执行“测试连接”。测试成功只说明当前连接的基础请求可达;图片生成、工具调用或某个 Runtime 的原生客户端仍需在实际请求中验证。
|
||||
|
||||
连接可以复制。复制会生成新的连接,不会修改原连接;已保存的密钥通过安全凭据路径复制,不会回显到页面或错误信息。需要在项目或会话中使用时,回到选择器选中连接或选择“跟随默认”。
|
||||
|
||||
## 自定义 Header 和 Body
|
||||
|
||||
连接详情底部的“高级设置”提供两个 JSON 编辑器。请求头必须是字符串值的 JSON 对象,例如 `{ "X-Team": "research" }`;请求体是可序列化的顶层 JSON 对象。语法、类型、大小或保留字段错误会阻止保存,并保留原始输入供修正。
|
||||
|
||||
Runtime、协议和认证生成的字段优先,自定义字段不能替换模型、消息、工具、流式或认证字段。请求体是顶层浅合并,不会递归合并内部对象。API Key、Cookie、签名和其他秘密信息必须放在专用凭据字段,不要写入 Header 或 Body。
|
||||
|
||||
直连模型和 Continue 支持两项定制;本地 OpenCode 和 DeepSeek Harness 支持自定义 Header,不使用自定义 Body;受管远程 OpenCode 在其模型网关支持两项。使用 Runtime 自有配置时不引用 GoodBuddy 连接,也不会应用连接级定制。
|
||||
|
||||
## 范围和诊断
|
||||
|
||||
连接是设置级资源,项目和会话只保存选择关系。向量连接只用于知识库索引,不会出现在 Runtime 模型选择器中。远程项目只能使用支持的远程 Runtime。连接地址变化、证书错误、缺少 API Key、模型不兼容和服务商限流都应在连接详情或请求状态中分别处理。
|
||||
|
||||
诊断时先执行基础连接测试,再检查实际请求使用的 Runtime 和模型。若自定义字段导致失败,暂时清空 Header 和 Body 重测;若基础测试成功而生成失败,检查协议、模型能力、输出限制和服务商的实际接口,而不要把测试结果当作生成保证。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 建立知识库
|
||||
|
||||
知识库把本地资料或已配置的外部知识服务接入对话检索。第一次使用按“创建知识库 → 添加资料 → 等待可检索 → 用于当前对话”的顺序操作;高级检索参数可以在基本流程可用后再调整。
|
||||
|
||||
## 本地知识库流程
|
||||
|
||||
1. 在知识库页面新建本地知识库,填写名称和描述。
|
||||
2. 添加文件、目录或网页来源,等待解析、全文索引和可选向量索引完成。
|
||||
3. 打开会话输入区的知识范围,明确勾选要使用的知识库。
|
||||
4. 选择“模型按需检索”或“每次先检索”,再发送问题。
|
||||
5. 打开回答中的引用,查看命中分块、相邻上下文和原始来源。
|
||||
|
||||
知识范围属于 Conversation。新会话默认不勾选知识库;切换会话会恢复各自的选择。新建一个知识库只把它加入当前会话,不会重新启用你以前取消的其他库。每次请求只能使用当前明确选择的范围。
|
||||
|
||||
## 检索方式和证据
|
||||
|
||||
“模型按需检索”只开放当前范围的只读搜索能力,由模型决定是否查询;没有调用时不会显示虚假的“已检索”。“每次先检索”会在 Runtime 启动前先查一次,零结果、失败和降级都会显示,模型仍可在同一范围内继续查询。图片生成请求不执行知识预检索。
|
||||
|
||||
本地检索可以使用全文、中文词组、向量和知识图谱通道。引用是本次检索得到的有界证据,不等于自动证明回答中的每一句话都来自该片段。点击引用可以核对来源;来源被移动或删除时显示失效,不会打开任意替代路径。
|
||||
|
||||
## 状态、降级和维护
|
||||
|
||||
分别查看解析、全文、向量和图谱状态。没有向量模型、查询向量失败、索引不兼容或部分文档失败时,全文和中文检索仍可用,但页面会标明降级原因,不会悄悄切换到另一个向量服务。重建失败时,上一版已就绪索引继续服务。
|
||||
|
||||
分块设置修改后需要显式重建。重建或大规模检索可以取消;已完成的文档保持可用。原文件移动、解析失败、OCR 不可用和零结果都应按照页面给出的原因处理,而不要只看“索引完成”这一总状态。
|
||||
|
||||
## 外部知识库
|
||||
|
||||
知识库页面可以管理 Dify、FastGPT 和 RAGFlow 实例,并把远端知识库绑定到 GoodBuddy 的本地列表。检索只发送本次查询、远端知识库 ID 和已保存的检索参数;返回片段经过 GoodBuddy 校验后作为引用使用。
|
||||
|
||||
外部绑定是只读的。GoodBuddy 不在外部实例中创建、修改、上传或删除知识库、文档、分块或图谱,也不批量同步远端资料。移除本地绑定不会删除远端内容;外部请求失败时显示实例、认证、超时或目标问题,不自动改用另一个实例。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 使用魔法笔记
|
||||
|
||||
魔法笔记适合保存会议记录、研究草稿、灵感和长期计划。笔记属于应用级数据,不会因为你从哪个项目或会话打开它而改变归属。完整工作区支持富文本、待办和分页画布,助手工作栏提供快速浏览和追加。
|
||||
|
||||
## 从会话保存内容
|
||||
|
||||
在一条助手回答的操作菜单中选择“加入笔记”,或在会话更多菜单中选择“保存当前对话到笔记”。先检查预览、来源和目标,再选择已有笔记或新建笔记并明确保存。整段会话保存的是已完成的用户和助手文字,保留 Markdown、换行和代码文本;推理、工具日志、系统提示、附件二进制和图片不会写入正文。
|
||||
|
||||
保存后的记录保留会话、项目、消息和采集时间等结构化来源。点击来源可以回到会话或定位消息;原会话或原消息后来删除时,笔记正文仍保留,并显示来源不可用。保存失败会保留预览、目标和输入,便于重试;取消不会写入。
|
||||
|
||||
## 快速面板和完整工作区
|
||||
|
||||
从工作栏“+ → 笔记”打开快速面板,可以搜索笔记、查看记录摘要和快速追加文字。面板与完整笔记工作区共享同一份存储,任一处成功写入后另一处会更新。未保存草稿保存在当前应用内存中,切换项目、会话或收起工作栏不会覆盖它;离开面板、关闭笔记或用另一条采集替换草稿时会要求选择继续编辑或放弃。
|
||||
|
||||
完整工作区可编辑记录、管理待办、搜索和调整列表。标签由用户手动维护:每篇笔记最多 10 个标签,标签不区分大小写;可以按一个或多个标签筛选,也可以在标签管理中重命名、合并或删除标签。删除标签不会删除笔记内容。
|
||||
|
||||
## 画布和 AI 评论
|
||||
|
||||
分页画布支持纸张模板、文字、手写、高亮、图片和 PDF 底版。画布保存是手动操作;缩放和适应宽度只影响查看,不改变已保存内容或导出尺寸。画布没有无限画布,也不提供单独的 PNG 下载按钮。
|
||||
|
||||
AI 评论由你手动触发,或在设置为保存后自动分析时于保存完成后触发。评论使用默认模型的图像输入能力,并按设置发送画布开头的 1 至 8 页。纯视觉内容需要图像模型;文字模型只能使用明确标记的文字降级。分析失败不会撤销已经成功保存的笔记或画布内容,也不会自动重复追加。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 数据与隐私
|
||||
|
||||
GoodBuddy 是本地优先的桌面应用,不要求 GoodBuddy 账号。数据是否离开本机,取决于你使用的模型连接、Runtime、知识库类型、远程项目和浏览器等功能。
|
||||
|
||||
## 数据流向
|
||||
|
||||
本地对话、笔记、任务、设置和知识库索引保存在系统应用数据目录。使用直连模型时,请求发送到你选择的模型服务;OpenCode、Continue 和 Harness 也会按其 Runtime 配置调用模型。GoodBuddy 不会因为安装应用或打开空白会话自动上传历史。
|
||||
|
||||
启用本地知识库时,资料和索引留在本机,Runtime 只收到本次检索选出的有界文本证据和引用。使用 Dify、FastGPT 或 RAGFlow 时,本次查询会发送到你配置的外部实例;GoodBuddy 不上传完整会话或本地附件,也不向外部知识库写入内容。实际返回的有界片段和引用会随会话保存在本机。
|
||||
|
||||
## 凭据和 Runtime 边界
|
||||
|
||||
API Key、SSH 凭据和其他认证信息由 Main 进程和系统安全存储管理,Renderer 不直接读取它们。自定义 Header 和 Body 属于普通连接设置,不适合保存秘密。工具、MCP、终端和插件使用当前用户权限以及明确启用的能力;没有 Ask / Execute 模式来替代这些实际配置。
|
||||
|
||||
远程项目会把有界的模型配置交给已认证的远程 Agent,模型请求在远端按 SSH 账号权限执行。远程 Agent 不随桌面包提供,GoodBuddy 不会凭空提高远端账号权限。断线恢复只同步 Agent 已记录且能确认的结果,不会自动重放不确定的模型调用。
|
||||
|
||||
## 预览和清除
|
||||
|
||||
会话中的 HTML 预览只处理已完成的助手 HTML,脚本、网络请求、表单提交、嵌套页面和窗口操作均被禁用;关闭 HTML 预览后仍可查看源码。它不是联网浏览器,也不会因为预览而把页面内容上传。
|
||||
|
||||
清除本地数据是设置中的明确操作,会先要求确认。它不删除外部模型服务、远程主机、远程知识库、导出文件或你手动保存到工作区的文件。备份、系统同步和第三方服务的保留策略由相应系统决定。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 远程项目
|
||||
|
||||
远程项目让 Workspace、终端和支持的 Runtime 在 SSH Host 上执行。远程路径不会被当成本机路径处理,文件、进程、网络和模型调用都受远端登录账号及远程环境限制。GoodBuddy Agent 不随桌面安装包提供,需要单独准备。
|
||||
|
||||
## 准备 SSH Host
|
||||
|
||||
在设置的远程项目入口添加 Host,填写地址、端口、用户名和认证方式。首次连接先查看 Host Key 的算法和 SHA-256 指纹并明确接受;指纹变化时必须单独确认替换。可用系统 SSH Agent 或支持的证书方式,凭据由桌面安全存储管理。GoodBuddy 不要求 root,也不默认启用 SSH Agent Forwarding。
|
||||
|
||||
Host 保存成功不等于远程环境已经就绪。按 Host 卡片的操作准备或更新签名 Agent 和 Runtime,等待环境检查通过。版本匹配只表示版本事实,不代表 Agent 正在运行或能力完整;安装失败、取消或网络中断会保留 Host 并提供重试。
|
||||
|
||||
## 创建项目和使用空间
|
||||
|
||||
创建远程项目时选择已验证的 Host,输入或通过有界目录选择器选择远端工作目录,再选择 OpenCode 或 Continue 等支持的远程 Runtime。项目保存 Host ID、远端路径和 Runtime 选择。普通切换项目只切换本地配置,不自动连接、安装或扫描主机;首次使用 Workspace 或 Runtime 时才建立或复用 Agent 连接。
|
||||
|
||||
远程 Workspace 提供有界目录、文件预览、Git 和导入操作;终端使用远端默认 Shell。所有操作使用 SSH 账号权限,不能获得 root 或账号本身没有的访问权。本机 Skills、stdio MCP 和工具环境不会自动上传到远端;远程能力以 Agent 报告的实际能力为准。
|
||||
|
||||
## 失败和边界
|
||||
|
||||
SSH 可达、认证成功、Agent 就绪、Workspace 可用和 Runtime 可启动是不同状态。分别检查网络、防火墙、端口、Host Key、凭据、远端目录、Agent 版本、Runtime 版本和项目权限。远端 Agent 不可用时不会退化成 SSH stdio Runtime,也不会把同一请求偷偷改在本机执行。
|
||||
|
||||
断开 SSH relay 不一定会停止已接受的远程请求;重新连接后 GoodBuddy 只恢复 Agent 已记录且能确认的输出。无法确认模型请求是否已处理时不会自动重放,以免重复计费或重复副作用。显式停止、Agent 关闭或远端进程失败会按请求状态显示取消、失败或结果未知。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 选择 Runtime
|
||||
|
||||
Runtime 决定请求由哪条执行路径处理。它与模型连接不同:连接回答“调用哪个模型服务”,Runtime 回答“由谁组织请求、工具和执行空间”。每次请求都会保留可查看的状态、工具活动、取消或失败结果。
|
||||
|
||||
## 四种选择
|
||||
|
||||
| Runtime | 适合场景 | 模型配置 |
|
||||
| --- | --- | --- |
|
||||
| 直连模型 | 普通问答、知识检索、图片生成和 GoodBuddy 内置能力 | GoodBuddy 模型连接 |
|
||||
| OpenCode | 编程 Agent、文件和命令工具、Skills | GoodBuddy 连接或 OpenCode 自有配置 |
|
||||
| Continue | 使用 Rules、Prompt 预设和编程工作 | GoodBuddy 连接或 Continue 自有配置 |
|
||||
| DeepSeek Harness | 固定 Host 和兼容插件能力 | GoodBuddy 管理的兼容连接 |
|
||||
|
||||
先用直连模型完成普通问答或知识检索;需要持续操作项目文件、命令和 Skills 时选择 OpenCode 或 Continue;需要其固定 Host 能力时再选择 DeepSeek Harness。不同 Runtime 的工具、问答和原生客户端能力可能不同,选择器会显示当前可用项。
|
||||
|
||||
## 选择范围和能力
|
||||
|
||||
项目设置必须选择具体 Runtime。会话默认跟随项目,也可以在输入框的两栏选择器中为本会话单独选择。模型栏会显示“默认”、具体连接和 Runtime 自有配置,并标明来源。切换 Runtime 后,原来不属于该 Runtime 的模型选择不会被强行沿用。
|
||||
|
||||
请求使用当前 Runtime、已启用 Capability、知识库范围、执行空间和当前账号权限。GoodBuddy 不提供 Ask / Execute 模式,也没有一个额外的通用工具审批开关;不支持的能力会明确报告,不会用隐藏模式代替。
|
||||
|
||||
## 本地、远程和记录
|
||||
|
||||
本地 Runtime 在本机项目执行。远程项目目前使用 SSH Host 上的 GoodBuddy Agent 和受管 Runtime,远程执行的文件、终端和进程使用 SSH 登录账号权限。远程 Agent 不随桌面安装包提供,需要先按[远程项目](./remote.html)准备。
|
||||
|
||||
查看请求消息的活动或助手工作栏,可以看到 Runtime、模型连接、工具状态、取消、错误和已保存结果。关闭面板不会自动取消后台请求;停止请求只影响所属请求。请求失败或取消时,已经保存的正文和工具记录会保留。
|
||||
@@ -0,0 +1,30 @@
|
||||
# GoodBuddy 用户手册
|
||||
|
||||
这是一份面向第一次使用者的操作指南。GoodBuddy 不要求注册 GoodBuddy 账号,聊天、项目和笔记默认保存在本机;模型服务、远程主机和外部知识库是否收到数据,取决于你的配置。
|
||||
|
||||
## 开始使用
|
||||
|
||||
### 安装
|
||||
|
||||
从[官网首页](https://mesalogo.github.io/goodbuddy/#download)选择系统、处理器架构和安装包。Windows 提供安装版和便携版,Linux 提供 AppImage、DEB 和 RPM。LoongArch 使用单独的实验预览通道。安装完成后启动 GoodBuddy,首次启动不需要登录。
|
||||
|
||||
### 初始设置
|
||||
|
||||
1. 打开“设置”,确认本地工作数据位置,并准备一个可用的文本模型连接。
|
||||
2. 在“模型连接”中填写服务地址、协议、模型名称和凭据,先执行连接测试。具体字段见[配置模型连接](./connections.html)。
|
||||
3. 新建一个项目,选择项目目录和 Runtime。项目用于集中管理会话、工作区和任务。
|
||||
4. 打开或新建会话,先发送一个简单问题,确认模型能正常回答,再尝试文件、知识库或远程操作。
|
||||
|
||||
### 项目和会话
|
||||
|
||||
项目是工作范围的容器,可以包含多个会话。会话保存用户消息、助手回复、工具活动和引用;同一个会话也可以关联多个任务。创建会话时,通常会继承项目的 Runtime 选择,之后可以在输入区为当前会话单独选择。
|
||||
|
||||
需要换主题或工作目录时,先切换项目,再确认右侧[助手工作栏](./workbar.html)中的工作区和终端范围。发送请求前检查当前 Runtime、模型和知识库范围,避免把内容发到错误的服务。
|
||||
|
||||
### 本地数据
|
||||
|
||||
对话、笔记、任务记录、知识库索引和设置存放在系统应用数据目录。清除本地数据是设置中的明确操作,执行前会要求确认。卸载应用不等于删除你导出的文件、远端目录或模型服务中的记录;需要了解数据流向时查看[数据与隐私](./privacy.html)。
|
||||
|
||||
### 下一步
|
||||
|
||||
先读[核心概念](./concepts.html)了解 Project、Conversation 和 Runtime 的关系,再按需要配置[知识库](./knowledge.html)、[任务](./tasks.html)或[远程项目](./remote.html)。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 监督工作
|
||||
|
||||
监督者用于回顾已发生的工作、标出变化和问题,并把有依据的结果放进故事线图谱。它不会替你发消息、批准工具、扩大文件范围或改变执行权限。监督者应用默认关闭,启用后才会接受新的监督工作。
|
||||
|
||||
## 启用和设置范围
|
||||
|
||||
在应用中心启用监督者,然后在“设置”中选择跟随默认文本模型,或选择一个已有的文本模型连接。监督执行复用连接和凭据;回顾进行中修改设置只影响后续执行。
|
||||
|
||||
首次使用需要明确选择项目、会话和时间范围。自动监督可以选择 Global 或一个或多个项目,当前项目切换不会改变已保存的范围。启用应用不会自动观察全部历史,也不会自动创建计划。
|
||||
|
||||
## 工作回顾和故事线图谱
|
||||
|
||||
工作回顾按所选范围读取会话、任务、笔记和已有来源,生成带来源的阶段总结、变化和未解决事项。没有新的有效变化时记录“无变化”,不重复调用模型或发送通知。默认“回顾”处理新增和修订内容;需要重新阅读历史区间时,使用明确的“重新整理”操作。
|
||||
|
||||
故事线图谱展示事件、实体、关系和随时间的变化。点击结果或图谱对象可以查看来源和定位。来源正文仍由会话、笔记或知识库维护,图谱不会复制一套可编辑原文。核对后可以继续讨论,也可以预览并确认写入本地知识库;外部知识库保持只读。
|
||||
|
||||
## 跟随、固定和介入
|
||||
|
||||
助手工作栏中的监督面板可以跟随当前会话,也可以固定到会话或 Task。切换页面不会改变固定目标;目标失效时保留错误并提供修复入口。监督建议只生成可编辑草稿或明确的后续操作,用户确认“发到当前会话”后才会写入聊天。
|
||||
|
||||
监督者不会自动执行建议。它不能替用户发送消息、批准工具、修改能力开关或扩大工作区。监督模型调用是无工具的内部文本请求,普通聊天和 Task 不会被监督并发设置改成同一队列。
|
||||
|
||||
## 智能心跳和失败处理
|
||||
|
||||
智能心跳是监督者的定时唤醒机制。它按计划检查指定范围的增量变化,并按设置生成建议或只更新记忆。心跳应用和计划分别有启停状态;关闭应用后不领取新的心跳,但已经开始的运行可以完成。
|
||||
|
||||
暂停、取消或失败会保留上次成功结果和已保存的成功批次,当前运行显示覆盖范围和遗漏。可以重试或继续仍有效的工作。建议生成失败不会撤销已经发布的图谱或回顾结果;活动记录会明确显示失败阶段。监督者默认关闭不会影响普通聊天和普通任务。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 任务
|
||||
|
||||
Task 是你明确创建或确认的一项工作。它只关联一条 Conversation,但一条 Conversation 可以有多个 Task。普通聊天请求和工具调用不会自动出现在任务中心。
|
||||
|
||||
## 创建立即或定时任务
|
||||
|
||||
在任务中心或会话任务区选择“新建任务”。填写任务要求,名称可以留空让 GoodBuddy 根据内容生成。默认立即执行,也可以选择单次、每日或每周计划,并选择当前项目中的当前会话、其他会话或新会话。内容、会话和计划会一起提交,失败时保留表单。
|
||||
|
||||
任务使用关联会话在实际执行时的 Runtime、能力、知识范围和工作区。它没有 Ask / Execute 模式,也不会因为后台运行而扩大权限。新会话不会复制来源会话历史;已有会话会使用自己的最新上下文。
|
||||
|
||||
## Task、Job 和 Run
|
||||
|
||||
Task 是用户可见的工作对象。每次立即运行、计划触发或重试,内部都可能创建 Job 和 Run,但它们不会变成新的任务条目。任务中心聚合显示任务状态、最近进展、计划时间和需要关注的错误;活动和 Runtime 面板显示更详细的工具与执行信息。
|
||||
|
||||
一个周期任务始终是一条 Task。取消、暂停、恢复和重新运行都作用于 Task;删除 Task 默认停止后续计划并解除关联,保留会话和已有消息。删除一次待执行的计划项不会删除 Task。
|
||||
|
||||
## 排队和控制
|
||||
|
||||
同一 Conversation 的普通消息和任务消息按队列顺序执行。当前回复期间到期的任务会等待,不会把另一条回复插入正在生成的时间线。你可以删除尚未执行的队列项,或使用“立即中断并插入”让任务成为下一项;取消或失败后,后续队列项可以继续。
|
||||
|
||||
任务卡提供暂停、继续、立即运行、编辑和取消等操作。暂停阻止新的执行,不撤销已经完成的外部操作;取消会停止所属执行,但已经保存的消息、工具记录和成果不会伪装成回滚。
|
||||
|
||||
## 任务与心跳
|
||||
|
||||
智能心跳不是任务中心里的 Task。它属于[监督工作](./supervision.html),用于按计划唤醒增量回顾和生成建议。需要一项明确的结果、文件或周期性消息时创建 Task;需要观察工作变化时配置监督计划。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 问题排查
|
||||
|
||||
先确认当前项目、会话、Runtime、模型和本机或远程执行空间,再根据症状处理。不要重复点击发送或立即重建全部资料;先查看请求状态、活动记录和错误原因。
|
||||
|
||||
## 模型连接测试失败
|
||||
|
||||
检查协议、URL、模型 ID、API Key、代理、证书和服务商限流。若填写了自定义 Header 或 Body,先暂时清空后重测基础连接。基础测试成功但生成失败时,确认当前会话选择的 Runtime 和模型能力。
|
||||
|
||||
## 回复没有使用工具或文件
|
||||
|
||||
检查 Runtime 是否支持所需能力,项目或会话是否使用正确执行空间,工作区目录是否存在,能力或 MCP 是否启用,以及当前账号是否有权限。打开工作栏的 Workspace 或 Runtime 确认目标;远程项目另外检查 SSH、Agent、远端路径和 Agent 版本。GoodBuddy 没有 Ask / Execute 模式。
|
||||
|
||||
## 知识库没有返回内容
|
||||
|
||||
确认会话勾选了目标知识库,文档已解析,全文索引可用,向量没有失败或不兼容,图片型 PDF 已做 OCR。用“测试检索”查看实际通道和降级原因;先用原文关键词测试,再重建失败文档或索引。外部库还要检查认证、远端 ID、超时和 Provider 状态。
|
||||
|
||||
## 笔记、任务或远程项目失败
|
||||
|
||||
笔记和画布先确认明确保存;AI 评论失败不会撤销已保存内容。任务检查启用、暂停、关联会话、GoodBuddy 是否运行及最近错误;心跳默认关闭。远程检查网络、Host Key、凭据、目录权限和 Agent/Runtime 准备状态。结果未知的远程请求不会自动重放,先查看活动再决定是否重新运行。
|
||||
|
||||
## HTML 预览为空或显示源码
|
||||
|
||||
确认回复已完成,内容是 HTML 文档或 `html`/`htm` 代码块,并且设置已开启会话 HTML 渲染。流式回复、用户消息和推理只显示源码。预览故意禁用脚本、网络、表单和窗口操作;需要交互页面时导出到你信任的环境。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 助手工作栏
|
||||
|
||||
助手工作栏是主窗口右侧的应用级工作区。它不属于某一条聊天消息,聊天、知识库、任务和监督页面都可以打开它,用来查看任务、工作区、Runtime、终端、浏览器、笔记和监督状态。
|
||||
|
||||
## 面板和加号目录
|
||||
|
||||
任务中心和工作区是默认存在、不可关闭的基础面板。点击“+”可以打开 Runtime、监督、终端、进程、浏览器、资源监控、文档或笔记等能力。终端和浏览器可以创建多个实例;笔记是单实例。后台状态会更新徽标或通知,不会无故把当前页面切走。
|
||||
|
||||
工作栏面板显示能力名称、实例和当前目标。能力不可用时应显示原因及配置入口,而不是把入口悄悄隐藏。面板可以收起,终端、日志和较大的成果可以按界面提供的选项停靠到底部或独立窗口。
|
||||
|
||||
## 跟随和固定
|
||||
|
||||
可绑定目标的面板有两种范围:跟随当前上下文,或固定到指定会话、Task、项目、Runtime、终端、工作区或浏览器实例。跟随模式会随项目或会话切换;固定模式保持原目标。固定目标失效时显示“目标不可用”和修复入口,不自动改到另一个目标。
|
||||
|
||||
工作栏的当前上下文只是默认值。打开工作区不会自动扩大文件范围,临时选择目录也不会静默修改项目设置。确认目标后,GoodBuddy 仍会检查项目、执行空间和当前能力。
|
||||
|
||||
## 执行空间边界
|
||||
|
||||
工作区显示当前项目目录或你明确选择的本机、远程目录。终端创建时绑定项目执行空间和目录;没有项目时使用本机 Home。浏览器只管理 GoodBuddy 创建的受控浏览器标签,不控制你已经打开的系统浏览器。进程面板只显示 GoodBuddy 创建或托管的进程,不是系统级进程管理器。
|
||||
|
||||
工作区支持文件预览、Git 状态和有界目录操作。远程工作区使用 SSH Host 上的 Agent,不把远程路径伪装成本机路径。终端输入、文件操作、浏览器和 Runtime 各自有明确范围;一个面板的选择不会给另一个面板额外权限。
|
||||
|
||||
## 实际使用
|
||||
|
||||
处理一个项目时,可以让任务中心跟随当前项目,同时把监督固定到某个会话;打开工作区查看文件,再创建一个终端执行命令。查看活动时确认面板顶部的项目、会话、Runtime 和本机或远程标识。需要完整任务关系时打开 Task;Job 和 Run 不作为工作栏中的独立面板。
|
||||
@@ -0,0 +1,21 @@
|
||||
export type Language = 'zh' | 'en'
|
||||
|
||||
export type Chapter = {
|
||||
slug: string
|
||||
title: { zh: string; en: string }
|
||||
}
|
||||
|
||||
export const chapters: Chapter[] = [
|
||||
{ slug: 'start', title: { zh: '开始使用', en: 'Get started' } },
|
||||
{ slug: 'concepts', title: { zh: '核心概念', en: 'Core concepts' } },
|
||||
{ slug: 'connections', title: { zh: '配置模型连接', en: 'Configure model connections' } },
|
||||
{ slug: 'runtimes', title: { zh: '选择 Runtime', en: 'Choose a Runtime' } },
|
||||
{ slug: 'workbar', title: { zh: '助手工作栏', en: 'Assistant workbar' } },
|
||||
{ slug: 'knowledge', title: { zh: '建立知识库', en: 'Build a knowledge base' } },
|
||||
{ slug: 'notes', title: { zh: '使用魔法笔记', en: 'Use Magic Notes' } },
|
||||
{ slug: 'tasks', title: { zh: '任务', en: 'Tasks' } },
|
||||
{ slug: 'supervision', title: { zh: '监督工作', en: 'Supervision' } },
|
||||
{ slug: 'remote', title: { zh: '远程项目', en: 'Remote projects' } },
|
||||
{ slug: 'privacy', title: { zh: '数据与隐私', en: 'Data and privacy' } },
|
||||
{ slug: 'troubleshooting', title: { zh: '问题排查', en: 'Troubleshooting' } },
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user