<span id="升级"></span>
# 升级已安装的发行版

升级行为取决于安装类型。stable 安装只在已发布版本之间变更，不跟随 `origin/main`。

0.7.0 是从 v0.6.6 进入 updater release 线的一次性过渡：Desktop 用户手动安装 v0.7.0 DMG；CLI/server 用户重新执行一次完整 release installer：

```bash
curl -fsSL https://openprogram.io/install | sh
```

安装 v0.7.0 后，后续 stable release 可使用下面的 Desktop 设置和 `openprogram upgrade` 命令。

## 桌面 release

Desktop 会自动检查最新 stable GitHub Release，也可以在“设置 → General → Application → 立即检查”手动检查。

- macOS：有新版本时选择“下载并打开 DMG”。OpenProgram 会选择与架构匹配的完整 DMG，下载到用户指定位置，验证字节数与 SHA-256 后打开。退出 OpenProgram 并替换 `OpenProgram.app`；旧的未签名安装包可能再次要求通过“隐私与安全性 → 仍要打开”授权。
- Linux：从目标不可变 tag 重新执行 release installer。当前不发布 Linux 桌面包。

应用外壳与完整 product runtime 一起替换；`~/.openprogram` 下的状态保持不变。


### macOS 本地迭代和正式发布

本地开发与对外发行使用两种签名方式：

- **本地迭代：** `scripts/refresh-local-app.sh` 使用固定的本地证书重建默认安装的开发版 App，不会把每次修改提交给 Apple。签名状态保存在仓库外并持续复用，不要为了消除权限弹窗删除它。
- **正式发布：** GitHub Actions 的 `Release` 工作流只在推送 `v*` 标签，或手动指定已有不可变标签时运行。它对 App 和内嵌原生组件进行 Developer ID 签名，等待 Apple 公证，附加并验证票据，检查完整 runtime，最后发布带校验和的安装包。普通分支推送不会触发公证。

工作流需要仓库 Secrets：`MAC_CSC_LINK`（加密 PKCS#12 的 base64）、`MAC_CSC_KEY_PASSWORD`、`APPLE_ID`、`APPLE_APP_SPECIFIC_PASSWORD`、`APPLE_TEAM_ID`，以及仓库变量 `MAC_SIGNING_IDENTITY`。这些值只保存在 GitHub 设置中，不写入源码。CI 把凭据导入临时钥匙串，签名步骤结束时删除。

重新构建已经审查的发行标签时，在 **Release → Run workflow** 指定标签，或执行 `gh workflow run release.yml -f tag=vX.Y.Z`。标签、包版本和发行说明必须一致。每次发布不需要重新创建证书或专用密码；只有凭据过期或被撤销时，才更新对应 Secret 后重试。

签名任务日志会显示公证申请编号。`In Progress` 表示 Apple 尚未处理完；`Accepted` 才允许发布；其他状态停止发布。等待超过 45 分钟不会取消 Apple 端的申请。可用 `xcrun notarytool info 申请编号 --keychain-profile 凭据名称` 查询状态，用 `xcrun notarytool log 申请编号 --keychain-profile 凭据名称` 查看拒绝原因。解决原因后，重新运行同一个不可变标签的失败工作流；新的构建可能产生新的公证申请。

频繁源码开发的机器应保留开发版 App。正式 Developer ID App 的身份不同，本地刷新会主动拒绝用开发签名覆盖它。切换这两种模式可能需要重新同意 macOS 权限；Agent 的 bypass 设置不控制 macOS 权限。


## CLI 和服务器 release

检查或升级到最新 stable release：

```bash
openprogram upgrade --check
openprogram upgrade
```

需要指定不可变 release 时使用：

```bash
curl -fsSL https://openprogram.io/install | OPENPROGRAM_VERSION=X.Y.Z sh
```

Windows 对应命令为：

```powershell
$env:OPENPROGRAM_VERSION = "X.Y.Z"
irm https://openprogram.io/install.ps1 | iex
```

命令从不可变 release tag 取得版本化 installer。installer 下载同平台 runtime archive，在暂存目录验证 checksum 和完整 capability manifest，并在发布或激活该版本前执行 worker cold-start。macOS/Linux 会串行化升级并原子切换 `current` symlink；Windows 原子替换 PowerShell launcher，并保留上一份 launcher。激活前失败时，旧版本仍保持选中，尚未发布的暂存 runtime 会被删除。版本目录会保留，因此回滚时用上一版 `OPENPROGRAM_VERSION` 重跑同一命令即可；运行中的 worker 不会自动重启。

Windows 会在激活前准备好 PowerShell 和 CMD 两个 launcher，再逐个原子替换。
第二个替换失败时，installer 会恢复第一个入口。恢复也失败时，会报需手动恢复的
错误，并在报告的路径保留原 launcher。在两个入口恢复可用之前，请保留该文件；
后续安装成功也不会自动删除它。即使 launcher 激活失败，已验证 runtime 仍会保留，
供重试复用。

升级后重启登录服务：

```bash
openprogram worker restart
```

## 恢复对话内自更新

基于源码的对话式更新控制器目前要求 macOS。Windows 和 Linux 上的聊天准备、重试
工具会在创建更新请求、占用活动更新位置之前明确说明这一点。这不影响已发布版本的
升级：通过发布安装器安装的 CLI 使用 `openprogram upgrade`，Desktop 使用发布更新器。
已有更新的状态查询和取消仍然可用；仅有 worker 服务并不代表已经具备源码更新控制器的
构建、激活、验证和恢复流程。

更新进入维护后，普通排队 Job 保持原有身份，等待维护结束。支持持久化暂停的运行任务
在下一个检查点暂停；活动 attempt 和资源占用结束后才替换 App。验证提交或回退完成后，
worker 只恢复由本次更新暂停的任务。用户原本暂停、等待输入的任务保持暂停。
不可安全暂停的任务必须在更新期限内结束，否则中止激活。检查点继续校验工具合同；
新版本改变工具实现时，不直接重放旧操作。由本次更新触发的恢复因此被拒绝时，
worker 保留旧检查点，依据原任务及历史创建一个新的规划 Job，保留工具选择和权限规则。
存在未解决的副作用或用户已取消任务时，不自动重新规划。

新创建的更新还保存具有固定 Job 身份的原会话后续任务。维护和自动修正序列完成后，
它根据原对话及更新证据继续检查目标行为。这是新的后台 turn，不授予再次安装权限。
旧更新记录不会追溯创建后续任务。安装成功与任务恢复成功分别记录。

对话内打包离线执行。依赖基线必须与候选的 `uv.lock` 和
`scripts/release/product-runtime.json` 完全一致；控制器使用保存 runtime 中固定版本的
构建工具，以及既有 npm/uv/Electron-builder/node-gyp 缓存的私有副本。Electron 平台归档
必须匹配可信控制器固定的版本、架构和发布 SHA-256，并作为 candidate 只读的
`electronDist` 输入。候选的 Web、文档、wheel 和
Desktop archive 仍重新构建。缓存缺失或依赖基线不匹配时，在激活前停止更新；
不会因此允许联网下载或沿用不匹配依赖。该构建过程及真实已安装验收仍是发布条件。

packaged worker smoke 只使用控制器选择的单个私有 loopback 端口，且不会使用默认端口
18100。浏览器自动化由重新校验过的保存 controller runtime 单独检查；该 runtime 位于
candidate 所有可写路径之外，并要求候选浏览器资源树 hash 与其一致。控制器不会执行
可写的构建 runtime 副本。两项检查都完成后才允许激活。
固定的 macOS 图标渲染检查也由可信控制器执行，不向候选构建开放宿主图形服务。

源码 checkout 的聊天工具 `self_update_prepare` 接受可选的 `verification_plan`
参数。计划包含在强制 owner 审批中，并与不可变请求一起保存。例如：

```json
{"schema":1,"checks":[{"id":"diagnostics","assertion_id":"acceptance-1","entry":"/api/diagnostics","timeout_seconds":10,"max_output_bytes":65536}]}
```

每条 assertion（`acceptance-1`、`acceptance-2` 等）必须恰好对应一个 check，共 1–32 条。
示例中的所有字段均必填，`schema` 必须是整数 `1`。
check ID 唯一，最多 64 个字符，以字母或数字开头，只允许字母、数字、下划线和连字符。
每项必须指定 1–60 秒的整数超时和 1–262144 字节的整数输出上限（`ui:main` 为 1–1572864）。
目前支持 `/api/commands`、`/api/diagnostics`、`/api/doctor`、`/healthz`、`/chat`、
`cli:version`、`cli:help`、`test:python` 和 `ui:main`。任意 URL、查询参数及不支持的字段都会在
创建更新前被拒绝；只有 `test:python` 还必须提供下述 `argv`。

重启后的 verifier 收到相同计划，调用 `self_update_observe(check_id="diagnostics")`，
不能另传 `entry` 或执行参数。执行层应用已批准的限制和原总期限；签名证据必须匹配
对应 check 和 assertion，不能复用到另一验收项。省略计划时保留原有的 HTTP-only
验收行为，不增加权限。

提供计划的更新中，验收、回退后诊断和源码修复 Job 都收到相同的已批准计划与
iteration policy，以及本次尝试的超时。修复后的子候选保留原目标、assertion、
check ID、限制、模型和权限，不能重新延长迭代总期限。诊断和源码修复仍只有各自的
读取工具；在 prompt 中包含验收计划不代表授权它们执行检查。

固定 CLI 检查使用已安装 App 的 Python 执行 `-I -B -m openprogram`，分别追加
`--version` 或 `--help`，不接受模型提供的命令，也不通过 PATH 查找解释器。
缺少原生沙箱、兼容的 runtime manifest 或匹配的包/App 构建 revision 标记时，
准备阶段拒绝 CLI 计划。执行使用独立临时目录，禁止网络访问和 App/源码写入；
除必需的 runtime 与临时执行路径外，禁止读取 owner 的 HOME。
原生验收只允许单进程：沙箱禁止创建进程，包括普通或分离的子进程、`fork` 和
`posix_spawn`。需要子进程的检查不能通过这个适配层验收。这项限制适用于这些 CLI
检查和下述候选检查，不改变独立的源码修正 required-test 或构建执行路径。
证据绑定 runtime 身份、完整调用参数和退出状态；非零退出、超时、取消、身份变化、
输出超限或清理失败均不能通过。这两个入口只检查 CLI 启动/帮助，不证明任意功能行为。

候选源码测试可以使用这样的检查项：

```json
{"id":"source-test","assertion_id":"acceptance-1","entry":"test:python","argv":["tests/verify_feature.py","expected"],"timeout_seconds":30,"max_output_bytes":65536}
```

`argv` 包含 1–32 个字符串，每项最多 4096 字符，不能包含 NUL。第一项是相对于候选
根目录、已提交的普通 `.py` 文件，不能是符号链接、绝对路径、父目录跳转或解释器
选项。脚本路径以 ASCII 字母、数字或下划线开头，其余只允许字母、数字、下划线、
斜杠、点和连字符，总长最多 511 字符。后续项作为原样脚本参数，更新前批准后，
verifier 不能修改。使用已安装候选版本的 Python，在登记的 candidate worktree
根目录执行 `-I -B SCRIPT ARGS`。隔离模式不会自动把候选根目录加入 Python 导入
路径；测试需要导入候选代码时必须显式设置。已登记的候选目录是额外允许读取的位置，
即使它位于 owner 的 HOME 下；其他 HOME 数据仍不可读取。候选源码保持只读，临时测试数据应写入
独立 `TMPDIR` 或 `HOME`，不会自动安装依赖。原生 CLI 的前置条件和限制同样适用。
源码缺失、变脏、未登记或脚本变化都会阻止验收；证据记录源码 revision、脚本摘要和
调用参数。`candidate_test` 结果只证明源码测试执行，不证明已安装 App 行为。

对原会话的 App 主窗口进行只读截图，可以使用：

```json
{"id":"main-capture","assertion_id":"acceptance-1","entry":"ui:main","timeout_seconds":30,"max_output_bytes":1048576}
```

准备阶段要求兼容的已打包 UI 验收描述文件、runtime 身份，以及恰好一个已连接的
Desktop 主窗口。安装前，候选包和回退包都必须具有匹配的截图、后端和前端能力绑定；
缺少该能力的旧包不能使用这个计划。verifier 收到 PNG 图片和 accessibility tree，
不是只有文件路径；批准的输出上限覆盖包含 base64 图片数据的整个截图 JSON。
PNG 必须为非交错的 8-bit RGB/RGBA，每个维度最多 16384，总像素最多 3200 万。
解析后的 verifier 模型必须声明支持图片输入。准备阶段拒绝纯文本模型；重启恢复在
创建 verifier Job 前再次检查该能力。如果图片能力已不可用，启动过程记录错误，
不会改用纯文本 verifier 执行验收。
已排队的 Job 也在执行入口再次检查，检查发生在调用模型之前。

截图绑定活动验收 Job、候选 revision、worker、原会话路由和确切的主窗口连接。
过期、取消、重放或身份不匹配的请求不能产生通过证据。用户输入、导航、窗口变化、
截图资源冲突、输出超限或清理不完整也会阻止截图成功。不授权任意 URL、JavaScript、
目标窗口、点击、导航或数据修改。截图期间，当前 Desktop 适配层拒绝该主窗口发起的
新页面网络请求、原生 IPC 操作和外部链接导航。该限制在证据上传前释放，失败时也会
释放，不影响其他窗口。它不撤销已经执行中的请求。原生适配层为该主窗口的 HTTP
请求添加验收标记；后端拒绝带标记的请求，
包括认证 bootstrap 和过期标记。检查期间，该窗口已有的 WebSocket 只允许观察回执
和取消确切的 verifier Job，不接受普通应用命令。

要在已批准的滚动之后截图，可添加 `interaction`：

```json
{"id":"scroll-history","assertion_id":"acceptance-1","entry":"ui:main","timeout_seconds":30,"max_output_bytes":1048576,"interaction":{"kind":"scroll","delta_y":-400}}
```

`delta_y` 为 -1200 到 1200 CSS 像素之间的非零整数，目标始终是原会话的主聊天区域，
不接受 selector 或脚本。候选包和回退包都必须具有 UI protocol 2，绑定实际原生滚动
适配层、后端限制和编译后的 UI 位置记录限制。Protocol 1 包仍只能截图，在安装前
拒绝滚动计划。截图和 accessibility tree 对应滚动后的状态；签名证据还记录滚动前、
滚动后及恢复后的位置数据。成功前恢复原位置，不保存临时滚动位置。截图失败时，
只在原目标和原期限仍有效时恢复自己的滚动。用户操作中断检查时，不恢复覆盖用户的
新位置。目标变化或恢复失败均不能通过。在滚动边界，位置可能不变；仅凭这一点不能
证明要求发生位移的断言。

要检查原会话的上下文图，可冻结视图检查：

```json
{"id":"context-view","assertion_id":"acceptance-1","entry":"ui:main","timeout_seconds":30,"max_output_bytes":1048576,"interaction":{"kind":"view","target":"dag"}}
```

`target` 只能为 `session` 或 `dag`，不能是 URL 或其他会话。检查从可见的原会话开始，
触发实际的视图切换控件，捕获指定视图，并在成功前恢复会话视图和滚动位置。开始时
已经选中 DAG、控件不存在或被替换、用户中断、恢复失败均不能通过。`session` 目标
不会切换视图，不能证明切换发生。临时视图不持久化，后台标签更新也不能保存它。
用户中断时，适配层不会强制恢复原视图或滚动位置而覆盖用户的选择。恢复失败时，
界面可能停留在请求的视图；检查报告失败，不宣称清理成功。
候选包和回退包都要求 UI protocol 3，额外绑定编译后的视图支持。Protocol 1 和 2
保留原有截图、滚动能力，但不能接受视图检查。

不支持临时测试对象重命名对话框。新验收计划和旧计划的执行请求如果使用 `test_object`，会在请求界面操作前被拒绝。旧 protocol 4 安装包仍可读取，以保留回退兼容性；新安装包只声明支持的截图、滚动和视图检查能力。

截图只能支持对所捕获状态的断言；没有观察到的
交互行为仍应判为 inconclusive。HTTP 响应（包括 `/chat` HTML）不能证明 App 界面
实际行为。其他副作用操作不受支持；完整验收能力和实际已安装 App 验收仍待完成。

这项源码 checkout 能力与 stable release 升级分开。macOS 上，对话内自更新使默认
App 保持维护状态时，可以在本地终端检查，不需要启动 Agent：

```bash
openprogram self-update status --json
openprogram self-update status UPDATE_ID --json
openprogram self-update repair UPDATE_ID
```

将 `UPDATE_ID` 替换为 `status` 返回的 ID。恢复要求交互式终端，并准确输入与操作、
版本和计划摘要一起显示的确认内容。没有 `--yes` 或强制解除维护选项。已授权的操作
只能在原来的十分钟期限内恢复执行；失败或过期后需要重新确认。

恢复使用更新前保存的控制程序。仍可回退时恢复旧 App；已经开始不可逆提交时，
只有原始验收通过证据完整才能完成提交。已取消且尚未激活的事务保持旧 App。
证据缺失或变化时继续保持维护状态。恢复会重启默认 worker，检查 App 身份和实际
服务，然后解除维护。它不会新建验收 Job，也不会改变原更新结论。独立的恢复结果
记录恢复了哪个版本；服务恢复不代表失败的功能已达到原始目标。

新的对话更新请求同时冻结一个只读诊断阶段。确认旧版本恢复后，worker 在原会话中
创建一次 **Post-rollback diagnosis** Job，使用已批准的模型和 profile 读取失败证据，
报告原因与修正建议。该任务不能修改源码、执行 shell、安装软件或授权下一次更新，
也不覆盖原验收和回退结论。

诊断期限最多为回退后的五分钟；原迭代授权期限更早时取更早值。重启不会重新计时，
已终止的 Job 不重跑。通过普通 Job 取消入口停止诊断；`self_update_cancel` 仍只取消
激活前的更新。新更新会取代待处理诊断。模型不可用或证据无效只终止诊断，不重新
限制已经恢复的服务。`self-update status --json` 在有结果时包含 `diagnosis_result`。
缺少冻结诊断配置的旧请求保持原有行为。诊断报告本身不修改或安装代码。

新请求独立冻结 **Post-rollback source repair** 阶段。首次授权包含隔离修正与所列
`required_tests`。默认模式在再次安装前另行审批；显式批准的 `bounded_auto` 同时允许
在原限制内再次安装。implementation/test 诊断可触发一个只读模型 Job
提出文本编辑，由控制器逐项校验、新建 linked worktree 和 commit，并在无网络、无
App 写权限的原生沙箱运行冻结测试。原 worktree 不变。默认只修改原 changed_paths；
`bounded_auto` 使用原授权路径模式。受保护的 runtime、审批、安装器、依赖和 Git
文件不会自动修改。无效路径或 old_text 无法唯一匹配会停止修正。

修正和测试共用回退后的十分钟期限，原授权期限更早时取更早值。新更新、取消或
超时停止本阶段及测试进程。模型运行时可使用普通 Job 取消；模型结束后，在原会话
调用 `self_update_repair_cancel(update_id)` 取消仍运行的测试。重启不重放部分编辑、
提交或测试。失败 worktree 和证据保留供检查。缺少冻结修正配置的旧请求不获得此能力。

`self_update_status` 和 `self-update status --json` 包含 `source_repair_result`。
聊天工具与需要 owner 认证的 Web 历史接口
（`GET /api/self-updates?session_id=…`）和详情接口
（`GET /api/self-updates/{update_id}?session_id=…`）使用同一份只读投影。
历史包含已结束的 attempt，支持有上限的 `limit` 和 `cursor` 分页。
`candidate_revision` 与 `target_app` 表示请求的安装目标；
`last_verified_runtime` 包含最近一次匹配验证的 SHA、PID、时间和来源，
没有对应证据时为 null，不表示 worker 仍在线。`state_revision` 是状态计数器，不是 Git revision。
可使用投影中 verifier 的 `evidence_id` 或某条 assertion 的 `evidence_refs` 值调用
`GET /api/self-updates/{update_id}/evidence?session_id=…&evidence_id=…`。
此接口要求 owner 认证和原会话，只返回已校验签名结果引用的观察记录，不支持读取任意文件，
不返回凭据或配置。保存的 HTTP/HTML 响应文本不能证明 App 窗口已实际渲染；证据被修改或
校验失败时返回错误。

查询不初始化或修复更新状态，损坏状态返回错误，不显示为空历史。
Running 无法读取更新快照时返回 `self_update_error`。
投影不包含凭据、原始日志和配置；修正摘要包含状态及已生成的新 candidate SHA。
这不替代 CLI 独立的恢复检查输出。

会话只显示正常消息和工具结果，不挂载更新历史、更新操作按钮、证据查看器或恢复提示，也不轮询更新历史接口。显式状态工具和已认证的查询接口仍可使用。

`candidate_ready` 表示新 commit 与全部已配置测试通过校验；未配置必需测试时为
`awaiting_tests`。测试缺失、失败或源码漂移记录 `failed`；取消与超时分别为
`cancelled`、`expired`。这些状态均不代表已安装。

对已测试的修正版本，在原 owner 会话调用 `self_update_retry(update_id, candidate_sha)`。
即使使用 bypass，也必须进行单次审批，显示精确 SHA、变更路径、测试和剩余预算。
批准返回后重新检查 Git 内容和测试日志；任一内容变化都会拒绝提交。
`awaiting_tests` 候选不能通过这个入口批准，需要 owner 发起明确测试的新请求。

新的 `bounded_auto` 请求必须包含未来的总 `deadline`、允许路径和非空 `required_tests`。
只有独立冻结了迭代授权的请求才可自动提交后继版本；旧请求仅有 mode 字段不会获得权限。
每个 child 保留原目标、assertions、源码基线、model/profile 和 policy。
首次更新计为 attempt=1，最多三次包含首次。预留 child 就消耗一次预算，提交失败也不退还。
重启复用原 child 和期限，不重置预算。

`self_update_iteration_cancel(update_id)` 停止整个序列，包括待批准操作、诊断、源码修正和
候选测试。已进入激活或验收的 child 必须完成事务或安全回退，但不能再创建后续 attempt。
其他普通 Job 不受影响。

聊天工具 `self_update_status` 的 `iteration` 字段提供 root、parent、attempt 限制、总期限和
提交状态。`submitted` 只表示请求已交给外部 supervisor，不代表安装成功。
child 仍须完成打包、系统检查、新验证 Job，以及 commit 或 rollback。
真实已安装 App 的验收是独立发布条件；fixture 测试不能证明当前 App 已更新。

worker 还会把有限的更新结果持久化到原会话，即使窗口已关闭，或者更新在创建验证 Job
之前就停止。worker 再次检查待投递结果时，仍使用相同的 update、attempt 和结果类型标识，不启动新的模型轮次，
也不移动会话 HEAD。写入中断后会重试；原会话或发起操作的 assistant 节点不存在时，
结果保持待投递，不重建会话，也不改投其他会话。结果持久化不会重新打开 App，
也不能证明安装成功；安装结论仍以更新阶段和已验证的运行证据为准。

Desktop 已具备接收本次更新专用受信任会话恢复请求的能力。它通过 owner 认证只解析
原会话，并在主窗口实际加载会话记录后确认。请求过期、会话删除或认证失败时仍可普通
启动，不增加恢复提示。用户切换页面后停止自动定位；恢复原因保留在内部状态中。恢复与加载确认都不会创建新的验证 Job，也不证明更新成功。
控制器现在会在激活前持久化恢复意图，并把不透明更新 ID 绑定到安装事务。
App 原本打开时，激活与回退都使用该 ID 重新打开；原本关闭时保持关闭。
普通 App 启动不会消费这些恢复请求。

候选与已安装 App 都必须包含匹配的打包恢复协议声明。声明缺失、不兼容或内容变化、
事务更新 ID 不匹配、冻结的 owner 配置无效，都会在替换旧 App 前阻止激活。
等待结束及恢复已准备的更新后，控制器会重新检查这些输入。打包和本地刷新脚本根据
实际 Desktop archive、installer、runtime manifest、后端及编译后的 Web 文件生成声明。
没有该声明的旧 App 需要先进行显式完整更新，才能使用对话恢复。
源码连接已经过 fixture 测试；真实已安装 App 的重启及会话/标签恢复验收仍待完成，
因此这还不是已可发布的完整端到端功能。

App 或普通 CLI 无法启动时，使用本次更新保存的独立入口：

```bash
"$HOME/.openprogram/self-updates/UPDATE_ID/recover.sh" status
"$HOME/.openprogram/self-updates/UPDATE_ID/recover.sh" repair
```

脚本使用 App 外保存的原 runtime，不传参数时默认只查状态。`repair` 仍要求 owner
在交互式终端确认，不绕过证据校验或过期授权。`recover.sh resume` 在原授权和期限内
调用原 supervisor，不批准新更新，也不重新创建验收 Job。

激活前，OpenProgram 还在本用户的 `~/Library/LaunchAgents/` 中发布
`ai.openprogram.self-update.recovery.UPDATE_ID.plist`。它在之后每次用户登录时运行一次，
不依赖 App；没有常驻进程或周期重试，写入文件也不会立即启动另一个控制进程。
新恢复任务使用 launchd 的 `Standard` 进程类型，运行环境完整性读取不再按可延后的后台工作调度。
已保存的 `Background` 登录项保留原始绑定并继续受支持，恢复时不改写其调度策略。
恢复不在用户登录或磁盘解锁之前执行。当前登录会话中 App 和控制进程都停止时，
显式运行保存的脚本。已结束的更新在同一状态锁内验证保存的运行环境，并仅清理内容仍一致的本次登录文件，
清理时不再重复计算整个运行环境的哈希。恢复过程在运行环境验证和恢复 controller 前，将进度写入 `bootstrap.log`。保存的 runtime、
脚本和证据继续保留。可信恢复文件缺失或损坏时需人工处理，不从未验证的 App 重建。

## 开发 checkout

本地已安装的 macOS App 在提交改动后，从当前 checkout 运行 `scripts/refresh-local-app.sh`。脚本重新构建 wheel 与 Desktop 资源，在退出 App 之前先构建好 GUI、Research、Wiki 三个 Program 的 wheel（源码构建失败时已安装的 App 保持不变），再更新 PATH 和 App 内置的 Python 安装，并同步内置 Node 与 Ink 资源和 App 图标。完整内置运行环境通过实际检查后，脚本才重新生成能力清单并重启默认 worker。新增必需能力时，这个流程也会更新旧 App 的运行环境清单；检查失败会终止刷新，不会将未验证的能力标记为已验证。

复制后的 Node 可执行文件必须能够脱离原安装目录运行。脚本会在修改 App 之前检查这一点。如果 PATH 中的 Node 依赖相邻动态库，运行刷新脚本时将 `OPENPROGRAM_NODE_BIN` 设为独立 Node 可执行文件的路径。

刷新后的 worker 使用 App 内置 Python 和 `-I -B`。已有 launchd 服务会重新绑定到该解释器，独立进程启动也使用同一解释器。健康检查后，脚本核对真实 worker 进程的可执行文件及参数；包版本一致不等于解释器一致。已有 checkpoint 继续严格验证工具实现合同；切换 Python 安装路径可能导致实现指纹不一致。

在 source checkout 中，`openprogram upgrade` 执行开发升级流程，而不是 release installer。它验证 Git 目标，仅在相关源文件变化时更新依赖与构建产物，probe 新 checkout，并且只在 probe 成功后重启 worker：

```bash
openprogram upgrade --check
openprogram upgrade --dry-run
openprogram upgrade
```

历史 `openprogram update` 命令作为 `openprogram upgrade` 的兼容别名保留。

source checkout 的恢复细节见[服务器升级](../server/upgrading.zh.md)。
持续维护的架构、信任边界、界面状态和实现证据见
[正式版本自动更新](../reference/design/distribution/automatic-updates.zh.html)。
