Land the revised modular-monolith scheme on the implementation branch so U1–U4 can proceed against a frozen design, and register the docgov mutex without occupying the integration slot. Co-authored-by: Cursor <cursoragent@cursor.com>
21 KiB
模块化单体解耦架构
状态:设计已修订,首个文档可靠性试点待实施
决策:模块化单体解耦 ADR
实施:模块解耦整改与重构计划
1. 目标与边界
本设计把 Agent-System 收敛为模块化单体,使项目文档在刷新、服务重启、并发修改和工作区清理后仍能从稳定逻辑标识定位到正确正文。 模块边界必须同时约束依赖、事务、并发、授权、文件和最终成果;只减少 Go import 或移动目录不算解耦完成。
1.1 目标
- 每个业务命令有唯一应用用例、事务 owner 和幂等语义。
- 每张运行表和每类文件有唯一写入属主,但跨表不变量仍能在同一事务提交。
- HTTP、SSE、CLI、worker 和恢复工具经过同一授权决策,不信任调用方填写的身份或资源归属。
- workspace 只保存计算现场和可重建投影;最终文档、最终回复和 manifest 进入 FAV。
- 事件在崩溃、重复投递、多订阅者和水平扩容下可追踪、可重试、可隔离。
- 首个纵向试点直接消除受控正文刷新/重启后 404、409 和 projection missing 类故障。
1.2 非目标
- 不拆微服务,不引入分布式事务协调器。
- 不在本轮统一全部 HTTP 错误响应。
- 不在本轮替换 PI Runner stdout/result 协议。
- 不把所有历史数据永久保留,也不把生产数据复制为测试 fixture。
- 不允许 FAV 绕过数据库授权或自动重建活动工作流、lease、pending task。
2. 目标架构
flowchart TB
Client[Web or CLI] --> Adapter[server adapters]
Worker[Background workers] --> App[application use cases]
Adapter --> App
App --> Authz[authorization policy]
App --> UoW[UnitOfWork]
App --> ReadModels[read use cases]
UoW --> Owners[domain table owners]
Owners --> PG[(PostgreSQL)]
UoW --> Outbox[(outbox and staging)]
Outbox --> Dispatcher[event dispatcher]
Dispatcher --> Subscribers[idempotent subscribers]
Subscribers --> Vault[artifactvault]
App --> Workspace[workspace capability service]
Workspace --> Scratch[(scratch workspace)]
ReadModels --> PG
ReadModels --> Vault
2.1 分层职责
| 层 | 职责 | 禁止 |
|---|---|---|
internal/server |
参数解析、认证信息转 Principal、调用应用用例、序列化 HTTP/SSE | SQL、事务、路径拼接、跨模块编排、后台重试 |
internal/app/*flow |
用例编排、授权、UnitOfWork、命令幂等、事件发布 | 领域 SQL、物理路径、传输格式 |
| 消费者侧 ports | 表达一个用例需要的最小能力 | 聚合所有模块方法的万能接口 |
| 领域模块 | 业务不变量、表/文件写入所有权、事务绑定 repository | import 其他领域实现、开启不受控嵌套事务 |
| read model | 授权后的跨模块只读聚合 | 业务状态变更、修复式 GET 写入 |
| 基础设施 | PostgreSQL、CAS、FAV、workspace、事件 dispatcher | 推断业务终态或自行授予权限 |
2.2 契约分类
internal/contracts 不再作为所有 DTO 的公共仓库。
| 契约 | 所有位置 | 版本规则 |
|---|---|---|
| HTTP/SSE wire DTO | internal/server/api、internal/stream/transport |
对外兼容,breaking change 走 ADR |
| 一对一调用端口 | 消费者 use case 包内 | 随用例演化,不带 JSON tag,不自动走 ADR |
| 公共领域事件 | internal/contracts/events/<domain> |
有 schema version;生产者和多个消费者共同评审 |
| 持久文件/manifest schema | 所属模块,如 artifactvault、workspace |
major/minor 兼容;未知 major fail closed |
| 稳定共享词汇 | internal/contracts/vocabulary |
仅无行为枚举和值对象;禁止依赖业务实现 |
| 错误分类 | 领域错误留领域包;wire 映射留 server | 不把数据库/路径/第三方原始错误暴露给客户端 |
接口默认由消费者拥有,应用组合根只注入实现。 只有三个以上消费者确实共享相同语义时,才提升为公共端口。
3. 模块边界
| 模块 | 唯一职责与写入所有权 | 可依赖 | 禁止 |
|---|---|---|---|
app/docflow |
文档读取、提交、批准、投影和归档用例;相关事务 owner | 消费端口、authz、uow | SQL、物理路径 |
app/stageflow |
阶段推进和任务执行用例 | 消费端口、authz、uow | bizdev 门面、HTTP DTO |
app/chatflow |
一轮对话识别、协调、消息落账和任务触发 | 消费端口、authz、uow | 直接写共享上下文字段 |
docgov |
文档治理状态、角色、lease、document_records 写者 |
自有 repository、稳定词汇 | 直调 docrevision 实现、读 workspace 文件 |
docrevision |
immutable snapshot/version/revision/event/report 账本写者 | 自有 repository | 直接写 document_records、freeform_documents |
freedoc |
自由文档根表和发布状态写者 | 自有 repository | 被 docrevision 反向更新 |
docquery |
授权后文档列表、正文、版本聚合读取 | 只读端口、artifact read fallback | 任何写入、目录扫描、授权推断 |
chatlog |
chat_* 表族读写和会话/消息/turn 查询 |
自有 repository | prompt 装配、外部导出策略 |
contextasm |
记忆、摘要、召回和 prompt 上下文组装 | 消费者侧 ChatLog/KB read ports | HTTP、SQL 越界写入 |
intent |
纯规则意图分类 | vocabulary | bizdev、server、IO |
taskrun |
task run 状态机、claim/finalize、幂等写者 | 自有 repository | server 补协议、workspace 路径 |
squadrun |
在受限 workspace capability 内执行角色与步骤 | agent/template/quote 消费端口 | DB、SSE、业务授权、任意路径 |
workspace |
Workspace ID 签发、物理路径解析、projection 生命周期 | fsutil、storage identity | 业务终态、最终归档 |
artifactvault |
终态 payload/manifest/receipt/tombstone 权威 | 独立存储、registry | 活动业务关系、运行授权、workspace 扫描 |
notification |
通知偏好、投递和状态 | 事件消费、repository | 业务状态推进 |
app |
唯一组合根、worker 生命周期、事件订阅注册 | 所有构造函数 | 被 server 反向装配 |
模块“可依赖”只表示通过端口调用,不表示允许 import 对方实现包。
4. 事务与单一写者
4.1 两种所有权
- 表属主决定 SQL、约束和单表不变量。
- 事务 owner决定一个业务命令需要哪些属主共同提交。
两者不能合并为“每个模块各开一个事务”。
应用用例开启 UnitOfWork,取得绑定同一 pgx.Tx 的 repository capabilities;领域服务只能使用这些 capabilities,不能 commit、rollback 或启动嵌套事务。
4.2 文档提交原子集合
一次受控文档提交必须原子包含:
- 校验 Principal、项目成员、duty、lease 和 expected head。
- 锁定
document_records与当前 open version。 - 写 immutable content blob/snapshot/revision/change event。
- 更新版本 head 与
document_records当前指针/哈希。 - 冻结应归档 payload 或不可变 blob 引用。
- 写领域事件和 FAV publish request。
- 提交后再进行 workspace 投影、通知和外部存储发布。
任一步失败,步骤 2–6 全部回滚。 投影或发布失败不会回滚业务提交,而是进入可重试队列并在 UI 暴露 degraded 状态。
4.3 事务隔离选择
| 场景 | 机制 | 原因 |
|---|---|---|
| 单文档编辑/批准/提交 | READ COMMITTED + SELECT ... FOR UPDATE + expected head |
竞争范围明确,冲突可向用户解释 |
| 同幂等键并发创建 | 唯一约束 + payload digest 校验 | 数据库决定唯一赢家 |
| 跨表项目级 duty/heal | pg_advisory_xact_lock(project_id) + 固定行锁顺序 |
复用现有项目级互斥,事务结束自动释放 |
| 复杂跨聚合唯一不变量 | SERIALIZABLE,完整事务有限重试 |
仅在显式证明需要时使用 |
| outbox 多 worker claim | FOR UPDATE SKIP LOCKED + lease/fencing token |
提高队列吞吐,不作为普通读取语义 |
4.4 锁顺序
所有跨属主事务按下列顺序获得锁:
- project 或 workflow instance 事务级 advisory/row lock;
- document/freeform/chat aggregate root;
- version/snapshot/turn;
- task run;
- staging/outbox/delivery。
禁止在持锁期间调用 LLM、PI Runner、对象存储、HTTP、SSE 或执行大文件复制。 需要外部工作时采用 prepare → 外部执行 → compare-and-commit;commit 阶段重新验证 expected version。
4.5 超时与重试
- 每个写事务设置独立 context deadline 和数据库
lock_timeout;初始预算由配置管理,不在 handler 硬编码。 40001与40P01只允许重跑完整幂等事务,采用带抖动退避和有限次数。- expected-version 冲突、权限拒绝、唯一键 payload 不同不是瞬时错误,不自动重试。
- 事务重试期间不得重复发送消息、SSE、通知或对象存储写入;这些副作用只能由提交后的 outbox 驱动。
5. 并发协议
5.1 文档并发
客户端读取正文时获得 ETag = head_snapshot_id:head_revision_no。
PATCH、agent commit、approve 和 projection repair 都必须携带 expected head 或稳定 idempotency key。
旧 ETag 返回 typed VERSION_CONFLICT,响应包含当前 head 元数据但不泄露无权正文。
5.2 TaskRun 并发
Claim(idempotency_key, request_digest) 通过唯一约束保证只创建一个 run。
相同 key、相同 digest 返回既有 run;相同 key、不同 digest 返回 IDEMPOTENCY_CONFLICT。
状态迁移使用 WHERE status = expected AND version = expected_version,影响行数为零即重新读取并分类冲突。
幂等键按 environment、project、actor、action 和客户端 key 作用域化,并与规范化 request digest 绑定。
幂等记录的保留期不得短于客户端重试窗口、事件重放窗口和对应业务记录的审计期。
5.3 Workspace 并发
Workspace ID 由服务端签发并绑定 environment、project、run、storage identity 和生命周期。
同一 workspace 同时只有一个写 lease;续租返回 fencing token,所有写操作校验 token。
本地 sync.Mutex 可减少同进程竞争,但跨副本 lease 和 token 才是正确性依据。
文件写入采用同目录临时文件、校验、原子 rename 和目录同步;reader 只读取完整 projection marker。
5.4 FAV 并发
逻辑 artifact 的幂等键由终态事件、资源、payload SHA 和 policy version 决定。 对象写入使用 content-addressed key 和 no-replace 语义;blob、manifest 全部校验完成后最后写 commit marker。 重复 publisher 可共享已有 blob,但只有一个 registry 状态迁移为 verified。
5.5 查询与背压
docquery 的列表和版本历史必须分页,BatchGet 首期硬上限 100 个引用;超过上限返回 typed validation error,不自动拆成无界并发查询。 批量读取先按 Principal 和 project scope 分组授权,再使用有界并发和数据库 statement deadline。 API 对 actor/project/action 维度限流,后台 backfill 和 dispatcher 使用独立连接预算,不能耗尽在线读写连接池。 SSE 慢客户端只影响传输缓冲;不能持有业务 transaction、数据库锁或阻止事件持久化。
6. 可靠事件
6.1 事件信封
每个持久事件至少包含:
event_id
event_name
schema_version
environment
project_id
aggregate_type / aggregate_id / aggregate_version
actor_ref
correlation_id / causation_id
idempotency_key
payload_digest
occurred_at
事件不携带 secret、认证 token、Prompt、正文或任意文件路径。 大 payload 先冻结到不可变 staging,事件只保存引用和 digest。
6.2 投递模型
sequenceDiagram
participant UC as Use case transaction
participant DB as PostgreSQL
participant W as Dispatcher worker
participant S as Subscriber
UC->>DB: business state + staging + outbox
DB-->>UC: commit
W->>DB: claim delivery with SKIP LOCKED
W->>S: deliver event and fencing token
S->>DB: idempotent side effect and inbox receipt in one transaction
S-->>W: success or typed failure
W->>DB: ack, retry schedule, or dead letter
- 每个订阅者有独立 delivery 状态,不能共享单一
published_at。 - 投递语义为至少一次;消费者以 event ID/inbox 唯一约束幂等。
- 同一 aggregate 按 aggregate version 有序;不同 aggregate 可并行。
- claim 按
next_attempt_at, event_id排序并采用 aging,防止热点失败事件令旧事件永久饥饿。 - 数据库 subscriber 的业务副作用与 inbox receipt 必须同事务;外部副作用使用业务幂等键并在回执落库前允许安全重放。
- transient failure 指数退避;永久失败进入 dead letter,不能阻塞其他订阅者。
- worker 认领带 lease、attempt 和 fencing token;过期 worker 无权提交 ack。
- replay 必须指定事件范围、目标消费者、操作者和原因,并写审计记录。
- delivery/outbox 的清理只能在所有必需消费者完成、重放窗口结束且恢复 checkpoint 已覆盖后执行;审计类事件按独立保留策略处理。
7. Workspace、账本与 FAV
7.1 三层权威
| 层 | 权威内容 | 清理规则 |
|---|---|---|
| PostgreSQL | 项目、权限、workflow、task、文档关系、版本账本、当前指针 | 依完整备份/PITR 和环境生命周期处理 |
| workspace/state | 执行现场、临时材料、可重建 projection | 终态冻结和归档门禁通过后按 TTL 清理 |
| FAV | 已归档 payload、manifest、receipt、tombstone | 按 retention/legal hold/delete ledger 处理 |
7.2 Workspace capability
跨模块 DTO 只包含:
workspace_id
project_scope
run_id
lease_fencing_token
schema_version
调用方使用 ReadArtifact(key)、OpenAtomicWriter(key)、CommitProjection(key, digest) 等受限端口。
artifact key 必须是受控相对标识,禁止绝对路径、..、空段、跨作用域 ID、symlink 和 hardlink 逃逸。
7.3 RunManifest
RunManifest 的规范字节、SHA、workspace ID、storage identity 和输出成员在 taskrun finalize 时持久化。
Evidence map[string]string 和 Gaps []string 不作为核心协议;核心字段类型化,扩展只能进入带 namespace 和 schema 的 extensions。
workspace 内 manifest 是可重建副本,不是唯一事实源。
7.4 FAV 接入
artifactvault 只接收服务端确认的终态文档、终态助手回复和必要附件。 流式 chunk、候选、Prompt、工具轨迹、修复过程、临时报告和 workspace 草稿准入率必须为零。 发布从终态事务的 staging/outbox 驱动,禁止目录扫描。
docquery 的读取优先级为:
- 校验 Principal 和数据库 resource→project 授权图;
- 读取数据库 immutable ledger/current pointer;
- 检查并校验 workspace projection,仅作为缓存;
- 正文缺失但授权图完整时,可按 feature flag 从 FAV 只读降级;
- 权限图缺失、vault tombstone、SHA 冲突或来源不一致时 fail closed。
GET 不得修复数据库或覆盖 head。 修复和恢复是独立命令,并产生审计记录。
8. 授权架构
8.1 Principal
Principal 由 HTTP session、服务身份或恢复凭证适配器构造,至少包含 actor ID、actor type、environment、认证来源、request/correlation ID 和明确 capabilities。
UserID、ProjectID、role 和 duty 不能直接从业务请求体覆盖 Principal。
8.2 决策模型
授权决策输入为 Principal + Action + Resource + CurrentState。
| 动作类别 | 强制条件 |
|---|---|
| 文档读取 | 项目成员/管理员 + 分类可见性 + stage/duty 范围 |
| 文档编辑/提交 | 写权限 + 当前 duty + 合法状态 + 有效 lease + expected head |
| 批准/驳回 | 对应 action-level duty + 冻结实例配置 + 当前状态 |
| TaskRun 执行/恢复 | 项目权限 + 合法 workflow/stage + 服务端解析的 backend capability |
| FAV 降级读取 | 正常读取授权已成功 + artifact scope/hash/删除状态一致 |
| replay/backfill | 运维 capability + 精确范围 + dry-run + 审计 |
| scratch recovery promotion | 系统管理员 + 数据责任人确认 + scratch identity + 冲突计划无未决项 |
应用用例可以在事务前做快速拒绝,但状态改变必须在取得资源锁后重新读取授权相关事实并再次判定,防止成员、duty、stage 或 lease 在检查后变化。 授权服务不可用或关系不完整时默认拒绝。 事件携带的 actor 只用于审计;订阅者产生持久或外部副作用前重新读取当前权威状态。
8.3 后端与数据最小化
调用方不能提交任意 Backend string。
应用层按环境、项目策略和输入敏感级别解析 BackendRef,agent 适配器再注入受管凭证。
凭证不得进入 DTO、日志、事件、manifest、result 文件或 FAV。
InputBundle 只包含完成任务必需的材料,并标记来源、分类、允许用途和生命周期。
9. 架构门禁
9.1 静态规则
| 规则 | 检查对象 | 例外 |
|---|---|---|
| 领域实现互不 import | Go production packages | 消费端口和稳定 vocabulary |
| server 不 import repository/领域实现 | server production packages | app facade、wire DTO |
| 表写入唯一属主 | production SQL 的 INSERT/UPDATE/DELETE/MERGE | Goose、测试 fixture、批准的修复工具 |
| 工作区物理路径唯一属主 | production path construction/open/write | workspace/squadrun adapter |
| 事件发布者 allowlist | event name → producer package | 无运行时动态注册旁路 |
| 订阅者 allowlist | event name → consumer use case | replay 工具需显式 capability |
例外必须记录 owner、原因、范围、到期日和移除里程碑。 warn/block 不按固定“两周”切换;每条规则在对应纵向切片清零后单独转 block。
9.2 行为门禁
架构检查不能替代以下测试:
- 同文档并发编辑、批准和 agent commit;
- 任意事务步骤失败时全量回滚;
- worker 崩溃、lease 过期、重复投递和多订阅者部分失败;
- workspace 删除、换根、重启和 projection 重建;
- FAV 降级读取与 scratch 恢复;
- 匿名、非成员、跨用户、跨项目、错误 duty、错误 stage、授权依赖失败;
- 真实浏览器刷新/重登/rebuild 与真实 PI 主链。
10. 可观测性与容量
10.1 必须指标
- 数据库事务时长、锁等待、deadlock、serialization retry、版本冲突率。
- outbox pending/oldest age、delivery attempts、lease steal、dead letter 数。
- projection missing/mismatch/repair、FAV degraded read、vault verification lag。
- 授权 deny 按 action/reason 计数,不记录正文或敏感资源名。
- workspace active lease、过期清理、fencing rejection、storage identity mismatch。
10.2 高并发基线
首个试点必须建立并保存基线,而不是直接承诺未经测量的容量数字:
- 同一文档热点写入与不同文档并行写入分开压测。
- 1、2、4 个 app 实例下验证进程内锁不影响正确性。
- 1、2、4 个 dispatcher 下验证无重复可见副作用、无饥饿和有界积压。
- p50/p95/p99 延迟、锁等待、事务重试、CPU/连接池和队列年龄进入 test report。
- 正确性门禁固定;吞吐阈值在基线评审后写入环境配置与发布 SLO,不能靠降低一致性通过。
11. 迁移与兼容
采用 expand → migrate → contract:
- 新增兼容 schema、端口和读路径,不改变旧写入。
- 新写路径 shadow/dual-read 验证,禁止无审计双写。
- 按 checkpoint 幂等回填,异常进入 quarantine。
- 切主写者,保留旧 reader 一个明确发布窗口。
- 验证真实刷新/restart/rebuild、FAV coverage 和回滚。
- 删除旧路径前再次审计引用和数据差异。
旧 workspace 文件缺少 schema_ver 时按只读 legacy-v0 解析。
首次受控 Resume 先校验并原子升级到 v1,保留可回滚备份;未知 major 版本 fail closed。
回滚只保证回到最近兼容点。 已被下游里程碑消费的 schema 或契约必须按逆依赖顺序回滚,不能单独 revert 早期提交。
12. 首个交付边界
首个里程碑只覆盖受控文档可靠性:
flowchart TB
Read[Authorized content read] --> Ledger[Immutable ledger]
Ledger --> Projection{Projection aligned?}
Projection -->|yes| Return[Return canonical content]
Projection -->|no| Vault{Authorized FAV fallback enabled?}
Vault -->|yes| Degraded[Return verified degraded content]
Vault -->|no| Conflict[Typed consistency error]
Commit[Edit or agent commit] --> Tx[Single UnitOfWork]
Tx --> Ledger
Tx --> Root[Document root pointer]
Tx --> Stage[Artifact staging]
Tx --> Event[Outbox]
只有这个切片完成刷新、重启、并发、权限和恢复验收后,才启动其他包的物理拆分。
13. 后续扩展顺序
- 文档可靠性试点。
- taskrun finalize、RunManifest 持久化和 FAV 集成。
- 逐个事件替换闭包桥。
- chatlog/contextasm 原地端口化,再按证据移动包。
- squadrun capability 化和 backend policy。
- app 组合根迁移。
- UserProjectContext 增量收权。
- server/SSE 收口。
- 错误信封与 PI Runner 协议作为独立项目实施。
详细依赖、文件和测试场景见整改计划。