Web 会话协作实施计划¶
状态:正式功能与交办语义改造已实现,正在收口验证。原型不作为生产代码直接搬入。 本计划只覆盖同一 daemon 管理的项目与 Web UI,不新增 TUI 功能。
进度:P1–P4 已完成并验证(模型与持久化、受控执行闭环、Agent 工具与授权、服务契约与持久呈现)。 P5(正式 Web UI)已完成:输入框会话引用、对话下方协作提示、右栏协作面板与对方会话只读预览。 P6(收口)已完成真实 daemon 子进程和浏览器证据(模型受控);真实默认模型仍未通过。
当前语义:可识别的协作与独立交办¶
本节覆盖后文历史实施记录中的“新轮次退役旧许可”和“停止只作用于来源”等旧规则。
| 身份 | 当前含义 |
|---|---|
collaboration_id |
稳定协作组,当前沿用 scope_id 的持久身份;可显式复用给多个目标交办 |
assignment_id |
一件独立交办,沿用 request_id;单项许可与控制版本独立 |
message_id |
不可变消息身份;可信运行元数据与消息正文均可识别 |
turn_id |
一次实际执行;审批恢复保持交办身份,并关联恢复后的执行 |
- 普通新用户轮次不会撤销既有协作。结果处理轮次携带原交办,不被解释成当前其他工作的插话。
- 指挥者可在
contact_session指定已有collaboration_id;被交办方不能隐式转派。 reply_to_collaboration(kind='progress')保存进度通知但不启动对方轮次;clarification、supplement、reply仍在轮次边界处理,目标正在做的工作不被抢占。- Web 分别展示交办状态、执行状态(含审批阻塞)、结果处理状态;不把轮次结束当作任务成功。
- Web 控制指定
assignment_id:仅接收抑制该项来源接续;撤销停止该项两侧关联执行, 不影响同组其他交办或目标自己的工作。旧 scope-wide RPC 保留原有语义,不在新 UI 使用。 - 邮箱 schema 2 自动迁移 schema 1,保留历史、旧停止栅栏;未知版本仍拒绝。
- 回执在仅接收模式也自动刷新:有未完成交办/投递时每两秒查询,结束或切会话后自停。 预览依赖稳定目标身份,避免重新渲染触发重复请求;繁忙显示可理解提示,不隐藏重试。
当前不做:普通用户消息持久排队、任务 DAG、审批代办、跨 daemon 通信、隐式多级转派。
本轮验证与收尾¶
- Python 协作、生产 Agent 图、daemon 子进程重启、会话与 wire 契约回归:297 passed。
- 前端全量:1198 passed / 1 failed;失败仍是既有
themeContract的 “flyouts settle in, and reduced motion is respected”,本轮未改主题。 tsc -b、Ruff、契约生成--check、前端构建、MkDocs、git diff --check通过。web/tests/collaborationUi.verify.ts:真实宿主与 daemon、生产 UI、受控模型, 引用发起→指挥者在对方处理期间执行独立工作→原交办回执仍触发接续→只读预览; 1280/1024/820px 无横向溢出,无浏览器异常。- 同脚本
--receive-only:不手动刷新、不启动来源接续,回执正文仍自动显示为待处理。 - 审批暂停不结束交办,恢复保留可信元数据并关联实际 turn;单项撤销不误停其他交办。
- 提交被拒不重试、不变成插话;输入框收到接受结果后才清稿,拒绝时草稿保留。
真实默认模型首次响应仍超时;受控模型验收不等同于外部模型成功闭环。 多会话汇总看板、直接编辑/补充交办的 Web 操作尚未实现;当前提供身份、消息、状态与单项控制基础。
优先级:稳定性 → 职责内聚 → 有证据的扩展点 → 功能数量。本计划由主 Agent 独立完成,不使用子 Agent。
1. 交付目标与明确不做的事¶
用户在 A 的输入框引用 B,要求 B 协助;A 组织必要上下文后继续独立工作;B 在自己的项目和会话上下文中处理;回复自动回到 A,在授权仍有效时接续。点击 B 可在现有右栏只读预览,正常闭环无需切换。
| 第一版必须交付 | 第一版不做 |
|---|---|
| 同项目、跨项目的既有会话;在已注册项目中新建目标会话 | 远程 daemon 互联、自动发现未注册项目 |
| 只读引用与发起请求分离 | 将选择引用等同于立即执行 |
| 新请求排队、明确回复、一次请求内的澄清与补充 | 群聊、永久上下文同步、多级自动转派 |
| 结果持久化、有限自动接续、停止后仅接收 | 通用工作流引擎、任务 DAG、插件调度器 |
| 页面关闭不影响后台协作;故障后的状态可核验 | 承诺任意工具副作用 exactly-once 或无条件自动恢复 |
| 原型确定的输入、执行记录、回执、预览触点 | 独立协作工作台、交付表单、角色切换、TUI 新界面 |
同项目会话目前共享目录。通信不会提供文件锁、独立 worktree 或自动合并;修改任务须明确分工,目标沿用自己的工作区、模型和审批约束。
2. 当前代码事实与必须尊重的边界¶
| 代码位置 | 已有行为 | 实施约束 |
|---|---|---|
runtime/sessions/manager.py |
项目级并发额度、单会话提交锁、生命周期锁、等待结算释放 | 协作执行仍走 submit_ref,不直接启动 graph |
runtime/sessions/runtime.py |
turn reservation、取消栅栏、结算持久化、审批暂停 | 运行结束事件不等于已完成结算;不能在锁内或结算回调中递归启动 |
runtime/service/commands.py |
CommandReceipt 表示实际启动;command_id 用于关联 |
不把既有 submit 改成“已排队”;关联 ID 不是现成幂等保证 |
runtime/steer.py |
内存插话,下一模型步注入,结算时清空 | 不作为可靠的跨会话收件队列 |
runtime/daemon/application.py |
daemon 装配 router、各项目 manager 和持久化,连接独立 service | 协作协调器必须属于 daemon,不属于浏览器连接 |
runtime/service/recovery.py |
transcript 与实时 broker 不共享原子日志 | 不把 WebSocket 序号当作持久化提交凭证 |
sessions/transcript_projection.py |
会话历史可由 checkpoint 重建 | 不能只往临时 UI 或可重建投影中追加回执 |
runtime/service/access.py、web_console/host.py |
ACL 与项目范围约束 | 源、目标都校验;内部工具调用不能绕过 scope |
web/src/components/composer/ |
富文本 pill,目前普通 mention 主要序列化为文本 | 会话引用新增类型化身份,不能仅靠显示名称或路径 |
web/src/stores/sessionViews.ts |
既有多会话后台视图和恢复逻辑 | 预览复用有界订阅,不重建第二套 live reducer |
以上 Python 路径相对 src/synapse/。实现开始前须检查工作区是否有新的并行变更。
3. 最小内聚结构¶
新增一个 src/synapse/runtime/collaboration/ 领域,不在 local.py、manager.py 或 Web store 中分散实现协作规则。
| 起步模块 | 唯一主要职责 |
|---|---|
model.py |
不可变数据、合法状态转换、大小限制与接续判定 |
store.py |
一个 SQLite 实现;事务、唯一约束、条件更新与有界查询 |
coordinator.py |
请求、澄清、回复、撤销;保存后登记投递,校验请求归属和接续许可 |
runtime.py |
有界投递循环;适配现有 router/manager、结算唤醒与重启核验 |
tools.py |
Agent 工具适配;从可信运行上下文取得来源,不让模型伪造来源或权限 |
这不是五个服务;它们在一个进程内,由 daemon 显式装配。只有需要替换为测试替身的执行和存储边界使用窄接口,不新增通用注册器、事件总线或抽象 repository 基类。
其他层只接线:
runtime/service/collaboration.py定义纯 DTO;ports.py、access.py、contract_registry.py接入现有协议和授权。app/agent.py/agent_assembly.py接收可选协作资源,装配工具;没有 daemon 协作资源时不启用,不强迫 CLI/TUI 引入后台服务。daemon/application.py持有唯一协调器;关闭时先停止领取新投递,允许已执行轮次结算和写回,再关闭存储。sessions/manager.py/runtime.py只增加必要的提交来源、启动关联和结算通知接点;不理解协作业务状态。- Web 新增独立的协作投影模块;
useConsoleStore只组合动作,不成为调度器。
依赖方向为 Web/工具适配 → 协作协调器 → 存储与执行窄边界。纯模型和服务契约不得导入 UI、LangChain 或具体 daemon。
4. 数据与状态:区分消息、执行和接续¶
在 daemon 的 state_dir 下规划一个私有 collaboration.sqlite。由 daemon lease 保证实例 ownership;不向两个项目数据库做跨库原子事务,也不为每次请求单独建库。
起步只保留四类记录,具体表结构在 P1 固定:
| 记录 | 必要信息 |
|---|---|
| Request | 请求 ID、双方 SessionRef、来源工作代次、范围、状态、创建/更新时间 |
| Message | 稳定消息 ID、请求内顺序、发送方、类型、不可变正文及引用快照、展示锚点 |
| Delivery | 消息到指定会话的投递意图、执行标识、领取状态、关联 turn、错误/核验原因 |
| Continuation scope | 来源会话、工作代次、允许接续/仅接收/撤销状态、版本号 |
请求类型围绕一次委托,不把所有关系抽象成一张任意消息图。消息类型限制为请求、澄清、补充、最终回复;诊断信息单独记录,不自动触发往返。
三条状态轴不混用:
- 请求:待处理 → 处理中 → 待补充(可回到处理中)→ 已回复;异常为失败、撤销或待核验。
- 投递:待投递 → 已领取/准备启动 → 已关联执行 → 已结算;无法判定的启动结果进入待核验,不自动重新投递。
- 来源接续:有效 → 仅接收 / 已撤销;收到回复本身不能把撤销改回有效。
“已回复”只说明对方明确提交了最终回复,不代表来源已使用,更不代表代码验证成功。来源 UI 分别展示已送达、接续中和实际执行结果。
5. 第一版的确定性规则¶
5.1 投递不阻塞 Agent¶
- A 的联系工具校验目标和范围,在同一事务中保存请求、首条消息和目标投递意图。
- 工具立即返回“已接受请求”的凭证,不等待 B,不消耗 A 的并发额度干等。
- 后台协调器按目标会话串行领取,通过既有 manager 提交;B 忙或待审批则保留排队。
- B 沿用自身 graph 和设置,收到带来源的协作上下文。合作内容不冒充用户新授权。
- B 通过明确回复工具保存结果,并在同一事务中登记回传。普通 final answer 不自动当作协作完成。
- A 可继续独立工作,走到依赖点结束本轮;界面显示协作等待,不新增一个占用执行槽位的“等待轮次”。
同目标 FIFO;每个目标最多一个投递 worker;全局 worker 数量、队列容量、单条文本与返回页均有上限。达到上限明确拒绝新请求,不接收后丢弃。结算通知只唤醒协调器;领取和提交在锁外的统一运行循环中进行,辅以有界周期核验。
若 B 结束而既未回复也未提出澄清,显示“未返回协作结果”,不得伪造成功或无限自动追问。失败、取消和待审批分别保留明确原因。
5.2 首版仅在轮次结算后接续¶
第一版不把外部回执塞进现有 SteerQueue,不改造模型步级中断协议。
- A 忙:回执先持久化并显示;当前轮结算后再判断是否接续。
- A 空闲且许可有效:合并同一工作代次已到达的有限批次回复,启动一个接续轮。
- A 等待时不占用 manager semaphore;A、B 可以分别向前推进。
- 澄清/补充也在轮次边界处理,不保持双方执行锁互相等待。
- 工具说明明确禁止通过循环查询或 sleep 等待对方。
这是对原型“自动接续”的稳定实现,不承诺首版在任意工具批次后实时注入。只有实测证明轮次边界延迟影响使用,才独立规划持久化模型步收件能力。
5.3 工作代次与停止¶
工作代次不是新的用户任务系统,只是内部防止旧回执启动新工作的版本标记。
- 普通用户新提交默认建立新代次。自动接续和本次澄清继承原代次。
- 停止、改为仅接收、会话删除都会先撤销相应许可。
- 对活跃工作的新用户插话,首版保守使旧自动接续许可失效;请求仍存在,回执仍显示。不用关键词或模型猜测“是不是换了任务”。
- 用户明确要求继续某次回执时,建立新的有效许可;不静默复活旧代次。
- 关闭网页、切换项目、只读预览均不改变许可。
停止与接续必须由服务端在同一会话生命周期协调边界排序。提供一个“停止本次工作”的协作控制动作:撤销许可,撤销尚未启动的来源接续,并取消属于该代次的活跃轮次。前端不能用两个无关联 RPC 先 cancel 再禁用接续。
停止成功返回后,不允许任何该代次的新轮次启动。若接续已经先赢得启动权,停止必须识别并取消它,不能只拿旧 turn ID 调取消后忽略 mismatch。
停止 A 不自动终止 B:尚未投递的请求可撤销;B 已启动则允许回复,A 仅接收。绝不取消 B 正在执行的其他任务。
5.4 防止回复循环¶
接收方只允许对入站请求澄清或回复,不继承向第三个会话转派的权限。首版不允许 A→B→A 被误解释成新的委托链。澄清次数、自动接续次数有明确上限;达到上限保留消息并请求用户介入,不再自动唤醒。
6. 可靠性:不靠“重试一下”掩盖执行未知¶
保证消息可恢复和投递幂等,不承诺任意外部工具恰好执行一次。
- 请求/回复使用服务端绑定的稳定操作键(关联来源轮次及工具调用),唯一约束防止重试产生第二条请求;相同键但不同正文返回冲突。
- 保存消息与登记回传同库事务。数据库写入失败就返回失败,不显示“已发送”。
- 投递前持久化执行意图;将
delivery_id和工作代次放入可信的冻结请求元数据,关联实际 turn。 - claim 使用版本条件更新;同一投递不能被两个 worker 同时启动。不把进程内锁当持久化幂等。
- 现有
command_id只关联响应,现有on_started在执行提交后触发;两者都不能直接补上启动崩溃窗口。P2 必须明确准备、启动与关联持久化的线性化点。 - 进程在“可能已经启动,但关联记录未保存”处崩溃,重启后核对持久化请求标识/checkpoint;有确证则对齐状态,无法证明则进入待核验,不盲目再次执行。
- 过期领取不等于肯定没执行,不能直接释放重试。模型、shell 和文件副作用不能与 SQLite 事务原子提交。
| 故障位置 | 恢复策略 |
|---|---|
| 尚未写入成功 | 不返回接受凭证,调用方用相同操作键核验/重试 |
| 已写入、尚未领取 | 启动后继续调度 |
| 已领取、启动结果不明 | 核验执行标识;没有证据就暂停 |
| 已回复、来源尚未执行 | 从持久化投递恢复,重新检查代次和停止状态 |
| 回复已显示、浏览器断开 | 重连读取事实记录,不依靠旧 WebSocket 游标 |
| 项目不可用或会话已删除 | 明确标记原因,不自动注册项目或重新创建会话 |
| 待审批或执行失败 | 保留可见状态,不自动批准、不无限重试 |
顺序关闭先停止领取,再让 manager 结算并保存最后的回复,最后关协作库。异常重启不等于承诺自动重做中途被打断的代码修改。
7. 历史、实时展示与权限¶
单一事实来源¶
协作库是请求/回执事实的唯一来源;checkpoint 只记录送入 Agent 的协作上下文与稳定消息 ID。WebSocket 负责通知更新,不承担永久保存。
Web 对话按服务端返回的 anchor_turn_id、会话内顺序和 message_id 合并协作记录,不仅按浏览器时间戳排序。已送入 checkpoint 的回执与库中同 ID 回执折叠为一条。未接续的回执仍可读取,不需要制造一个空 turn 来持久化。
新增有界的协作查询:当前会话的待处理概览、指定请求详情、可见历史页关联的消息。实时事件只提示哪些记录/版本变化;重连、事件缺口和历史翻页重新查询。不得为此全量读取两个项目的 checkpoint 或不断加载全部历史。
首次实现即覆盖“完整历史由 checkpoint 重建后,来源、引用和去重标识仍正确”。不能只在实时路径加好看的卡片。
权限与来源¶
- 引用身份是明确的
(project_id, thread_id);显示名称、文件路径和原始 prompt 都不作为授权依据。 - 选择引用是目标范围,不是立即运行。只读引用不产生协作投递;执行工具仍遵守用户指令、readonly 设置和已有审批规则。
- 入站用户轮次由授权层验证引用;工具从可信上下文取得被允许的目标集合,不允许模型填入来源身份、ACL 或工作代次。
- 已定:允许跨项目协作。 目标可以位于同一 daemon 下已注册的另一个项目;跨项目读写分别验证双方能力,不因为同源就跳过目标侧校验。
- 浏览器侧项目范围不变:控制台仍不能直接寻址其他项目,跨项目只发生在服务端协作投递上,
project_scope继续约束 UI 的会话列表与直接读写。 - 已定:默认开启跨项目协作,
project_scope=all与workspace都允许,无需额外配置。 - 仍提供显式关闭开关(按部署或按项目):关闭后工具不向模型提供跨项目目标,投递一律拒绝,且错误不泄露其他项目的名称或路径。
- 需要留意的后果:
workspace原本是单项目边界(浏览器不能寻址其他项目)。跨项目默认开启后,A 项目会话可经服务端投递读写 B 项目会话,即边界从"不可达"变为"需用户显式要求协作"。面向多工作区/多用户的部署若依赖该边界,应显式关闭。 - 目标项目集合始终限制在同一 daemon 已注册的项目内;跨项目不获得目标项目的其他会话或文件访问权,也不因来源请求而放宽目标侧的审批与 readonly 设置。
- 目标项目集合由 daemon 配置决定并作为工具的可选范围下发;模型只能在该范围内选择,不能自行填入
project_id,也不能借内部工具绕过连接级约束。 - 跨项目只共享本次请求所需的背景与结论;来源不因此获得目标项目的其他会话或文件访问权。
- 排队到实际执行间再次核对目标存在、范围和授权有效性。无法重建有效授权时暂停,而非使用全权 daemon 身份替代。
- 非参与方无法凭请求 ID 读正文。隐藏项目不泄露标题或路径。
- 目标不会因为来源写了“忽略审批”而改变权限,也不能替用户批准操作。
文档结果优先返回有界结论;文件引用带所属项目、会话、相对路径及修订信息。读取走已有 artifact 服务的权限边界;不把另一个工作区的绝对路径当附件。超长正文明确截断或提供受控分页,不默认同步目录。
8. Web 接入范围¶
| 位置 | 改动方式 |
|---|---|
composerDocument.ts、mentionCatalog.ts、输入动作清单 |
增加 session pill 和结构化引用;保留普通文本、文件/技能引用与图片行为 |
runtime-client |
使用生成契约,增加有界协作查询与控制;不得手写另一套 wire DTO |
独立 collaborationView |
合并实时提示、持久消息和历史锚点;按消息 ID 去重 |
transcriptRows 清单 |
紧凑联系记录和带来源回执,展开实际请求;不向 Transcript.tsx 塞协作分支 |
| 右栏清单及会话视图 | 临时只读会话预览;订阅有所有者和上限,关闭释放,不占用无限后台视图 |
| 底栏、输入停止按钮 | 区分正在执行与等待回执;等待时也可撤销接续,不依赖 active turn 存在 |
已有用户运行中发送仍是插话:新增的会话引用必须随 steer 一并验证和持久化,不能静默丢弃引用。前后端版本不支持时保留草稿并明确提示,不能退化成看似成功的纯文本。
新建目标会话沿用现有 session create,采用稳定 ref 处理重试;引用选择器取消不自动启动会话。预览不改当前执行对象,也不提供第二个输入框。
使用能力协商决定入口是否出现;没有能力时不显示可点击的空功能。不修改现有普通 submit 回执语义、不重写历史恢复协议、不引入路由或全局协作页面。
9. 分批实施与过关条件¶
下列阶段按依赖执行,每批都要求测试通过后再进入下一批;不是分别交付半成品给用户。最终仍以完整 Web 闭环验收。
| 阶段 | 产物与主要文件范围 | 进入下一阶段的门槛 |
|---|---|---|
| P0 规划与边界 | 本文;固定交互范围、状态/恢复规则 | 已完成规划;未实现生产功能 |
| P1 纯模型与持久化 | runtime/collaboration/model.py、store.py;扁平 tests/test_collaboration_model.py、test_collaboration_store.py |
SQLite 事务/幂等/容量/重开恢复、非法状态和参与方检查通过;不调用模型 |
| P2 受控执行闭环 | coordinator/runtime 适配;manager 必要接点;daemon 生命周期 | 两个临时项目、受控 turn runtime 完成请求→回复→接续;并发、停止竞争、崩溃窗口测试通过 |
| P3 Agent 与授权 | 工具、可信轮次上下文、结构化引用、跨项目目标范围与关闭开关、装配;明确回复/澄清 | 真实 agent graph + 假模型验证工具契约;不能伪造目标/来源、不能循环唤醒;跨项目默认可用,关闭后一律拒绝且不泄露目标信息 |
| P4 契约与持久呈现 | service DTO、registry、transport、访问控制、历史查询与事件;生成 manifest/TS | Python/TS fixture、旧客户端兼容、回执历史重建、恢复/去重、导入纯度通过 |
| P5 生产 Web UI | composer、独立协作投影、消息行、右栏预览、等待/停止 | 使用生产组件与模拟 runtime 的浏览器测试通过,不搬入原型 timer/场景数据 |
| P6 收口 | 真实 daemon 子进程恢复测试、文档和发布前检查 | 页面关闭/重开、daemon 重启、取消与迟到回复、多会话竞争全部有证据;仅此时标记功能完成 |
P1 的数据身份决定 P2 的启动关联。P2 与 P3 稳定后才冻结新增 wire 形状;不先让前端依赖易变接口。每批保持旧路径默认不变,未完成能力不向 UI 宣告可用。
首批具体工作¶
- 固定 Request / Message / Delivery / ContinuationScope 的最小字段、允许转换和具名错误。
- 为状态转换先写表驱动测试,特别是停止不可被回执反转。
- 实现单 SQLite 存储、schema version、唯一约束、事务与有界查询。
- 验证重复请求、不同正文冲突、同目标顺序、停止与回传并发、重开库后的未投递恢复。
- P1 不修改正式输入框、不挂接真实 Agent、不开始后台投递。
必须覆盖的竞态与故障矩阵¶
- 同一请求工具重试 / 同一回复重试,只有一份消息和一个投递意图。
- 两个来源同时请求一个目标;目标已有普通工作或等待审批。
- 双向协作 A→B、B→A 不持有执行槽位互相等待;不会形成自动委托环。
- 停止与接续同时发生;多个浏览器发送或停止;旧工作不能在停止确认后重启。
- 一次来源工作有多份回复,合并有上限、逐消息去重,不并发启动两个来源轮次。
- 澄清答复、目标没有显式交付、执行失败、用户接管目标任务、会话删除、项目不可用。
- 故障注入到数据库提交前后、领取后/启动前、执行提交后/关联保存前、回执保存后/通知前。
- 只读引用不唤醒;未授权读取/发送被拒;跨项目默认可用但只能落在已注册且未被关闭的项目集合内,关闭后工具与投递都不能越界,拒绝信息不含目标名称或路径。
- live、断线重连、翻历史、checkpoint 重建四条展示路径一致。
- 中文 IME、键盘选择、窄屏、关闭预览释放订阅、无真实网络/模型的浏览器验收。
测试遵循窄测试 → 领域测试。wire 改动必须执行:
uv run --no-sync python scripts/export_contract_manifest.py --check
uv run --no-sync pytest tests/test_runtime_contract_manifest.py tests/test_runtime_architecture_boundaries.py tests/test_runtime_service_import_purity.py tests/test_runtime_transport_client_compatibility.py -q
# web/ 下
npx tsc -b
node --test tests/runtimeContractFixture.test.ts tests/runtimeClientBoundary.test.ts tests/sourceGuard.test.ts
再跑新增协作测试、受影响的 manager/session/daemon/persistence/access 测试及真实浏览器脚本。Python 修改跑 Ruff;文档修改跑 MkDocs。Windows 本地验证,平台相关的 SQLite/进程关闭逻辑同时审查 Linux,并由 CI 覆盖。
10. 完成定义与进度¶
已定决策:允许跨项目协作,默认开启(保留显式关闭开关);project_scope 继续约束 UI 的直接寻址,跨项目只经服务端投递。自动接续仍限定在轮次结算后。
| 项目 | 状态 |
|---|---|
| Web v2 交互原型确认 | 已确认 |
| 当前代码边界核对与实施计划 | 已完成 |
| P1 协作模型与 SQLite 存储 | 已完成(53 项测试通过) |
| P2 受控执行闭环 | 已完成(投递循环、排队、停止竞争、崩溃分类) |
| P3 Agent 工具与授权 | 已完成(两个工具、跨项目策略、代次轮换) |
| P4 服务契约与持久呈现 | 已完成(读与停止两个方法,契约与 TS 已生成) |
| P5 生产 Web UI | 已完成(会话引用、对话下方提示、协作面板、对方会话预览) |
| P6 收口 | 真实 daemon 子进程闭环与重启不重放通过(模型受控);浏览器验收通过(web/tests/collaborationUi.verify.ts);默认模型首次响应超时,两个界面缺陷待决策 |
真实运行故障修复与补充验收¶
指定会话 8487b3cc8ee0 的真实验收暴露了两个执行边界问题:
- 通用路径中间件按参数名把
contact_session.target从project:thread改成/project:thread,导致目录查找误报项目未注册;并非跨项目策略拒绝。 路径改写现仅限本地虚拟文件工具,协作标识和 MCP 等其他工具参数保持原样。 - 同步工具在工作线程调用投递循环的
asyncio.Event.set()不安全;现通过所属事件循环的call_soon_threadsafe唤醒。启动前和关闭后的通知不触碰循环,持久队列仍是恢复依据。
新增证据:
| 验证 | 结果 |
|---|---|
tests/test_collaboration_agent.py |
修复前复现 project-not-registered;修复后生产 Agent 图的同步/异步、裸 ID/引用令牌四种路径均能联系并显式回复 |
tests/test_middleware_async.py |
覆盖非文件参数透传和本地文件路径规范化,兼容对象/字典及同步/异步调用 |
tests/test_collaboration_runtime.py |
工具线程通知在 owner loop 执行,启动前/关闭后通知安全 |
tests/test_collaboration_subprocess.py |
两次独立 daemon 启动,真实 Agent 图、manager、WebSocket 与 SQLite;请求→读取目标文件→显式回复→来源接续,重启后历史相同且不重放;仅模型返回受控 |
| 默认模型实测 | 新 daemon、两个工作区内只读临时项目;gemini-3.8-flash-high 首次响应等待 180 秒超时,尚不能宣称真实模型闭环通过 |
可重跑真实模型验收:uv run --no-sync python scripts/verify_session_collaboration.py --timeout 180。
脚本在 .tmp/collaboration-check/ 下隔离项目、目录库和 daemon,继承默认模型且禁止子 Agent;
不选择已有项目、不替换正在使用的 daemon,不输出凭据。成功必须验证来源已经消费回复,
不能仅凭请求为 replied 判通过;随后关闭连接、重启并核对历史与不重放。
超时或失败同样关闭自己的进程,Windows 必要时清理所启动的进程树,避免只结束 venv launcher。
本轮最终回归:协作(含真实图和子进程)、Agent factory、路径中间件、manager/session、
契约与架构相关测试 386 passed;修改文件 Ruff、契约导出 --check、MkDocs 构建和
git diff --check 均通过。MkDocs 仍有仓库既存的导航/锚点提示。
本轮没有修改正式 Web UI,也没有重新声称其浏览器验收通过。
浏览器验收(真实宿主 + 真实 daemon + 无头浏览器)¶
用仓库既有 CDP 设施(web/tests/helpers/cdp.ts)跑通了正式协作界面,
并另用外部 CDP 工具(chrome-devtools-mcp)在可见浏览器里复核了同一流程。
可重跑脚本:web/tests/collaborationUi.verify.ts
它启动隔离 daemon(脚本模型,回复由闸门文件控制)、真实 synapse-web-console 宿主、
真实 dev server 与无头 Chrome,全部断言通过:
- 会话列表加载并选中来源会话;
@只提供其他会话(当前会话被排除),回车插入引用标签;- 提交后提示条显示「我请对方 / 对方会话 / 对方处理中」,并给出「仅接收结果」「停止本次协作」;
- 右栏「协作」显示方向、消息类型与请求正文;
- 放行后提示条转为「已收到结论」,来源会话真的消费了对方的结论;
- 右栏「协作」显示交付结论本身,而不只是状态;
- 「对方会话」只读预览显示对方的真实转录,并提供「进入完整会话」;
- 1280/1024/820px 下无横向溢出,浏览器无异常。
宿主用 --no-pairing 启动(文档化的本地调试开关),配对流程本身由既有验收覆盖。
本轮发现并修掉的缺陷:MentionFlyout 只渲染 file/skill/context 三种分组,
session 分组从未被渲染——目录、图标、文案都齐了,但浮层永远不画它,
因此读者在正式界面上根本无法引用另一个会话,协作功能不可达。
现改为由 mentionCatalog.MENTION_KIND_ORDER 统一给出分组顺序(同时必须与
RichComposer.offersFor 的条目顺序一致,否则方向键会高亮/插入错行),
composerMentions.test.ts 的守护测试改为遍历该列表,并断言浮层使用它。
仍未修、需要决策的两个缺陷:
- 等待中的回执不会自动出现。 提示条只在会话切换与「来源轮次结算」时重读。
若来源选择「仅接收结果」,对方结论到达时来源没有任何轮次结算,
提示条会一直显示「对方处理中」;实测邮箱里请求已是
replied、 来源投递为cancelled: scope_disabled,界面仍显示等待。打开右栏「协作」 (或切会话)后两处一起刷新,说明数据正确、只是缺少刷新触发。 方向选择:(a) 有待处理请求时做有界轮询(UI-only,请求结束后自停); (b) 服务端在写入回执时向来源会话补一个事件(改动 wire/事件契约,更贴合事件驱动)。 - 对方会话预览在传输繁忙时只显示原始错误。 连接 in-flight 达上限时,
daemon 返回
transport_busy,预览面板把该错误原样显示, 需要读者自己点「刷新对方会话」。建议改成可重试的明确状态并自动重试一次。
仅接收结果路径已在可见浏览器中人工验证:点击「仅接收结果」后,
邮箱显示请求 replied、来源投递 cancelled(scope_disabled)、来源代次 receive_only,
来源会话没有被自动续跑(与 P1 固定的规则一致);差的就是上面的刷新触发。
P5–P6 交付与验证证据¶
P5(web/):
- 输入框
@新增session引用:标签用会话名,令牌是@session:<project_id>:<thread_id>;候选来自侧栏已加载的会话,不额外读取;当前会话被排除。 runtime-client新增listCollaborations/stopCollaboration,类型与分页默认值随契约导出。stores/collaborationView.ts:只读视图投影(角色由参与方决定、状态与消息类型有文案表、 未知值不空白),并判定停止控制是否可用。CollaborationNotice.tsx:对话下方就地提示,按会话切换与每轮结算刷新, 等待期间提供「仅接收结果 / 停止本次协作」。- 右栏「协作」面板(完整列表与控制)与「对方会话」面板(只读预览 + 进入完整会话)。
为什么提示条没有做成 transcript 行:transcript 是运行时的投影,而本地合并的行会被下一次 历史加载清空、只在下次刷新时才回来(回执会时隐时现)。就地提示条位于同一阅读列、紧邻输入框, 且与会话切换/结算同步,因此没有这个时序问题。
P6(受控端到端,tests/test_collaboration_e2e.py):两个真实 RuntimeManager(两个项目)
加真实 submit_ref、提交锁、并发额度与结算等待,只有 turn 本身受控:
- 请求真的从 p1 的会话进入 p2 的会话,来源在目标工作期间保持空闲;
- 目标自己的用户轮次先跑,协作投递排队且不抢占,结束后按序执行;
- 停止通过 manager 真的取消了来源的接续轮次,且不触碰目标已完成的工作;
- 重启(同一邮箱、全新组合)把中断投递分类为
interrupted、请求置failed,不重放。
uv run --no-sync pytest tests/test_collaboration_*.py tests/test_runtime_contract_manifest.py \
tests/test_runtime_architecture_boundaries.py tests/test_runtime_service_import_purity.py \
tests/test_runtime_manager.py tests/test_session_runtime.py -q # 240 passed
cd web; npx tsc -b; npm test # 1195/1196
后续已补真实 daemon 子进程验收与浏览器 .verify.ts 级验收,详见上方补充证据。
仍缺默认模型成功闭环(首次响应超时,属外部服务问题);两个界面缺陷
(等待中回执不自动刷新、预览的传输繁忙错误)已记录待决策,未修复。
P2–P4 交付与验证证据¶
新增 collaboration/{coordinator,runtime,tools,service_adapter}.py 与 service/collaboration.py;
接线到 daemon/{application,config,entry}.py、sessions/manager.py、
service/{local,ports,access,contract_registry}.py、transport/protocol.py;
契约清单与 web/src/runtime-client/contract.generated.ts 已重新生成。
已用测试固定的行为:
- 请求→目标执行→回复→来源接续的完整闭环;等待结算不占用并发额度。
- 目标忙碌时保持 FIFO 且不抢占;不同接收方互不阻塞;同一接收方同时只有一个 owner。
- 停止先赢则不启动来源接续;接续先赢则必须取消该轮次;目标自身的运行工作不被触碰。
- 停止后迟到的回复仍被保存(请求为
replied),但来源投递为cancelled,不会重启。 - 目标结算却未回复 → 请求
failed(no_result),不伪造成功。 - 崩溃后
recover_uncertain只做分类、settle_interrupted标记为已结束,绝不重放。 - 工具身份来自可信运行上下文:模型不能伪造来源、请求 ID 或工作代次;角色与状态双重校验; 同一 tool call 重试幂等。
- 新用户轮次把上一代次降为
receive_only(请求保留、结果不再自动启动来源), 显式停止才是revoked;投递轮次绝不轮换代次。 - 跨项目默认允许;
--no-cross-project-collaboration关闭后工具与投递都拒绝。 - 客户端只能读取与停止:没有任何 wire 方法能发起请求或投递消息。
uv run --no-sync pytest tests/test_collaboration_*.py tests/test_runtime_contract_manifest.py \
tests/test_runtime_architecture_boundaries.py tests/test_runtime_service_import_purity.py \
tests/test_runtime_transport_client_compatibility.py tests/test_runtime_manager.py \
tests/test_session_runtime.py -q # 307 passed
uv run --no-sync python scripts/export_contract_manifest.py --check # up to date
cd web; npx tsc -b; node --test tests/runtimeContractFixture.test.ts \
tests/runtimeClientBoundary.test.ts tests/sourceGuard.test.ts # 22 passed
已知的既有失败(与本次改动无关,纯净检出同样失败,已用 git stash 复核):
tests/test_runtime_service_contracts.py 的两项源码扫描断言,
以及 tests/test_runtime_daemon_s8.py::test_mcp_rebinding_writes_only_what_was_asked_and_reuses_a_live_pool。
P2–P4 明确未做:未改正式 Web UI;未提供“发起请求”的 wire 方法(它是 Agent 动作); 未做群聊、永久同步与多级转派。
P1 交付与验证证据¶
新增 src/synapse/runtime/collaboration/{__init__.py,model.py,store.py} 与扁平测试
tests/test_collaboration_model.py、tests/test_collaboration_store.py。存储为单一
collaboration.sqlite(user_version=1,未知版本只读拒绝),全部写入走 BEGIN IMMEDIATE,
请求/消息按服务端操作键幂等,回执与投递意图同事务写入。
已用测试固定的规则:
- 停止(
REVOKED/RECEIVE_ONLY)取消待投递项、把已领取项保留为review,不重放; - 新工作代次撤销旧许可,但重试旧代次不会撤销当前有效许可;
- 每目标 FIFO 且同一目标同时只有一个 owner(跨连接并发领取验证);
- 交付方向按消息类型校验:终态请求不能启动新目标工作,最终回复仍可送达来源;
- 补充答复与最终回复会作废已过期澄清,
claimed/running的未知项不被静默抹掉; - 重启只做有界核验(
recover_uncertain),待投递保留、未决领取不自动重试。
uv run --no-sync pytest tests/test_collaboration_model.py tests/test_collaboration_store.py -q # 53 passed
uv run --no-sync ruff check src/synapse/runtime/collaboration tests/test_collaboration_*.py # All checks passed
uv run --no-sync pytest tests/test_runtime_architecture_boundaries.py \
tests/test_runtime_service_import_purity.py tests/test_runtime_contract_manifest.py -q # 65 passed
P1 明确未做:未装配 daemon、未注册 Agent 工具、未新增 wire 方法、未改正式 Web UI;参与方检查 只是纵深防御,不是项目 ACL。
完成必须证明:A 无需离开当前会话就能协作,B 忙时不被抢占,回执可恢复且不重复触发,许可有效时自动继续,停止后绝不被迟到结果重启。任何“状态未知”必须如实呈现,不能用重试掩盖。