版本兼容矩阵与升级窗口
本文定义平台各组件在升级或回滚时的支持边界,对应 AR-05 / W05。09-30 历史复核撤回了旧 ORM 缺列时的兼容结论;补偿后,10-01 M05 实机以完整 4c7eee8 镜像和自己的锁文件验证 PG/SQLite 旧入口、迁移中断、N 业务及保留 schema 的旧应用回滚。此前双副本浏览器定向证据保持有效;本轮补公共 Kit 与产物组合。M04 外部交接及其它发布/容量门的状态见已归档收尾计划;部署形态收敛后需要重开的门见现行执行计划。
1. 组件与版本轴
| 组件 | 版本标识 | 取值来源 |
|---|---|---|
| Backend | Release tag / Git SHA / 镜像 digest | 发布流水线;alembic_version 另记 DB revision |
| Runtime(preview/build/check) | 同一镜像 tag + runtime_kit_version + build_id | /__runtime_healthz |
| Renderer | Release tag / 镜像 digest | capabilities.protocol_version = internal/render/v1 |
| Runtime Kit | runtime-kit.manifest.json 的 version(当前 1.0.0)与导出 .vN | 清单与 @runtime-kit import path |
| 渲染契约 | PROTOCOL_VERSION = internal/render/v1;RUNTIME_RENDER_PROTOCOL_VERSION = render-ready.v1 | packages/render-contracts |
| 数据库 | Alembic alembic_version | backend/migrations/versions/ |
| 页面/组件产物 | 预览 artifact、构建 ZIP、组件 previewSchema | 存储中的产物元数据 |
2. N / N-1 支持矩阵(目标约定)
N/N-1 必须绑定具体版本。本轮 N 为 73b0036 + 迁移事务/Renderer 错误映射补丁,Runtime 复用 e82c793;N-1 固定 4c7eee8,是选定 Git 稳定点,不是任意前一 Release 的兼容保证。本地镜像 ID 和源码对拍见实机记录,未推送 Registry。
| 组合 | Backend N | Runtime N | Renderer N | DB revision N | Backend N-1 | Runtime/Renderer N-1 | 结论(M05 实测后) |
|---|---|---|---|---|---|---|---|
| A | ✓ | ✓ | ✓ | ✓ | — | — | 同版本全量,支持 |
| B | ✓ | ✓ | ✓ | ✓ | ✓ | — | 补偿 schema 上的逐版本业务通过,迁移中混跑不支持:完整旧依赖/镜像的 ORM 读写、页面创建/取消/自动续跑通过;仍先排空停旧,再迁移并启 N,未承诺 N/N-1 同时抢业务队列 |
| C | ✓ | ✓ | ✓ | ✓ | — | ✓ | 按角色判定:N Runtime + 旧 Renderer 的实际 PNG/ZIP 与加载通过;4c7eee8 Runtime 缺发布身份,N 正常预览 503,截图链路不能支持。独立构建与产物加载另记,协议/Kit 一致不能代替入口兼容 |
| D | ✓ | — | — | N-1 | — | — | 迁移窗口内旧 DB + 新 Backend:不支持(N ORM 需要新列);必须先 upgrade 再启 N。N-1 + 旧 DB 可运行 |
| E | — | — | — | ✓ | ✓ | ✓ | 固定样本完整应用回滚通过:两种数据库均保留含 20260930_0200 补偿的 head,关闭旧入口自动迁移;完整旧镜像业务、取消与自动续跑通过。补偿列值为 0,不能恢复删列前历史值 |
| F | 混用不同发布版本的 Runtime 副本 | 不承诺无中断混跑;此前同版双副本可用,具备版本门禁的实际旧镜像收到当前子请求明确 409。4c7eee8 更早且没有完整版本路由门禁,不能把该 409 证据外推;继续整池排空切换 |
2.1 明确不支持 / 明确拒绝
| 场景 | 行为 |
|---|---|
删除或修改仍被依赖的 @runtime-kit/...vN 公开路径 | 旧产物/旧页面源码导入失败;必须保留旧版本文件或做迁移 |
| N Renderer 接管时契约版本或画布尺寸语义错误 | HTTP 400 RENDER_CONTRACT_MISMATCH,不接管且不改变槽位代次;已有回执的过期票据幂等保留 |
完整 4c7eee8 Renderer 收到错误 contract_version | 历史镜像实测 500,未接管;不符合 N 的 400 错误映射,旧镜像源码保持原样 |
预览请求携带与 Runtime 副本不一致的 x-expected-runtime-version-fingerprint | 409 拒绝(指纹门禁) |
正常预览签名版本或 /__runtime_version/<指纹>/ 与当前 Runtime 不符 | HTTP 409 PREVIEW_VERSION_SKEW;跨版 HMR Upgrade 拒绝握手 |
| 旧 Runtime 探针未提供非空版本/发布身份 | Backend 返回 503 RUNTIME_VERSION_UNKNOWN;不能静默把旧镜像当同一 dev 版本 |
DB alembic_version 指向镜像不包含的 revision | 启动失败(Can't locate revision identified by '…') |
4c7eee8 页面 Batch ORM 运行在已删除 lease_generation 的 N schema | 完整 ORM 查询缺列失败;登录可成功,页面变更入队仍不兼容 |
| N Backend + DB 仍停在 N-1 schema | 启动失败(UndefinedColumnError,如 ai_agent_runs.process_owner);必须先 migration |
完整 4c7eee8 Lite + 远程 Renderer,Runtime 角色基址仍为容器回环 | 截图 RENDER_INTERNAL_ERROR;须配置同一受信网络内可达的 RUNTIME_PREVIEW_BASE_URL。本轮 http://m05_lite:7373 通过,不改旧源码或 CMD;端口未向主机发布 |
预览请求 x-expected-runtime-version-fingerprint 与副本不符 | 409 PREVIEW_VERSION_SKEW(不得收成 500) |
| 破坏性 External API 字段变更未做消费者迁移 | CLI/Skill/Editor 契约门禁失败;禁止静默兼容 |
2.2 固定样本的产物组合
全 N 主 Gateway、N Runtime + N-1 Renderer、完整旧 platform/Lite(补偿 schema)、最终 N platform/Lite 真实入口均通过 1920×1080 PNG、ZIP 下载与实际 HTTP 加载。完整旧两入口的同一 ZIP 再由 N 应用认证产物接口下载,摘要/字节数不变。23 个旧 Kit 入口路径/源码 hash 一致,实际渲染样本为 DataTable.v1 与 usePageSize.v1。
N Backend + 无发布身份的 4c7eee8 Runtime,在 N/N-1 两种 Renderer 下正常预览均 503;20 秒期限的真实截图重试均失败,Job RENDER_DEADLINE_EXCEEDED,没有成功结果且占用释放。独立旧 Runtime 构建 ZIP 与加载通过,不能计作截图整链路成功。具体 Job/Attempt、产物摘要及浏览器记录见组合证据。
3. 升级窗口与摘流策略
3.1 预览角色:同版池切换与排空
同版副本替换可增减池内实例;跨版切换当前按以下停机/排空顺序处理:
- 启动新版
runtime-preview,等待/__runtime_readyz,核对/__runtime_healthz的runtime_kit_version/build_id。 - 新版暂不加入旧池;暂停新的预览入口,旧池停止接收新请求。
- 等待在途连接结束(注意 HMR WebSocket 可能长连接,排空应有上界,超时后强制下线并记录)。
- 将预览池整体切换为新版并
reload,再恢复入口,避免同池混版。 - 停止旧容器。
同一预览池当前只放同一发布构建标识,不能只比较 runtime_kit_version。新实现自动传播并拒绝跨版请求;拒绝意味着预览需要刷新,不能等同无中断。当前候选正常浏览器双副本与旧镜像拒绝已验证;更早 Runtime 可能没有完整版本门禁,任意 N/N-1 混池仍不支持。跨版升级继续排空旧池再切换。
3.2 构建 / 检查角色:排空后替换
- 摘掉领取(停容器或阻断
runtime-jobs-net),等待在途任务完成或租约过期。 - 部署新版本,确认 Worker 启动(
RUNTIME_ROLE=build缺凭证会 fail-closed)。 - 构建任务有 attempt 围栏:迟到上传不会提升旧 attempt 产物。
3.3 Backend:停机或双副本
- 小团队 / Lite:停机升级。先备份 →
backend-migrate→ 启动新 Backend。 - 多副本:必须先逐项证明旧应用兼容新 schema,才能先扩新副本、再摘旧副本。当前 N/N-1 组合有删列冲突,页面业务按停机升级处理;共享签名密钥环、DB、对象存储与 Redis。普通 AI Run 不跨实例迁移(见 §5)。
3.4 DB migration 窗口
- 备份数据库(见备份与恢复)。
- 兼容检查须同时覆盖新增与删除字段;当前含删除旧 ORM 仍映射的列时,先排空并停止旧业务实例,再只跑一次
alembic upgrade head(backend-migrate或入口脚本)。 - 再滚动/启动业务副本。
- 当前旧迁移器不包含新 revision,应用回滚须关闭它并保留补偿后的 schema;数据库恢复另走一致备份方案。
20260930_0200 是 W05a 的前向补偿:恢复旧 ORM 需要的兼容列并设置服务端默认 0,当前 ORM 无需重新使用它。已应用的 20260930_0100 保持不变,补偿不能恢复历史代次。升级前排空页面任务并停旧实例;应用回滚保留补偿 schema,通过 PLATFORM_SIMPLE_RUN_MIGRATIONS=false / PLATFORM_LITE_RUN_MIGRATIONS=false 关闭旧镜像迁移器。旧列删除须另等旧实例退出和回滚窗口结束,不将数据库降回删列 revision 当应用回滚步骤。
独立 SQLite Alembic 引擎显式 BEGIN,使 DDL 与 revision 原子提交;真实 ADD 后强杀已复测。历史旧迁移器可能留下 orphan 列,不能据旧 revision 推断 schema 完整,详见中断处理。PG 的真实迁移强杀同样回退到旧 head。
4. 版本指纹门禁
W05c 将发布身份与传播机制收口为以下链路:
- 独立 Runtime 与 Lite 的构建阶段按 Runtime 源码/公开配置/构建脚本、包配置、Vite 配置与根
pnpm-lock.yaml生成.runtime-build-id,并复制到生产依赖裁剪后的目录。RUNTIME_BUILD_ID可统一覆盖该值,不能使用副本名;RUNTIME_INSTANCE_ID只出现在内部健康观测。交付镜像缺发布身份时启动失败,dev仅是本地开发默认值。 - Backend 正常 iframe 入口验签原 token,再从受信 Runtime 探针绑定
runtime_kit_version+build_id到子票据runtime_version_fingerprint,保留原 artifact、权限和exp。已绑定票据不重新探测换版;期望请求头由服务端覆盖,浏览器无需自填。 - HTML、Vite 的嵌套模块/CSS 和 HMR 使用
<公开挂载路径>/__runtime_version/v1.<base64url 指纹>/。HTTP 版本门禁早于 Vite/base/源码转换,失配返回 409;HMR Upgrade 在握手前拒绝。远程模块、Tailwind 与资源代理仍验签绑定声明,不依赖本进程缓存授权。 - 同版副本使用固定依赖预优化集合;跨副本 Vue 子请求在本次票据验签后恢复主模块描述符,ctx 前置保留样式转换。正常浏览器样本覆盖页面、scoped CSS 和字体;新增依赖及公共能力组合继续补对应冷启动验收。
- 固定
/__preview入口与版本资源路径共用挂载解析;本轮真实 Renderer 截图、ZIP 下载及构建入口浏览器加载通过。两个协调器同时读取结果时,迟到产物 410 的失败写入也必须先取得占用条件更新,不能由 ORM 自动 flush 重开已释放的 attempt。 - Backend 保留上游错误状态、版本响应头和缓存语义;探针与 HTML 恰好落到不同发布时明确拒绝。重新导航原预览入口可绑定当前发布,不延长票据有效期。
- 同池继续固定同一发布镜像并按 §3.1 排空;本轮完整 N-1 与两项公开 Kit 样本另有 M05 证据,最终模板、任意旧版本混池与每项公开能力渲染仍不外推。
- 检查角色缓存指纹包含 Runtime Kit 清单 hash(
CodeCheckFingerprintBuilder),升级编译器后缓存自然失效。
5. Run 收敛边界(实例心跳 / 历史 owner)
普通 AI Run 使用进程内执行,产品承诺「中断不自动续跑」。W05b 已新增实例心跳表与独立后台收敛;真实双 Backend 的强杀、暂停、hostname 改变/PID 重用、静默 B 保护、会话解除及旧实例迟到写拒绝通过,完整外部交接 M04 仍待执行:
- 新 Run 使用同一进程实例的
hostname:pid:uuid,登记ai_agent_process_owners;容器/主机名改变或 PID 重用不会让过期的 UUID 重新存活。心跳不依赖模型输出,默认 TTL 90 秒、心跳/扫描各 10 秒;过期实例不能续期复活,当前执行器停止接收普通 Run,须重启实例。 process_reaper复用统一 CAS 时序;复核 owner、状态、取消快照与外部交接,在同事务写终态/事件、解除会话占用。普通执行与工具提交传播存活写围栏,迟到结果被拒绝。paused、waiting_external、未完成外部 Batch 交接保持原路径,人工继续绑定新 owner。- 在数据库可用、时钟同步且无额外调度/重试延迟时,最后有效心跳后约 TTL + 扫描周期收敛。实机应记录配置、写冲突/数据库不可用时长与完整收敛时间,不能只拿一次恢复耗时与 TTL 比较。
历史未登记 owner 不自动补造心跳。升级前排空旧 Run;下表仅是 run_recovery.py 的历史兼容路径,跨容器历史遗留继续用 force_cancel:
| 条件 | 启动恢复是否收敛 | 说明 |
|---|---|---|
process_owner 为空 / 无法解析 | 仅单进程部署(include_unowned) | 多副本默认跳过,避免误杀 |
| hostname ≠ 本机 | 否 | 保护其它副本活跃 Run |
| hostname = 本机且 PID 仍存活 | 否 | 含当前进程与同机 sibling |
| hostname = 本机且 PID 已死 | 是 | 终态为 AI_RUN_PROCESS_STOPPED 或取消 |
历史路径边界(M04 需区分新/旧实例):
- 容器重建 / hostname 改变:历史未登记 Run 归属旧 hostname,启动恢复不收敛,需
force_cancel;新登记实例按过期心跳处理。SSE 始终只观察。 - PID 重用:历史未登记 Run 的
process_is_alive为真时跳过;新实例以 UUID/持久化租约区分,不依赖 PID 存活探测。 - 同名主机、不同 PID namespace:可能把异 namespace 进程误判为存活或死亡;多副本部署应保证 hostname 唯一(Docker 默认 container id 即可)。
6. 验收用例(M05 / M04)
- 组合 A:全 N 完成一次登录 → 预览 → 截图 → 构建下载。
- 组合 C:逐角色验证 Backend N 与 Renderer/Runtime N-1;先核对发布身份与入口,再执行截图。协议一致但 Runtime 缺身份的组合应按本轮负例明确失败,独立构建与加载另记。
- 组合 B/E:除登录外,执行 N-1 页面任务入队与 Batch ORM 查询;缺列必须判不兼容。按既定 schema 恢复后再验证旧版完整业务。
- 指纹:人工失配入口返回 409;另用正常浏览器让 HTML/模块落到不同版本,验证版本传播或稳定路由。
- Run:强杀 Backend-A 后重建容器(hostname 变)→ 新登记的失效 Run 有界终态、会话可再发起,B 活跃 Run 不被误杀;历史未登记 Run 单独验证手动补偿,不算新实例自动收敛通过。
- Run:PID 重用模拟 → 启动恢复不误杀;记录收敛延迟。