MIA-249 · MIGRATION EXPLAINER · 2026-08-18

数据库迁移 255–259 到底在做什么

结论不是五套产品概念,而是一条连续演进链:先建立 Group Memory 与设备目录,再补 ownership fence、旧 Server 共存保护和 cutover 后恢复。

01 · 最终数据库模型

私人 Memory 的有效 owner 永远只有一个。Ungrouped 继续用老表;进入 Group 后直接切到 Group 的一份 Memory,不再展示多组概念。

UNGROUPED OWNER

work_session_context

只服务未分组 Work Session;沿用已发布表和 wire 名称。

PLACEMENT

Session 进入 Group

非空 Session Memory 以固定格式 append 一次,留下不可变证据,然后活跃 Session owner 置空。

GROUP OWNER

一份私人 Group Memory

普通 Group 用 work_session_private_group.context;PIWG 用当前用户的 work_project_group_projection.context。共享 Project Rules/Memory 保持原样。

本地目录的边界:数据库只登记 creator、Group、daemon/device 与路径;不上传目录内容,不跨设备解释路径,也不改变一个 Turn 只有一个 Work Root/CWD 的事实。

术语

INITIAL CUTOVER

把 v0.0.1 已存在的 grouped Session Memory 按 Group 确定性拼接上移。

RECONCILIATION

initial 后若旧 Server 又写 Session Memory,只补合并没有被精确覆盖的新 revision/content。

EXACT EVIDENCE

Session ID、revision、原文全部相同,并且 merge kind 与完成 epoch 的 run kind 匹配。

SNAPSHOT

Turn 创建时冻结 owner 与目录集合,后续移动 Session 或编辑目录不会篡改已建任务。

02 · 为什么是五个迁移

255

Memory 主体

Group context/revision、Turn owner snapshot、merge/cutover evidence、ungroup reset trigger。

256

设备目录

Group 目录 binding 与 Turn 目录 snapshot;可以被独立 feature flag 关闭。

257

PIWG owner FK

把 handler 层 creator 检查提升成数据库复合外键。

258

旧 255 修复

已经执行早期 255 的数据库不会重跑旧编号,需要新编号替换危险 trigger。

259

恢复批次

持久化 initial/reconciliation 类型,使 cutover 后旧 writer 污染可恢复、可审计。

Fresh DB 与已安装 DB:当前 255 文件已经是最终安全形态;258/259 不能因此删除,因为持久数据库可能早已把旧版 255 标成 applied。

03 · 每个 up/down 详细说明

255 · work_group_private_memory

Up:两种 Group owner 增 Memory

对象新增数据库约束
work_session_private_groupcontextcontext_revisionrevision > 0;最多 32,000 codepoints 与 131,072 UTF-8 bytes
work_project_group_projectioncontextcontext_revision

PIWG 在这里新增的是私人 projection Memory;老的共享 Project Rules/Memory 继续走老字段。

Up:Turn 冻结 owner

agent_task_queue 增加 work_private_memory_scopework_private_memory_group_kindwork_private_memory_group_id。CHECK 只允许:三列全空、Session owner、完整 Group owner。这样 Group A 创建的 Turn 不会因 Session 后来移到 B 而写进 B。

Up:应用只追加的 merge ledger

work_group_memory_merge 保存来源 Session/revision/原文/hash、目标 Group/revision/全文 hash、source order、merge kind、cutover epoch。UNIQUE(epoch, source_session_id) 防同批重复;保留原文而不是只留 hash,才能真实回滚与审计。正常 Context 路径不读它。“不可变”是应用和发布规则;schema 没有另设 trigger/RLS 禁止管理员直接 UPDATE/DELETE,受控数据库权限和 runbook 仍是证据完整性的一部分。

Up:cutover completion

work_group_memory_cutover 保存 epoch、initial|reconciliation、source/group count、manifest SHA-256。部分唯一索引保证最多一个 initial。

Up:Group → Ungrouped trigger

  1. 非该 placement 变化:不处理。
  2. Session context 为空:保持空并推进 revision。
  3. 非空且当前 ID + revision + 原文有精确完成证据:清空并推进 revision。
  4. 非空但无精确证据:保留,让 startup fence/reconciliation 看得见。

它覆盖普通移出、Group 删除、PIWG dismiss、Project delete/access revoke 与 FK cascade,不依赖每个 handler 都正确实现。

Down

只要已有 cutover、merge evidence、非空/非初始 revision 的 Group Memory,或任务 owner snapshot,就抛异常。仅在确认从未承载有效数据时才删 trigger、表和列。生产 cutover 后的回滚不能靠 255 down,而要按 ledger 恢复 Session owner。当前没有自动 --rollback 命令;恢复 SQL/脚本与 representative snapshot rehearsal 仍是生产前 release blocker(OpenSpec task 4.9)。

256 · work_group_local_directories

Up:目录 binding

字段语义
workspace_id / creator_idWorkspace 与私人 owner
private_group_id / project_idXOR:必须且只能有一种 Group owner
daemon_id路径所在设备;同样的路径字符串可在不同设备分别注册
local_path / path_key展示/执行路径与 daemon canonicalize 后的判重键
label / position1–200 字符显示名与稳定排序

private Group FK 同时约束 group/workspace/creator;256 的 Project FK 先只约束 project/workspace,creator projection 由 257 补严。部分唯一索引按 creator + group + daemon + path_key 判重;排序索引按 position、id 返回。

Up:Turn 目录 snapshot

agent_task_queue.work_group_directory_snapshot 是非空 JSON array,默认 []。它冻结 Server 当时选择的当前 Runtime 设备目录,客户端不可写。claim 时由 Server 根据 claimant capability、持久 Runtime capability 与 feature flag 校验、剥离或 requeue;daemon 负责声明能力并消费通过校验的 snapshot。

Down

目录表有任何 row,或任一 task snapshot 非空,就拒绝 destructive rollback。目录功能正常回滚是关闭 work_group_directories_v1 并保留 binding,不是删表或删用户本地文件。

257 · work_group_project_directory_owner_fence

先从已有 Project 目录 binding 中 SELECT DISTINCT workspace_id, creator_id, project_id,幂等补建缺失的 work_project_group_projection;再增加:

(workspace_id, creator_id, project_id)
  → work_project_group_projection(workspace_id, user_id, project_id)

因此 PIWG 目录从“属于这个 Workspace 的 Project”收紧为“属于当前 creator 的私人 PIWG projection”。projection 删除时目录 cascade。Down 只删 FK,不猜测和删除 up 阶段补建的 projection,可能留下无害空 row,但不会误删 Memory。

258 · fence_ungroup_session_memory_reset

早期 255 trigger 曾可能在 expand schema 与旧 Server 共存时,因 ungroup/delete 直接清空仍然权威的 grouped Session Memory。已执行数据库不会自动重跑后来修订的 255,所以 258 用 CREATE OR REPLACE FUNCTION 把 exact evidence fence 安装到这些数据库。

258 是过渡兼容形态:placement evidence 可直接授权;历史/reconciliation merge 只要关联到已完成 epoch 即可。259 再把 merge kind 与 run kind 配对收紧。

Down 故意是 no-op:只输出 NOTICE,不恢复已知会丢数据的旧函数。安全修复一直保留到未来明确移除整个 255 模型。

259 · work_group_memory_reconciliation
  1. 给旧 cutover 表补 run_kind,历史 completion 默认 initial。
  2. CHECK 限制 initial/reconciliation。
  3. 部分唯一索引保证最多一个 initial。
  4. merge kind 允许 post-cutover reconciliation。
  5. trigger 要求 historical ↔ initial、reconciliation merge ↔ reconciliation run 精确配对;placement 保持独立。

Down

已有 reconciliation completion/evidence 时拒绝 down。否则先恢复不引用 run_kind 的 258 安全函数,再删除 initial 唯一索引、把 merge kind 收窄、最后删除 run_kind

P1 已闭环:真实 PostgreSQL 曾复现旧 down 删列后仍保留 259 函数,任何 Group → Ungrouped 都报 SQLSTATE 42703。当前顺序已修复,并覆盖无 context、无 evidence 原文保留、historical exact evidence 清空、reconciliation evidence 拒回滚和 down 后重新 up。

04 · SQL 建路,命令真正搬数据

真正执行历史合并的是 server/cmd/work_group_memory_cutover/main.go,不是 migration 里的一条大 UPDATE。

Initial cutover

  1. read-only preflight 盘点所有非空 Session context;Ungrouped 不参与。
  2. 按 workspace、creator、group kind、group ID 分组;来源按 Session created_at、ID 固定排序。
  3. 统一 builder 拼接,不静默摘要、不截断;计算来源/目标/整体 manifest hash。
  4. 有在途 grouped snapshot 或任一目标超过 32,000 codepoints / 131,072 bytes 就阻塞。
  5. apply 用 serializable transaction、advisory lock 与相关表写栅栏;锁内重建 manifest,必须等于人工批准的 Final Scan hash。
  6. 同一事务更新 Group Memory、写全部 source evidence、最后写 completion;任一步失败整体 rollback。
work_group_memory_cutover --preflight --read-only

work_group_memory_cutover --apply \
  --cutover-epoch <initial-uuid> \
  --expected-manifest-sha256 <approved-final-scan-sha256>

Post-cutover reconciliation

若旧 writer 在 initial 后写出新 grouped Session revision/content,startup fence 拒绝新 Server;停止旧 writer 后,用新 epoch 只选择未被精确 evidence 覆盖的来源:

work_group_memory_cutover --reconcile --preflight --read-only

work_group_memory_cutover --reconcile --apply \
  --cutover-epoch <new-reconciliation-uuid> \
  --expected-manifest-sha256 <approved-reconciliation-sha256>

完成 epoch 在没有新污染时可幂等重试;同 epoch 出现新来源必须拒绝并重新 preflight/换 epoch,避免旧审批 hash 被挪作新用途。

Startup fence

SAFE

无未覆盖来源

允许启动;fresh DB 也属于此类。

BEFORE CUTOVER

有 grouped legacy data

拒绝启动,要求 initial preflight/apply。

AFTER CUTOVER

出现新 revision/content

拒绝启动,要求新 reconciliation epoch。

失败域:这是全 Server 的临时 cutover guard,会造成部署级不可用;它保护的是不能用过期 Group Memory 静默服务。owner 为 release captain,至少保留 v0.0.4 与回滚窗口,下一稳定版本生产验收、确认无旧 writer 后,才能通过单独审批/OpenSpec 移除或缩小。

05 · 回滚与发布成本

时点正确动作不要做
expand 后、cutover 前继续旧 Server;关闭新 surface;保留未合并原文先启动只认 Group owner 的新 Server
initial apply 失败让 transaction rollback;修正 manifest/容量/在途任务手改 completion/evidence
cutover 后回旧 binary写栅栏、排空 Turn、按 ledger 恢复 Session owner,再切 binary;生产前先固化恢复 SQL/脚本并 rehearsal跑 255 down;双读两套 owner
旧 writer 污染新 epoch reconciliation重跑 initial、清空来源、复用旧 epoch
关闭目录功能work_group_directories_v1,保留 binding跑 256 down、删除本地目录

新增人工配置:0

没有新环境变量、Secret、DNS、TLS、Ingress 或第三方控制台配置。

一次性人工发布工作

工作Owner / 时机阻塞项
Early/Exact/Final Scan 与容量例外闭环数据 owner + release captain;候选冻结前生产 cutover
停旧 writer/worker,排空或取消在途 grouped snapshotrelease captain;维护窗authoritative cutover
保存并双人核对 manifest SHA-256数据 owner + release captain;apply 前cutover
initial apply,核对数量与 startup fencerelease captain;维护窗新 Server 启动
固化 rollback SQL/脚本并做 representative v0.0.1 snapshot rehearsal数据 owner + release captain;生产 cutover 前emergency rollback readiness
保留 evidence 与旧 Session rows数据 owner;回滚窗口数据恢复能力
macOS/Windows 目录 UATDesktop/QA owner;feature 开启前目录功能发布

reconciliation 只在真实污染时发生,不是周期任务。持续成本是回滚窗口内的证据存储、发布期 fence/flag 监控,以及下一稳定版本对临时 fence 的移除审批。

06 · 分端架构影响 / Architecture Impact by Layer

Web CHANGED读 Group Memory、管理目录、发起规则导入;不执行 migration。
Desktop CHANGED设备目录选择/搜索与 daemon 身份;需随功能发布。
Mobile Web / PWA CHANGED可远程管理本人在线设备目录,依赖目标 daemon;不直接访问宿主路径。
Shared frontend CHANGEDGroup 设置、Context 导入、root selector 与 capability gate。
Server CHANGED选择 Memory owner、冻结 snapshot、授权目录、startup fence、cutover/reconciliation。
Database CHANGED255–259:Group Memory、目录 binding、evidence 与 trigger。
Daemon / CLI / Runtime CHANGED按当前 Runtime 设备消费目录 snapshot;Memory wire 名称沿用。
Deployment / external CHANGED一次性扫描、维护窗与 evidence 保留;无新外部配置。
Cross-cutting contracts CHANGEDownership、跨设备、历史数据、version skew、rollback 与临时 guard。

07 · 当前证据与视觉验收

259

QA schema

255–259 已全部应用。

11

Source Sessions

1 个 initial epoch,把 11 个来源合入 3 个 Group。

0

Unfenced grouped rows

当前无未被精确 evidence 覆盖的非空 grouped Memory。

0

Reconciliation

当前 QA snapshot 不需要恢复批次。

这是 2026-08-18 production-shaped QA DB 的只读证据,不替代正式生产 Final Scan。

Browser:QA Server/Web/daemon 与同源 API proxy 健康,/api/config 返回 200 并广告相关 capability。Codex in-app Browser 不受锁屏影响,但其 URL 安全策略拒绝本机 localhost,并禁止换其他 surface 绕过,因此不能宣称真实功能 UI 已通过 Browser 验收。此公开 HTML 可用 Browser 验收;真实 UI 需要 Browser 可访问的 QA 域名或人工验收。
一句话主体逻辑就是直接把已分组历史 Session Memory 按 Group 拼接上移;复杂度来自保护已发布数据、旧 writer、在途任务和可恢复切换,不是为了保留多套 Memory 概念。