- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
导读
本文围绕 Kun 开源仓库中的fix-linux-graph-shutdown-validation变更提案,完整讲解其如何解决 Linux 打包任务在dist:linux之前因 Graph 调度器关闭恢复测试超时而阻断发布门禁的问题,并同步梳理该变更在 Windows 安装器迁移、AppImage 发布门禁、桌面 smoke 数据隔离等方面的配套加固。读者读完可以掌握:Kun Graph 调度器关闭语义(双快照、活跃 Promise 排空)、如何在不改动生产代码的前提下调整测试预算来兼顾「正常 CI 延迟」与「真实死锁检测」,以及 Windows 迁移清理如何借助准备日志保持 fail-closed 授权边界。
一、问题背景:一次 500ms 假设引发的 Linux 发布门禁阻塞
在 Kun 的 Linux PR/发布打包流程中,npm run test:graph:platform必须先于npm run dist:linux执行(见 kun/package.json 中 Graph 平台测试相关的脚本组织)。然而原生 Linux 打包任务此前会在打包之前就失败,根因不在运行时代码,而在测试本身:
Graph 调度器的关闭恢复测试把「基于文件持久化的清理」假定为一次 500ms 量级的操作。在正常负载的 Ubuntu CI 上调度器其实能正确完成关闭,但测试因超时误报,从而阻塞了所有 Linux 打包与发布门禁。
也就是说,这是「测试预算与真实运行环境不匹配」的问题,而非调度器存在死锁缺陷。变更提案的核心理念在 proposal.md 中表达得非常克制:只改测试与门禁 fixture,不触碰 GraphScheduler 生产行为、公开 API、electron-builder 配置和用户可见的发布说明。
二、核心修复:关闭恢复断言预算统一提升到 5 秒
2.1 测试场景本身是什么
关闭恢复测试要验证的是 Kun 调度器在「宿主关闭」时的两个关键行为:
- 首快照后的迟到准入(late admission)被中断:调度器暂停写租约准入,调用
GraphScheduler.stop(),再释放准入,验证一个在首个关闭快照之后才被准入的尝试会被中断、并排除在有效尝试计数之外; - 跨关闭与重启不消耗唯一的工作进程尝试:
maxAttempts: 1的计划在关闭时中断、重启后重试,最终成功,且有效尝试计数正确。
对应实现位于 graph-scheduler-shutdown-recovery.test.ts,其测试常量(第 18-20 行)如下:
const schedulerWaitTimeoutMs = process.platform === 'win32' ? 30_000 : 10_000 const schedulerStopTimeoutMs = 5_000 const schedulerTestTimeoutMs = process.platform === 'win32' ? 60_000 : 15_000其中schedulerStopTimeoutMs = 5_000正是本次修复的核心:所有平台统一给GraphScheduler.stop()5 秒完成预算,与 Windows 已有的宽限保持一致,同时保留测试整体 15 秒超时(Windows 上为 60 秒)。
2.2 为什么只改测试常量而不改生产代码
design.md 明确给出了决策依据:
- 调度器在
stop()中已经做了完整且正确的关闭编排:设置停止围栏(stopping fence)、清空定时器、对活跃尝试逐一abort、进行第二次活跃尝试快照、中止迟到准入、Promise.allSettled等待活跃 Promise 与 peer review 收尾、停止租约心跳并排空清理任务——见 graph-scheduler.ts; - 文件持久化在负载较高的 Ubuntu runner 上可能超过 500ms,这属于正常延迟,不是死锁;
- 若为「迎合测试」在生产代码中新增检查点,会改变已经过审计的关闭行为,且没有运行时缺陷证据;
- 移除
Promise.race的方案被否决:未来若调度器真的死锁,测试仍应在断言边界上失败,不能把死锁检测变成无限制等待。
因此决策是「Change only the test constant」——保持 15 秒整体超时与完整的持久化状态断言(interrupted状态、GRAPH_HOST_SHUTDOWN_ATTEMPT_FAILURE归一化失败、effectiveRunAttemptCount为 0),只把 stop 断言预算从 500ms 提升到 5 秒。
2.3 测试中的 no-deadlock 守卫如何工作
看「catches an attempt admitted after the first shutdown snapshot」用例(graph-scheduler-shutdown-recovery.test.ts):
const stopping = harness.scheduler.stop() releaseAdmission() await expect(Promise.race([ stopping, new Promise<'timed_out'>((resolve) => setTimeout(() => resolve('timed_out'), schedulerStopTimeoutMs)) ])).resolves.not.toBe('timed_out')测试先用vi.spyOn(harness.writes, 'acquire')挂起写租约准入,触发首个关闭快照后再释放准入,令「快照后的迟到准入」实际发生;随后用Promise.race同时等待scheduler.stop()与 5 秒计时器——若 5 秒内 stop 未完成,竞速解析为'timed_out'即视为疑似死锁失败。关闭完成后,测试断言该尝试被持久化为:
expect(parked.nodes[source.id]).toMatchObject({ status: 'ready', attempts: [expect.objectContaining({ status: 'interrupted', normalizedFailure: GRAPH_HOST_SHUTDOWN_ATTEMPT_FAILURE })] }) expect(effectiveRunAttemptCount(parked)).toBe(0)即:迟到尝试以「宿主关闭失败」持久化为interrupted,有效尝试计数为 0——这意味着重启调度器后,唯一的一次maxAttempts配额不会被关闭所消耗(第一个用例验证了重启后执行两次、有效计数为 1 并完成运行)。这组持久化语义来自 graph-scheduler-policy.ts 中的GRAPH_HOST_SHUTDOWN_ATTEMPT_FAILURE与effectiveRunAttemptCount。
三、配套的测试预算调整:监督活跃性、根 Vitest 与 Windows 保留往返
3.1 监督活跃性 fixture:多检查点场景让位给 60 秒 Vitest 预算
Graph 监督活跃性测试(graph-scheduler-supervision-liveness.test.ts)驱动四个彼此独立且有界的调度器检查点:两次「纯散文式」Lead 剧集重投递、评审完成运行、隔离的合成失败 fixture、独立就绪分支并发。原先的外层 20 秒超时在繁忙的 Windows runner 上可能先行耗尽——即使没有任何检查点表现出死锁。
修复后supervisionLivenessTimeoutMs = 60_000(第 19 行),即该 fixture 使用仓库既有的 60 秒 Vitest 上限;每个 10 秒的调度器状态等待(waitFor)与全部活跃性断言保持不变。同时在异常路径上,测试会输出leadEpisodes、各节点状态、obligation 的noProgressCount、lease 状态、最近事件序列等诊断信息(第 155-176 行),便于在慢 runner 上区分「等待未满足」与「真实死锁」。
3.2 根 Vitest:Windows 上 15 秒/测试、保留双 worker 并发
根级 Vitest 配置在 Windows 平台将每个测试的预算提升到 15 秒,同时保留既有的双 worker 并发。原因是原生文件系统迁移操作与同步的 Windows PowerShell helper 在 runner 负载下可能超过跨平台的 5 秒默认值;迁移断言、进程退出检查和 fail-closed 行为全部不变。
3.3 Windows 保留往返测试:三个原生 PowerShell 进程的 15 秒预算
聚焦的迁移测试会同步启动 PowerShell 依次执行Prepare、FallbackCleanup、Restore三个原生辅助进程。原生 runner 启动可能把「正确但较慢」的往返推过 Vitest 默认 5 秒;该测试单独获得 15 秒上限,所有行为断言(准备、清理、恢复、日志)保持完整。这个 fail-closed 原则很重要:放宽的是时间预算,不是校验强度。
四、AppImage 发布门禁:显式传入解压路径
4.1 问题的本质:resolve(undefined)的隐式路径解析
桌面 smoke 启动器现在会以「已校验的AppRun+APPDIR/APPIMAGE环境变量」的方式启动经过解压验证的 AppImage。发布门禁 fixture 此前没有显式提供解压后的 AppDir 与AppRun路径,导致校验的是resolve(undefined)这种隐式解析结果,而非与打包 smoke 完全一致的调用形态。
4.2 fixture 中的显式路径契约
在 check-extension-release-gate-packaging.mjs 中,发布门禁测试通过createAppImageSmokeInvocation构造最终调用并断言其形状:
const invocation = packagedAppImageSmokeModule.createAppImageSmokeInvocation({ appImage: '/release/Kun-1.2.3-linux-x86_64.AppImage', appRoot: '/extract/squashfs-root', appRun: '/extract/squashfs-root/AppRun', resourcesDir: '/extract/squashfs-root/resources', desktopSmokePath: '/repo/scripts/smoke-packaged-extension-desktop.cjs', environment: { APPDIR: '/untrusted', APPIMAGE: '/untrusted', ELECTRON_RUN_AS_NODE: '1' } })随后断言:命令以process.execPath启动、APPIMAGE_EXTRACT_AND_RUN与ELECTRON_RUN_AS_NODE均被清空、APPDIR解析为解压根、APPIMAGE解析为原 AppImage 文件、--desktop-executable与AppRun/resources路径出现在参数中、不携带app.asar——即「启动已校验的 AppRun,同时不做 AppImage 自解压」的调用形态。配套的 smoke 实现见 smoke-packaged-extension-appimage.cjs(其中inspectExtractedAppImageBundle校验解压根、AppRun常规文件与可执行位)。
4.3 配套断言:可执行文件格式与启动参数
同一门禁还校验 Linux launcher 内容包含Exec=AppRun --disable-setuid-sandbox --no-first-run %U(check-extension-release-gate-packaging.mjs),并与 electron-builder 配置中的executableArgs: ['--disable-setuid-sandbox', '--no-first-run']对应——这与 after-pack-launchers.cjs 中「AppImage 可能通过 PATH 调用 AppRun」的注释所描述的问题一脉相承。
五、Windows 迁移辅助程序的 fail-closed 加固
5.1 准备日志成为回退清理的授权记录
Windows 迁移 helper(windows-installer-migration.ps1)的Prepare动作在旧卸载器运行之前,就已经完成源身份、安全根目录、reparse point 边界与已识别 payload 的校验(helper 通过switch ($Action)分发Prepare/FallbackCleanup/Restore等动作,见第 198-207 行)。
修复的核心变化:Prepare现在记录每一个通过校验的安装源——包括没有任何未知内容需要保留的源。原因在于旧卸载器会在回退清理之前合法地删除Kun.exe身份可执行文件,导致「凭身份可执行文件验证源」这条路在清理时已经失效。若没有匹配的准备记录,回退清理仍然保持 fail-closed:除非源仍等于安装目标且保留有效身份可执行文件,否则拒绝清理;有记录时,只删除已识别的应用 payload。
对应的需求场景写入 linux-graph-ci-validation/spec.md:
- WHEN准备校验了已注册源、且旧卸载器随后移除其身份可执行文件
- THEN回退清理通过匹配的准备记录接受该确切源,且只删除已识别的 payload;
- 未准备的源若与安装目标不同或已无身份可执行文件,则被拒绝。
5.2 NSIS 本地恢复引号包裹的注册源
electron-builder 会在启动旧卸载器之前解析其引号包裹的UninstallString,但其installUtil.nsh的宏在customHeader之后才加载。Kun 因此在customHeader内自持等价的引号路径解析与父目录函数(见 packaging-config.hooks.test.ts 中对ReadRegStr $R9 HKEY_CURRENT_USER "${UNINSTALL_REGISTRY_KEY}" UninstallString的断言),从而:
- 避免了对 electron-builder 后续宏的依赖;
- 避免了「跨进程
ResolveSource+ 环境变量/UTF-16 结果文件」往返返回空源的失败路径。
PowerShell 侧仍然对最终源执行规范化,并强制全部目标、身份、payload、日志与 reparse point 检查——NSIS 只负责「在customHeader阶段自包含地恢复出非空源」,校验职责不转移。
5.3 仅当 smoke 主动选择时才暴露安装器诊断
辅助程序会把动作边界与被捕获的错误追加到KUN_INSTALLER_DIAGNOSTIC_PATH指定的路径(见 windows-installer-migration.ps1 的Write-InstallerDiagnostic,以及 installer.nsh 中对该环境变量的读取与设置)。Windows smoke 在自己的可清理根目录内设置该路径,仅在安装器出现非预期退出码时打印日志;诊断写入失败被忽略——普通安装器运行不设置该环境变量,就不会产生这份日志,生产行为完全不变。
5.4 作用域迁移:退休旧的 HKCU 注册与快捷方式
当 all-users 安装成功退休经过校验的 current-user 安装时,electron-builder 只会在此时调用customUnInstallCheckCurrentUser回调(packaging-config.hooks.test.ts 断言脚本包含该回调)。Kun 的回调在支持校验式回退清理的基础上,还会:
- 读取注册的快捷方式元数据;
- 移除旧用户的已注册、规范化 Kun 与遗留 DeepSeek GUI 桌面/开始菜单快捷方式;
- 在清理成功后,精确删除
HKEY_CURRENT_USER下的 KunINSTALL_REGISTRY_KEY与UNINSTALL_REGISTRY_KEY(测试断言DeleteRegKey HKEY_CURRENT_USER "${UNINSTALL_REGISTRY_KEY}"与DeleteRegKey HKEY_CURRENT_USER "${INSTALL_REGISTRY_KEY}",见第 379-382 行); - 不删除任何动态或父级注册表路径,shell 上下文在安装继续前恢复为 all-users(测试同时断言
DeleteRegKey SHELL_CONTEXT ...仅作用于固定键,第 451-452 行)。
清理只发生在 fail-closed payload 清理成功之后;空记录路径(无未知内容)也在覆盖范围内。
六、桌面 smoke 的 appData 显式隔离
6.1 从 Known Folder 发现依赖中解脱
桌面 Chromium smoke 原本就会创建并填充隔离的 app-data 目录。修复后,当专用环境变量存在时,主进程 bootstrap 会在第一次app.getPath('appData')之前调用app.setPath('appData', ...)把 Electron 的appData特殊路径绑定到该目录。实现位于 app-identity.ts:
export function configureDesktopSmokeAppDataPath( env: NodeJS.ProcessEnv = process.env ): string | undefined { if (env.KUN_PACKAGED_EXTENSION_DESKTOP_SMOKE !== '1') return undefined const appDataPath = env.APPDATA?.trim() if (!appDataPath) { throw new Error('The isolated desktop smoke requires an APPDATA path') } app.setPath('appData', appDataPath) return appDataPath }判定函数isPackagedExtensionDesktopSmoke()见 main-lifecycle.ts(process.env.KUN_PACKAGED_EXTENSION_DESKTOP_SMOKE === '1')。
6.2 三个边界条件
- 有隔离目录(
KUN_PACKAGED_EXTENSION_DESKTOP_SMOKE=1且APPDATA非空):绑定后 smoke 不再依赖 Windows Known Folder 发现——这在安装器变更测试之后尤其重要,因为系统级目录状态可能已不可靠; - 无
APPDATA:隔离 smoke 直接 fail closed 抛出错误; - 普通启动:不设置专用环境变量,绝不覆盖 Electron 的
appData路径,打包与日常启动行为零影响。
七、验证路径:本地串行复跑与原生 CI 门禁
按 tasks.md 的验证计划:
- 本地验证:关闭恢复测试串行跑 10 次全部通过;
npm run test:graph:platform下 Kun 与 renderer 的 Graph 套件通过;随后跑npm run build:kun、npm run typecheck、npm run lint、npm run test、npm run build与git diff --check,并重跑聚焦的打包配置与发布门禁检查; - 原生 CI 验证:当前开发宿主为 macOS arm64 且 Docker daemon 不可用,因此 Linux 原生验证由 PR CI 承担——Ubuntu runner 运行的是与打包完全一致的 Node 22 依赖链及全部下游 Linux smoke/evidence 门禁;提交分支
codex/fix-linux-graph-shutdown-timeout面向develop开 PR,绿色后才合并进发布 PR; - 发布:合并进
develop、等待发布 PR 刷新并通过全部门禁后进入master,验证自动化的v0.2.34版本与跨平台资产及 R2 stable/latest 提升;若计算版本不一致则停止。
design.md 还记录了三个风险与缓解:
| 风险 | 缓解 |
|---|---|
| 真实关闭死锁现在最长 5 秒才在 race 处失败(而非 500ms) | 既有 15 秒整体超时 + race 之后完整的持久化状态断言兜底 |
| Mac 本地测试无法证明 Linux 原生 AppImage 行为 | 合并与发布前强制通过原生 Linux PR 任务 |
| 准备记录可能授权错误的 Windows 目录 | 记录只在安全根、应用身份、已识别 payload、reparse point 检查全部通过后写入 |
八、结论:不改运行时,用「预算治理」疏通发布流水线
fix-linux-graph-shutdown-validation是一个教科书式的「测试基建修复」:它没有声称调度器有缺陷,而是精确诊断出「基于文件持久化的关闭清理在负载 CI 上超过 500ms」这一环境事实,然后通过三组互不干扰的改动疏通流水线:
- 时间预算的合理放宽——关闭断言 5 秒、监督活跃性 60 秒、根 Vitest 与保留往返 Windows 15 秒,全部保留既有断言与超时兜底;
- 门禁 fixture 的显式契约——AppImage smoke 显式传入解压
appRoot/appRun,与打包启动器调用形态对齐; - Windows 迁移的 fail-closed 增强——准备日志授权回退清理、NSIS 本地恢复引号注册源、可选诊断、HKCU 注册与快捷方式的精确退休,以及桌面 smoke 的
appData显式隔离。
最终的验收标准是独立能力linux-graph-ci-validation(spec.md)所表述的:原生 Linux Graph 测试必须容忍持久化关闭延迟,同时仍能检测真实的关闭死锁,从而放行下游 Linux 打包与 smoke 门禁。对于同样在「测试超时阻塞发布」泥潭中的项目,这套「先定位假设错误、再只改测试边界、最后用原生 CI 兜底」的方法论,比盲目加大超时或粗暴跳过测试都更值得借鉴。
- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
相关推荐
Kun 的 Linux Graph 关闭验证与跨平台发布门禁加固:从超时修复到 CI 套件门禁的完整方案
Kun 的 Linux Graph 关闭验证与跨平台发布门禁加固:从超时修复到 CI 套件门禁的完整方案 本篇技术指南以 Kun 仓库 OpenSpec 变更记
人工智能AI Agent自主智能体桌面应用MCP ClientsKun 项目 Linux Graph 关停验证修复:CI 门禁超时预算与跨平台回归治理实践
Kun 项目 Linux Graph 关停验证修复:CI 门禁超时预算与跨平台回归治理实践 本篇文章围绕 Kun 仓库中 OpenSpec 变更 fix lin
人工智能AI Agent自主智能体桌面应用MCP ClientsKun 平台关机校验修复实录:Linux Graph 测试预算、AppImage 门禁与 Windows 迁移器加固
Kun 平台关机校验修复实录:Linux Graph 测试预算、AppImage 门禁与 Windows 迁移器加固 Kun 是一个本地优先的 AI Agent
人工智能AI Agent自主智能体桌面应用MCP Clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考