fixup! docs(client): document split ownership

This commit is contained in:
imccyu
2026-08-23 16:28:18 +08:00
parent 55dd6320d2
commit f5767ba15e
@@ -6,20 +6,456 @@ Status: implemented
## 问题
通用 Client Runtime 同时承载 Session 与 Workspace 对象、Conversation 组装、React hooks、Slot 注册表和 Store 引擎。领域消费者因此依赖一个持续扩张的聚合包,Session 快照也容易混入事件窗口与具体视图数据
Web Client 曾由一个通用 Runtime 同时承载 Session 与 Workspace 对象、事件窗口、Conversation 组装、React hooks、Slot 注册表和 Store 引擎。协议状态、业务投影、React 绑定和页面呈现共享同一个依赖汇点,任何一层的变化都可能扩大到完整前端
Session 快照也容易混入事件数组、Conversation View、Chat Node 和待处理交互等并非 Session 自身拥有的数据。普通消费者由此需要理解事件重放与具体视图,新增一个 Conversation target 也可能要求修改 Session、Runtime 和 renderer。
React 生命周期与 Session 生命周期之间缺少明确接口时,binding 释放、Hook source 替换和 Slot store 清理会演变为互相回调的专用协议。Approval 与 Question 同时影响侧边栏状态和 composer takeover;若两处各自维护状态,它们还可能选择不同的待处理请求。
需要把数据 owner、React adapter、通用渲染机制和具体视图拆成单向依赖,同时保持既有应用行为。
## 决定
Session 与 Workspace 的 Client 对象分别归 `api/session-controller/client``api/workspace-controller/client`,只发布 React-free 快照。`ui-session``ui-workspace` 提供 React adapter;需要同时读取两个 Controller 的初始选择、blank Session 复用和 New Session 导航归 `ui-workspace`,不形成联合快照。Session 快照不暴露原始事件,`ui-conversation` 从内部事件源组装 Conversation,再由 `ui-chat``ui-trajectory` 提供目标视图。Approval 与 Question 各自持有 pending 对象和 Remote Event listener,仅把统一 pending source 登记给 `ui-session`。Store 引擎归 `client/store`Slot 注册、scope materialization 与 hook 绑定归 `ui-renderer``client/runtime` 被删除
Client 采用“Controller 与领域对象 → UI adapter → renderer → Slot component”的分层。Controller 和领域对象发布不依赖 React 的 observable source;所属 `ui-*` package 声明标准 props 并注册 source`ui-renderer`Slot binding 点生成 selector hook;组件只从 Slot props 读取数据与操作
```text
[Remote / Controller / domain object]
|
| bare observable source
v
[ui-* adapter]
|
| standard source registration
v
[ui-renderer]
|
| selector hook binding
v
[Slot component]
```
Session 与 Workspace 的 Client 对象分别归 `api/session-controller/client``api/workspace-controller/client`。Conversation 的 target-neutral 数据结构和组装归 `client/ui-conversation`Chat 与 Trajectory 分别归 `client/ui-chat``client/ui-trajectory`
Session 与 Workspace 的 React 适配分别归 `client/ui-session``client/ui-workspace`。Store engine 归 `client/store`Slot registry、scope materialization 和 observable-to-hook 绑定归 `client/ui-renderer`
系统不提供聚合式 `client/runtime` package,也不设置替代它的总控 facade。Session history、Remote stream、分页 cursor 和重连连续性由 [Session 历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md) 定义;本 Note 从 Controller 发布的 Client 对象与 source 开始。
## 分层原则
### Controller 是无 React 的逻辑 owner
Controller 可以作为 Cordis service 安装,但不拥有 React Context、React hook、Slot props 或组件。Controller snapshot 只包含自身拥有的事实,命令只改变 Host 或领域对象状态。
UI 层可以同时读取多个 Controller 做一次导航决定,但不得把组合结果写回任一 Controller snapshot。UI adapter 也不复制 Controller 命令的业务实现。
### UI adapter 拥有 React 接入
每个标准 hook 归最接近其数据语义的 `ui-*` package。
| Hook | Owner | Source |
| --- | --- | --- |
| `useSessions` | `client/ui-session` | Session Controller 全局列表 |
| `useSession` | `client/ui-session` | 当前 Session snapshot |
| `useProjection` | `client/ui-session` | 当前 Session keyed projection |
| `useSessionPendingInteraction` | `client/ui-session` | pending domain 聚合结果 |
| `useWorkspaces` | `client/ui-workspace` | Workspace Controller 列表 |
| `useConversation` | `client/ui-conversation` | Conversation binding snapshot |
| `useChat` | `client/ui-chat` | `chat` target source |
| `useTrajectory` | `client/ui-trajectory` | `trajectory` target source |
`ui-renderer` 只实现通用绑定,不 import Session、Workspace、Conversation、Chat 或 Trajectory 的业务类型和值。
### Slot scope 与标准 props 分离
`ui-slots` 声明 root、session 和 session-maybe scope,以及可通过 declaration merge 扩展的标准 props 类型;它不决定每个 scope 安装哪些 hook。
`ui-renderer` 实现通用 scope adapter 与 source materialization。`ui-session` 安装 Session scope 并提供内建 source,其他领域 package 只注册自己的 source 和消费它的 Slot entry。
新增 target 不要求 renderer 或 Session Controller 增加分支。数据 owner 负责状态身份、更新、错误和释放;UI adapter 负责 hook;显示 owner 负责 target-specific projection 与交互状态。
## Package 所有权
| Package | 拥有内容 | 明确不拥有 |
| --- | --- | --- |
| `api/session-controller/client` | Session 对象、列表、选择、命令、projection、queue、事件窗口和 Agent Context | Conversation target、React、Slot、Workspace |
| `api/workspace-controller/client` | Workspace 对象、顺序、归档、命令和 snapshot | React、Session 导航策略、目录 UI |
| `client/ui-session` | Session scope、标准 source、`SessionProvider`、pending interaction 聚合 | Session transport、Conversation 组装、Approval/Question 结果 |
| `client/ui-workspace` | Workspace hook、浏览器 UI 和跨 Controller 导航策略 | Workspace transport、Session 数据副本 |
| `client/ui-conversation` | Conversation core、registry、binding、shell、input、composer、queue 和 View 导航 | Session transport、Chat/Trajectory snapshot |
| `client/ui-chat` | Chat target、Node definitions、renderer、selection、details、locale 和历史图片 | Session 生命周期、通用 View 导航、Trajectory |
| `client/ui-trajectory` | Trajectory target、事件记录投影和检查视图 | Session snapshot、Chat snapshot |
| `client/ui-approval` | Pending Approval、Remote listener、composer 和审批 UI | Session control、通用 composer election |
| `client/ui-user-questions` | Pending Question、Remote listener、composer 和问题 UI | Session control、通用 composer election |
| `client/store` | React-free store contract 与实现 | 领域对象、React hook、Slot 生命周期 |
| `client/ui-renderer` | SlotRegistry、scope binding、selector hook、outlet 和 React root | Session、Workspace 与 Conversation 业务逻辑 |
## 总体数据流
Session 数据按以下路径进入 UI:
```text
[ctx.remote.session]
|
v
[api/session-controller/client]
|-- SessionListState --------------------------> [ui-session] -> useSessions
|-- SessionSnapshot ----------------------------> [ui-session] -> useSession
|-- ProjectionValueSource ----------------------> [ui-session] -> useProjection
`-- per-Session SessionEventSource
|
v
[client/ui-conversation]
|
| assemble
v
ConversationSnapshot ----------------> useConversation
|
|---------+----------|
v v
[ui-chat] [ui-trajectory]
| |
useChat useTrajectory
```
Workspace 数据从 `ctx.remote.workspace` 进入 Workspace Controller,再由 `ui-workspace` 暴露为 `useWorkspaces`;需要跨域导航时,`ui-workspace` 临时读取 Session Controller 并发出选择或命令。
Approval 与 Question 从 Host waterfall 经 `ctx.remote.$on` 到达各自 UI owner。Owner 发布 Pending 对象,`ui-session.pendingInteractions` 再把同一对象送往 Session 导航状态和 Conversation composer selection。
## Session Controller Client
### SessionSnapshot 的范围
`SessionSnapshot` 表示 Session 自身的控制与生命周期事实。它可以包含 identity、running、removed、blank、subagent address、open phase、history phase、prompt error、agent error 和 queue 状态。
它不包含以下数据:
- raw event array
- Conversation View
- Chat Node
- Trajectory row
- Approval 或 Question 的待处理对象;
- 要求调用者遍历 event 才能解释的呈现状态。
字段由 event、control frame 或本地命令推导,并不自动决定其 owner;消费语义决定 owner。`composerPhase` 同时依赖 Session lifecycle 与 Conversation target activity,因此由 `ui-conversation` 合成,不进入 `SessionSnapshot`
### 三个读取面
Session Controller 对外提供三个互不替代的读取面:
1. 全局 Session list 与 current selection source,供导航和 `useSessions` 使用。
2. 每个 Session 的逻辑 binding,包含 `sessionId``SessionSnapshot` source、commands 与 projection sources。
3. Conversation-facing `SessionEventSource`,只供 Conversation assemble core 使用。
普通 UI component 不直接读取 `SessionEventSource``ui-session` 不读取私有 event window`ui-conversation` core 也不接收 React binding 或 Slot API。
### SessionEventSource
`SessionEventSource` 暴露已经物化的事件窗口,而不是 transport。
窗口携带有序 `entries``hasMore`、单调 `revision`,以及 `replace | prepend | append` 变更描述。
首次打开、重连、gap repair 和无法证明连续性的更新发布 `replace`;历史分页发布 `prepend`;连续 live event 发布 `append`。Conversation core 依据 revision 与 change 选择增量更新或完整 rebuild。
`MutableSessionEventSource` 是 Session Controller 内部写端,消费者只依赖只读的 `SessionEventSource`
### Session binding 生命周期
每个 Session binding 持有自己的 Cordis Context 与 Fiber。Session Controller 创建 binding,也负责释放它。
依赖 Session 的对象把清理注册到 `binding.ctx.effect()`。Binding 释放会触发 Conversation binding、UI materialization 和 scoped Slot store 的清理,不存在额外的 `onBindingRelease``onRelease` 回调协议。
这种清理方式不要求 Session Controller 了解上层消费者名册。
## UI Session
### 服务职责
`client/ui-session` 是 Session Controller 与 React/Slot 系统之间唯一的 Session adapter。它提供 `ctx.uiSession`,并负责:
- 观察 Session list、current selection 和 per-Session binding
- 安装 session 与 session-maybe scope adapter
- 提供 `SessionProvider` 的呈现语义;
- 内建 session snapshot、projection 和 sessionId source
- 接收其他领域 package 的 Session-scoped source contribution
- 聚合业务 package 注册的 pending interaction。
它不拥有 Session transport、event folding、Conversation target 或具体业务结果。
### 标准 source 注册
领域 package 调用 `ctx.uiSession.provide()` 注册 bare source。Descriptor 静态声明 hooks、keyedHooks 和 props 名册,`resolve(binding)` 为一个 Session binding 返回完全对应的值;例如 `ui-conversation` 把每个 binding 的 snapshot 注册为 `conversation` hook source。
普通 source 被 renderer 转换成 `use<Name>`Projection 等开放 key 空间通过 keyed hook resolver 暴露,稳定值通过 props 暴露。
运行时拒绝未声明、缺失或重复的标准 prop。`ui-session` 自身也走相同 materializationrenderer 不为 Session 名字写特殊分支。
### Scope binding
session 与 session-maybe 使用同一个 adapter,但绑定语义不同:
- strict session scope 在没有 current binding 时拒绝渲染;
- session-maybe 使用稳定 absent binding,保持 hook 调用顺序;
- current Session 切换以 `sessionId` 为 key 重建严格 Session subtree
- root 与 session-maybe entry 可以跨 Session 切换常驻。
每个真实 materialized binding 保留 Controller binding 的 Context。`ui-session` 通过 `binding.ctx.effect()` 删除缓存项并撤销 current binding。
Contribution roster 变化会重建已 materialize 的 binding 并发布新的 source 集合。同一 binding 生命周期内,source identity 保持稳定,以满足 `useSyncExternalStore` 的缓存要求。
### SessionProvider
`SessionProvider``PropsRenderSlots` 根据 session-scoped child 声明派生的标准席,不是业务 component 直接 import 的 React Context。
它接收普通 `ReactNode` children,不接收 `(sessionId) => ReactNode` render function;调用方直接用它包裹 `renderSlot('details', {})`
Session identity 通过 scope binding 和标准 `sessionId` prop 提供。Provider 只负责 absent branch 与按 Session identity 隔离 subtree,组件不得借助 Provider 回调取得 Session 数据。
### Pending interaction
`SessionPendingInteractionMap` 由业务 package declaration merge 扩展。每个 pending object 至少携带稳定 `key`、领域 `kind``sessionId``ui-session` 不 import Approval 或 Question 的具体类型。
业务 plugin 在 `apply()` 中调用 `registerPendingInteraction(precedence)`,为自己的 pending domain 建立稳定注册。该调用返回逐请求 publication functionpublication function 发布一个精确对象,并返回移除该对象的幂等 disposer。
相同 key 的并发对象被拒绝,替换请求必须使用新 key。同一 Session 可以同时存在多个领域或多个请求。
`ui-session` 使用各 domain 的 precedence 选出每个 Session 当前生效的对象。较高 precedence 胜出,相同 precedence 下后遍历到的有效对象胜出。
聚合结果发布为 `pendingInteractions: ObservableSnapshot<ReadonlyMap<SessionId, SessionPendingInteraction>>``useSessionPendingInteraction` 是其 React 读取面。
Session 导航状态和 composer takeover 必须读取同一个 effective object,不得分别维护 status map 或 takeover roster。
## Workspace Controller 与 UI Workspace
### WorkspaceSnapshot 的范围
`WorkspaceSnapshot` 只包含 Workspace Controller 拥有的 Host-authoritative 数据,包括 Workspace rows、顺序、archive set、follow phase 和错误。Workspace row 的 `sessionIds` 是关联字段,不等于把 Session 对象复制进 Workspace snapshot。
以下组合事实不进入 `WorkspaceSnapshot`
- Workspace 与 Session 两条 baseline 是否同时 ready
- 根据 Session 更新时间推导的最近 Workspace;
- 当前 Session 是否因归档而清除;
- New Session 应复用哪个 blank Session
- 首次启动应选择哪个 Session。
### UI Workspace 的组合职责
`client/ui-workspace` 把 Workspace list source 注册为 root 标准 source `workspaces`renderer 由此提供 `useWorkspaces`
初始选择、blank Session 复用、新建导航、并发创建合并和归档后导航属于 UI navigation policy。该 policy 可以在决定时同时读取 `ctx.workspaces``ctx.sessions`,但只调用 Controller command 和 selection action,不发布联合 snapshot。
目录 picker、目录浏览和 `openPath` 属于独立目录能力,不进入 Workspace Controller。
## UI Conversation
### Assemble core
`client/ui-conversation` 同时包含不依赖 React 的 Conversation assemble core 和同领域的 React adapter。
Core 拥有 `ConversationSnapshot`、Definition registry、View registry、event assembler、location index、每 Session binding、target source 和 target activity。
Core 从 Session binding 取得 `SessionEventSource`。连续 revision 的 append 与 prepend 使用增量组装;replace 或 revision 断档从完整窗口 rebuild。
Definition 或 View roster 变化只重建 Conversation binding,不重建 Session 或重开 Remote stream。Core 不 import React,可独立测试事件折叠、增量更新和 registry lifecycle。
`ConversationSnapshot` 不复制 `SessionSnapshot`,也不暴露 raw events;它只发布 target-neutral 的 View 名册、target activity 和 target source lookup。
`useSession``useConversation` 来自两个 source,不承诺在同一个 React commit 原子发布。同时读取两者的组件按当前 snapshot 纯计算,不把通知顺序解释为业务因果。
### Definition 与 View registry
`UiConversation.events` 是 event Definition 的唯一 registry`UiConversation.views` 是 target snapshot builder 的唯一 registry。
Registry 拒绝重复 key,保持注册顺序并返回幂等 disposer。Roster 变化时,现有 Conversation binding 使用当前 event window 重建。
Target package 通过 declaration merge 扩展 snapshot 与 location data map,再向 registry 注册自己的 Definition、builder 和 View。注册随 Cordis effect 释放。
`ui-conversation` 不 import 具体 target package。
### Conversation React adapter
React adapter 把每个 Conversation binding 的 snapshot 注册为 Session 标准 source `conversation`renderer 由此提供 `useConversation`
同包还拥有 shell、input、composer chain、queue UI、draft、View navigation 和 phase 合成;Core 不读取 React Context、Slot props 或 component state。
View 选择顺序固定为:有效的持久化 selection、已注册的 `chat`、无 View。无效 selection 不覆盖持久化值,系统不 fallback 到第一个已注册 View。
没有 `ui-chat` 时 shell 仍能激活和 mount,但不会隐式选择 Trajectory 或其他 target。
Shell phase 由 Session lifecycle 与 Conversation target activity 纯合成。Session 已 active 或任一 target 报告可见内容时显示 active;首条 prompt 失败仍保持 engaging。
### Input 与 composer
Composer chain 属于 `ui-conversation`,具体 takeover 属于业务 package。`ConversationRoot``useSessionPendingInteraction` 读取当前 Session 的 effective object,并作为 `ComposerChainProps.pendingInteraction` 交给 chain selector。
Selector 是 owner currency 的纯函数,非 null 结果作为 `matched` 传给获选 component。Stable composer entry 与默认 composer 可以同时常驻,chain 只选择一个有效呈现。
Draft 与输入状态属于 Conversation UI,不进入 Session snapshot。Queue command 通过 Session-scoped service 寻址,不把 queue UI 写入 Conversation core。
## Chat 与 Trajectory target
### Chat owner
`client/ui-chat` 注册 target id `chat`,并拥有 Chat snapshot builder、Conversation Node definitions、keyed node renderers、selection、details、stats、locale、tool inspection 协作和历史图片 cache。
它通过 `ctx.uiSession.provide()` 注册 `chat` target source。`ChatNodeSeat` 和 Chat 内部消费者使用 `useChat`,不再传递 `useConversation(snapshot => snapshot.views.get('chat'))`
Chat activity 只由可见且非 command 的 Chat Node 激活。普通 command-only history 保持 Hero`/goal``command-input` Node 激活 fresh Conversation。
历史图片 cache 的 Session key、pending promise、generation guard、blob URL 和 disposer 同属 `ui-chat`Draft 图片仍属于 Conversation input。
### Trajectory owner
`client/ui-trajectory` 通过相同 target 协议注册 `trajectory`。它拥有事件记录、时间线、虚拟行、selection 和 inspection view,并通过标准 source 提供 `useTrajectory`
Session 生命周期读取 `useSession`Trajectory 数据读取 `useTrajectory`。Trajectory 不通过 Session snapshot 或 Chat snapshot 取得自己的数据。
其他 target 使用同一注册流程,不修改 renderer、Session Controller 或 ui-session。
## Approval 与 Question
### 稳定注册
Approval 和 Question 的 plugin 安装分为稳定注册与单次请求处理。`apply()` 注册 locale、调用 `registerPendingInteraction()` 注册本领域 pending domain,并向 `conversation.composer` 注册唯一稳定 entry。
Approval 的 detail child Slot 也由稳定 entry 声明。并发请求和 Session 数量不会增加 composer entry 或重复声明 Slot,所有注册随 plugin fiber 释放。
### 单次 waterfall 请求
Remote Event listener 从自身 Agent Context 解析 Session。没有 Session scope 时调用 `next()` 继续 waterfall;存在 Session scope 时创建 `PendingApproval``PendingQuestion`
Listener 通过已注册 domain 的 publication function 发布对象,等待用户完成、取消或请求 signal 中止,并在 `finally` 中精确移除对象。
单次请求不注册 Slot,不创建第二套 lifecycle effect,也不修改 Session snapshot。
Approval 暴露 allow 与 rejectQuestion 暴露 answer 与 cancel。用户主动取消 Question 返回 `ASK_CANCELLED`;等待中的请求被 `AbortSignal` 中止时返回 `UserQuestionError(ASK_ABORTED)`,不泄漏载体的 `AbortError` 或普通 `Error`
Gateway 只要求 Remote Event 参数和结果是合法 JSON 传输值,不复制 Question 选项的领域校验。
### 单一 pending 投影
Sidebar 与 composer 使用相同 `pendingInteractions` snapshot。导航根据 effective object 的 `kind` 显示审批、计划审阅或问题状态,composer entry 根据对象实例选择自己的面板。
同一请求 identity 同时驱动两处 UI。新请求替换同类型旧请求时使用新 key,因此 selector 与订阅者都观察到身份变化。
`ui-session` 只实现跨领域 precedence,不解释 Approval 或 Question 的字段。
## UI Renderer 与 Store
### UI Renderer
`client/ui-renderer` 拥有 `SlotRegistry` service 和 React renderer。它负责:
- `ctx.slots.register()``inject()``renderSlot()` 与声明生命周期;
- root、session 和 session-maybe scope adapter
- 标准 observable source 到 selector hook 的绑定;
- Slot outlet、错误隔离、root mount 与 hydration
- 按 scope key 管理 Slot store instance 生命周期。
Renderer 可以认识通用 scope 名称和 binding 协议,但不读取领域 service。渲染 Session scope 而没有安装 adapter 是装配错误,并立即失败。
### Store
`client/store` 是 React-free 普通库,拥有 `ObservableSnapshot``SnapshotStore``defineStore``createSnapshotStore``shallowEqual`
`ui-slots` 引用 store contract`ui-renderer` 管理 store instance 并提供 `useStore`
Store 只承载 draft、View selection、Chat selection、inspection request 和面板尺寸等观看或交互状态。Session、Workspace、Conversation、Remote stream 和 connection generation 不进入 Store。
### 注册与释放顺序
一个 plugin 同时提供 source 与 Slot entry 时,先注册 source,再注册 entry。Cordis 反向 disposal 先移除 entry,再移除 source,仍挂载的 entry 因而不会短暂失去必需 hook。
Session binding 释放通过 `binding.ctx.effect()` 清理 UI materialization 与 scoped store。Plugin fiber 释放通过 registration disposer 清理 source、listener 和 Slot entry。
所有 disposer 都可重复调用,不依赖 Cordis 生命周期以外的隐式回调。
## 组合与依赖方向
应用 bundle 显式安装所需 Controller、adapter、target 和 renderer plugin。每个 owner 的 `apply()` 只安装自己的 service、listener 和 contribution。
运行时消费方向是 `session-controller → ui-session → ui-conversation → target UI``workspace-controller → ui-workspace``store → ui-slots → ui-renderer`Approval 与 Question 只依赖 `ui-session` 提供的 pending 注册点。
图中的箭头表示运行时消费关系,不覆盖 type-only declaration merge 边。Controller 不反向依赖 UI adapterrenderer 不反向依赖领域 packageConversation core 不依赖具体 target。
UI component 不接收 `ctx`。跨 package 协作使用 Cordis service、standard source 或 Slot registration,不新增聚合 facade。
## 开发者遵循方式
### 先确定数据 owner
新增状态前先按消费语义确定唯一 owner:Host 通信、命令和实体生命周期归 API Controller;由 Session events 形成且与 target 无关的数据归 Conversation core;只服务一种 View 的投影归对应 target package;草稿、选择和面板状态归拥有该交互的 UI package。
同一事实不得同时保存在 Controller snapshot、Conversation snapshot 和 Store。需要跨域决策时读取多个 source 并立即发出 command,不创建联合 snapshot,也不缓存另一领域的对象副本。
以下信号表示 owner 选择错误:Controller 开始 import Reactrenderer 出现业务类型分支;组件遍历 Session eventsStore 保存 Session 或 Workspace 实体;一个 target 的变化要求修改 Session Controller。
### 新增 Session-scoped 数据
1. 在领域 owner 中提供 React-free observable source。
2. 在所属 UI adapter 中 declaration-merge 标准 prop 类型。
3. 通过 `ctx.uiSession.provide()` 声明固定 roster,并从 Session binding 解析 source。
4. 让 Slot component 从 `PropsRuntime` 获得生成的 hook,不向组件传 `ctx`
5. 把每个 binding 的资源清理挂到 `binding.ctx.effect()`,把 registration 清理留给 plugin fiber。
6. 测试缺失值、重复名字、roster 替换、Session 切换和 binding disposal。
只有开放 key 空间使用 keyed hook;有限且稳定的 source 使用普通 hook;不会变化的标识使用 prop。不得为了减少一次注册而把业务名称硬编码进 renderer。
### 新增 Conversation target
1. 在 target package 中扩展 Conversation snapshot 或 location data map。
2.`UiConversation.events` 注册所需 event Definition。
3.`UiConversation.views` 注册 snapshot builder、target id、View 与 activity 规则。
4. 通过 `ctx.uiSession.provide()` 暴露该 target 的标准 selector hook。
5. 在同一 package 中注册 renderer、locale 和 target-specific Slot entry。
6. 验证 target 卸载只重建 Conversation binding,不改变 Session、其他 target 或 Remote stream。
Target 不得读取另一个 target 的 snapshot 作为自己的数据源。可选协作通过窄 port 或 Slot 完成;缺失 target 时,shell 必须保持可启动且不得猜测 fallback。
### 新增 pending-interaction 业务
1. 业务 package 定义 Pending 对象及其完成、取消和中止语义。
2. 通过 declaration merge 把对象加入 `SessionPendingInteractionMap`
3.`apply()` 中调用 `registerPendingInteraction()` 一次,并注册唯一稳定的 composer entry。
4. Remote waterfall listener 从 Agent Context 解析 Session;无法处理时调用 `next()`
5. 可处理时创建 Pending 对象,使用 publication function 发布,等待结果,并在 `finally` 中移除。
6. 测试并发 key、precedence、用户取消、transport abort、plugin disposal 和无 Session delegation。
单次请求不得注册 Slot、声明 child Slot、修改 Session snapshot 或另建状态索引。Sidebar 与 composer 都从 `useSessionPendingInteraction` 读取同一个 effective object。
### Review 检查点
- 每个新 source、registry contribution、listener 和 cache 都有明确 Cordis fiber 或 Session binding owner。
- 每个公共 hook 能追溯到唯一 React-free source;不存在只为传参而层层转发的 selector。
- 每个 component 的数据与 action 都来自标准 props 或所属 Slot inject face。
- 每个 target 在缺席、动态注册和卸载时都有定义明确的结果。
- 每个跨层 import 都沿 Controller、adapter、renderer、component 的单向关系前进。
- 每个错误由最早能解释其语义的 owner 归类;载体错误不直接泄漏成业务错误。
## 验证
各 owner 的测试分别固定 Controller binding 与 event source、UI scope 与 pending precedence、Conversation 增量组装与 View fallback、target projection、waterfall 结果以及 renderer 的 scope/store 生命周期。应用组装测试同时覆盖完整 roster 和缺少具体 target 的启动;组件测试不替代对象层、重放和生命周期测试。
## 备选方案
**保留 Runtime facade。** 这会继续形成依赖汇点并允许新代码绕过领域 owner。
**让 Controller 直接提供 React hooks。** 这会让协议与状态对象依赖 React阻止非 React 消费者复用。
** Conversation 数据放回 Session 快照。** 这会让每个目标视图的结构变化扩大 Session API,并迫使普通消费者理解事件组装
- **保留 Runtime facade。** 它维持单一入口,却继续形成依赖汇点并允许新代码绕过领域 owner;系统因此不保留 facade 或兼容出口
- **把所有 Client 状态放进 API Controller。** 这会让协议对象承担 React、View 和 presentation policyController 因而只保留无 React 的领域状态。
- **让 Controller 直接提供 React hooks。** 这会阻止非 React 消费者复用同一对象,也使 transport 与 renderer 生命周期相互依赖
- **把 Conversation 放进 SessionSnapshot。** 这会扩大 Session API,并迫使普通 Session 消费者理解 event folding 与 target roster。
- ** Chat 和 Trajectory 各自重放 Session events。** 这会重复维护顺序、location 和 registry rebuild;共享 assemble core 因而留在 `ui-conversation`
- **把 Conversation core 拆成额外的非 UI package。** Core 与 adapter 当前共同演化且没有其他非 UI package 消费者;同包目录隔离足以保持 React-free core。
- **把 Workspace 与 Session 合成联合 snapshot。** 这会制造新的跨域 owner;跨域逻辑保留为 `ui-workspace` 的即时决策。
- **让 renderer 内建所有标准 hook。** 这会要求通用基础设施认识每个领域;standard source registration 保持 renderer 与业务类型解耦。
- **让每个 pending 请求动态注册 composer entry。** 这会重复声明 child Slot,并让并发请求竞争注册顺序;稳定 entry 与请求期对象发布保持分离。
- **把 pending interaction 写回 Session projection。** 待回答 waterfall 不是已提交的持久 Session 事实,刷新恢复由 Remote Event replay 负责,因此它留在业务 UI source。
- **为 binding 增加专用 release callback。** 这会重复 Cordis 生命周期;`binding.ctx.effect()` 已能把消费者清理挂到同一 owner。
- **让 SessionProvider 通过 render function 传 Session id。** 这会产生另一条数据注入路径;普通 children 与标准 `sessionId` prop 保持 scope 数据只有一个入口。
- **把 Store 留在 renderer。** Store contract 不依赖 React,并被对象与测试基础设施复用;独立 `client/store` 保持 engine 与渲染生命周期分离。
## 后果
数据 owner、React adapter 和具体视图可以独立演化,Slot 仍通过标准 props 注入 hook。代价是组合包必须显式装载所需 adapter 和视图插件;缺失具体目标插件时 shell 仍可运行,但不生成该目标视图
Session、Workspace、Conversation 与具体 target 各自拥有一份权威状态,非 React consumer 可以直接复用 Controller 和 assemble core。新增 Conversation target 只需注册 Definition、builder、View、标准 source 和 Slot entry;新增 pending-interaction 业务只需声明类型、注册 domain 并提供稳定 composer entry
Renderer 和 Session Controller 不因新增业务领域而增加分支,Session binding 与 plugin fiber 则提供两条明确且可组合的释放路径。UI 可以观察到 Session 与 Conversation source 的独立发布,消费者不得依赖二者的通知顺序。
组合包必须显式装载所需 adapter 与 target plugin。缺失具体 target 时 shell 仍可运行,但不会生成或猜测该 target 的 View。更多 package 和显式注册增加了装配工作,但依赖方向、测试范围与故障 owner 均可局部确定。