Client Session, Conversation, and UI ownership layers
Client Session、Conversation 与 UI 所有权分层
The Web Client once placed Session and Workspace objects, event windows, Conversation assembly, React hooks, the Slot registry, and the Store engine in one general Runtime. Protocol state, business projections, React bindings, and page presentation shared one dependency hub, so a change in any layer could spread across the entire frontend. Session snapshots could also accumulate data they did not own, including event
English
Problem
The Web Client once placed Session and Workspace objects, event windows, Conversation assembly, React hooks, the Slot registry, and the Store engine in one general Runtime. Protocol state, business projections, React bindings, and page presentation shared one dependency hub, so a change in any layer could spread across the entire frontend.
Session snapshots could also accumulate data they did not own, including event arrays, Conversation Views, Chat Nodes, and pending interactions. Ordinary consumers then had to understand event replay and concrete views, while adding a Conversation target could require changes to Session, Runtime, and the renderer.
Without an explicit interface between React and Session lifetimes, binding release, Hook source replacement, and Slot store cleanup became dedicated callback protocols. Approval and Question both affect sidebar state and composer takeover; independently maintained state could make those surfaces select different pending requests.
The Client needs one-way dependencies between data owners, React adapters, generic rendering machinery, and concrete views while preserving application behavior.
Decision
The Client uses the layering “Controller and domain object → UI adapter → renderer → Slot component.” Controllers and domain objects publish React-free observable sources; their ui-* packages declare standard props and register sources; ui-renderer creates selector hooks at Slot binding points; components read data and actions only from Slot props.
[Remote / Controller / domain object]
|
| bare observable source
v
[ui-* adapter]
|
| standard source registration
v
[ui-renderer]
|
| selector hook binding
v
[Slot component]
Client Session and Workspace objects belong to api/session-controller/client and api/workspace-controller/client, respectively. Target-neutral Conversation data structures and assembly belong to client/ui-conversation; Chat and Trajectory belong to client/ui-chat and client/ui-trajectory, respectively.
The React adapters for Session and Workspace belong to client/ui-session and client/ui-workspace. The Store engine belongs to client/store; the Slot registry, scope materialization, and observable-to-hook binding belong to client/ui-renderer.
The system has no aggregate client/runtime package and no replacement central facade. Session history and event transport defines history continuity. Client Session references owns reference acquisition, exact-generation lifetimes, main-area ownership, and unified UI status; this note owns the layering and source-registration rules.
Layering principles
Controllers are React-free logic owners
A Controller may be installed as a Cordis service, but it does not own React Contexts, React hooks, Slot props, or components. A Controller snapshot contains only facts that it owns, and its commands change only Host or domain-object state.
The UI layer may read multiple Controllers for one navigation decision, but it does not write the combined result back into any Controller snapshot. A UI adapter does not duplicate a Controller command's business implementation.
UI adapters own React integration
Each standard hook belongs to the ui-* package closest to its data semantics.
| Hook | Owner | Source |
|---|---|---|
useSessions | client/ui-session | Session Controller global list |
useSession | client/ui-session | Bound Session snapshot |
useProjection | client/ui-session | Bound Session keyed projection |
useSessionStatus | client/ui-session | Running, effective pending request, and unread completion |
useSessionRetainInfo | client/ui-session | Read-only Controller reference-source counts |
useWorkspaces | client/ui-workspace | Workspace Controller list |
useConversation | client/ui-conversation | Conversation binding snapshot |
useChat | client/ui-chat | chat target source |
useTrajectory | client/ui-trajectory | trajectory target source |
ui-renderer implements only generic binding. It does not import Session, Workspace, Conversation, Chat, or Trajectory business types or values.
Slot scopes and standard props are separate
ui-slots declares root, session, and session-maybe scopes plus declaration-merge-extensible standard prop types. It does not decide which hooks each scope installs.
ui-renderer implements generic scope adapters and source materialization. ui-session installs the Session scope and supplies its built-in sources; other domain packages register only their own sources and the Slot entries that consume them.
Adding a target does not add a branch to the renderer or Session Controller. The data owner handles state identity, updates, errors, and release; the UI adapter owns the hook; the presentation owner owns target-specific projections and interaction state.
Package ownership
| Package | Owns | Explicitly does not own |
|---|---|---|
api/session-controller/client | Session objects, catalog, references, source counts, commands, projections, event windows, and Agent Contexts | Navigation, completion reminders, Conversation targets, React, Slots, Workspace |
api/workspace-controller/client | Workspace objects, ordering, archive state, commands, and snapshots | React, Session navigation policy, directory UI |
client/ui-session | Explicit Session scope, standard sources, SessionProvider, and unified UI status | Session transport, reference ownership, Conversation assembly, Approval/Question results |
client/ui-workspace | Workspace hook, browser UI, main-area reference, and cross-Controller navigation policy | Workspace transport, copies of Session data |
client/ui-conversation | Conversation core, registries, bindings, shell, input, composer, queue, and View navigation | Session transport, Chat/Trajectory snapshots |
client/ui-chat | Chat target, Node definitions, renderers, selection, details, and locale | Session lifecycle, generic View navigation, Trajectory, historical-image cache |
client/ui-trajectory | Trajectory target, event-record projection, and inspection view | Session snapshots, Chat snapshots |
client/ui-approval | Pending Approval, Remote listener, composer, and approval UI | Session control, generic composer election |
client/ui-user-questions | Pending Question, Remote listener, composer, and question UI | Session control, generic composer election |
client/store | React-free Store contract and implementation | Domain objects, React hooks, Slot lifetimes |
client/ui-renderer | SlotRegistry, scope binding, selector hooks, outlets, and React root | Session, Workspace, and Conversation business logic |
Overall data flow
Session data reaches the UI through this path:
[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 data enters the Workspace Controller from ctx.remote.workspace, then ui-workspace exposes it as useWorkspaces. ui-workspace reads explicit targets for cross-domain navigation and owns the main-area reference without making it a default business Context.
Approval and Question arrive from the Host waterfall through ctx.remote.$on at their respective UI owners. Each owner publishes a Pending object; ui-session.sessionStatus supplies the same effective object to Workspace indicators and Conversation composer selection.
Session Controller Client
Scope of SessionSnapshot
SessionSnapshot represents control and lifecycle facts belonging to a Session. It may contain identity, running, removed, blank, subagent address, open phase, history phase, prompt error, agent error, and queue state.
It does not contain:
- a raw event array;
- Conversation Views;
- Chat Nodes;
- Trajectory rows;
- pending Approval or Question objects;
- presentation state that requires callers to traverse events.
Whether a field derives from an event, control frame, or local command does not automatically determine its owner; consumption semantics determine ownership. composerPhase depends on both Session lifecycle and Conversation target activity, so ui-conversation composes it instead of placing it in SessionSnapshot.
Three read faces
The Session Controller exposes three distinct read faces:
- The Session catalog and local ownership sources, used by
useSessionsand read-only reference metadata consumers. - A logical binding for each Session containing
sessionId, aSessionSnapshotsource, commands, and projection sources. - A Conversation-facing
SessionEventSourceused only by the Conversation assembly core.
Ordinary UI components do not read SessionEventSource directly. ui-session does not read private event windows, and the ui-conversation core receives neither React bindings nor Slot APIs.
SessionEventSource
SessionEventSource exposes a materialized event window, not a transport.
The window carries ordered entries, hasMore, a monotonic revision, and a replace | prepend | append change description. Append links an immutable segment in constant time; a consumer that needs the complete entries array materializes and caches it for that snapshot.
Initial open, reconnect, gap repair, and updates whose continuity cannot be proven publish replace; history pagination publishes prepend; a continuous live event publishes append. The Conversation core selects incremental update or complete rebuild from the revision and change.
MutableSessionEventSource is the Session Controller's internal write face. Consumers depend only on the read-only SessionEventSource.
Session binding lifecycle
Each live Session generation owns a Cordis Context and Fiber. The Controller creates its binding on acquisition and retires it on final reference release or root disposal.
Objects that depend on a Session register cleanup through binding.ctx.effect(). Generation retirement cleans up Conversation bindings, UI materializations, and scoped Slot stores without a dedicated onBindingRelease or onRelease callback protocol.
This cleanup does not require the Session Controller to know the roster of upper-layer consumers.
UI Session
Service responsibilities
client/ui-session is the sole Session adapter between the Session Controller and the React/Slot system. It provides ctx.uiSession and:
- observes the Session catalog, local reference metadata, and explicitly supplied bindings;
- installs the session and session-maybe scope adapters;
- supplies
SessionProviderrendering semantics; - supplies built-in Session snapshot, projection, and sessionId sources;
- accepts Session-scoped source contributions from other domain packages;
- aggregates domain-owned pending interactions with running and completion-reminder facts in
sessionStatus.
It does not own Session transport, event folding, Conversation targets, or concrete business results.
Standard source registration
A domain package calls ctx.uiSession.provide() to register a bare source. The descriptor statically declares its hook, keyed-hook, and prop rosters; resolve(binding) returns exactly those values for one Session binding. For example, ui-conversation registers each binding's snapshot as the conversation hook source.
The renderer converts an ordinary source into use<Name>. Open key spaces such as projections use a keyed-hook resolver, while stable values use props.
The runtime rejects undeclared, missing, or duplicate standard props. ui-session materializes its own built-ins through the same mechanism, so the renderer has no Session-specific name branches.
Scope binding
session and session-maybe use the same adapter with different binding semantics:
- a strict session scope refuses to render without an explicitly supplied binding;
- session-maybe uses a stable absent binding to preserve hook call order;
- changing the exact Context generation remounts a bound subtree, including same-id replacement;
- an unbound session-maybe entry adopts its first binding without remounting; root entries have no Session binding.
Each UI materialization borrows the Controller binding's Context. ui-session removes that generation's cache entry and publishes absence through binding.ctx.effect(); it does not retain the Session.
Changing the contribution roster rematerializes existing bindings and publishes a new source set. Source identity remains stable within one binding lifetime, as required by useSyncExternalStore caching.
SessionProvider
SessionProvider is a standard seat derived by PropsRenderSlots from a session-scoped child declaration, not a React Context imported directly by business components.
It accepts ordinary ReactNode children and a required session={reference | undefined}. The Provider borrows the caller-owned reference without acquiring or releasing it; callers wrap renderSlot('details', {}) directly.
Session identity reaches components through the explicit scope binding and standard sessionId prop. An absent Provider stays unbound, and neither nested Providers nor root entries fall back to a main-area Session.
Pending interactions
Business packages extend SessionPendingInteractionMap through declaration merging. Every pending object carries at least a stable key, domain kind, and sessionId; ui-session does not import concrete Approval or Question types.
A business plugin calls registerPendingInteraction(precedence) in apply() to create a stable registration for its pending domain. The returned per-request publication function publishes one exact object together with its waterfall-delegation callback and returns an idempotent disposer for that object. Plugin teardown removes all published objects before invoking and awaiting their delegation callbacks, so active Host requests cannot remain suspended after their Client answerer unloads.
Concurrent objects with the same key are rejected; replacement requests use a new key. One Session may hold multiple domains or requests at once.
ui-session selects each Session's effective object using domain precedence. Higher precedence wins; at equal precedence, the later valid object in traversal order wins.
The pending aggregate is private to ui-session; its effective request appears unchanged as sessionStatus.getSnapshot().get(id)?.pendingInteraction. useSessionStatus is the public UI read face.
Session navigation state and composer takeover read the same effective object. They do not maintain separate status maps or takeover rosters.
Workspace Controller and UI Workspace
Scope of WorkspaceSnapshot
WorkspaceSnapshot contains only Host-authoritative data owned by the Workspace Controller, including Workspace rows, order, archive set, follow phase, and errors. A Workspace row's sessionIds is an association field, not a copy of Session objects in the Workspace snapshot.
These combined facts do not enter WorkspaceSnapshot:
- whether the Workspace and Session baselines are both ready;
- the most recent Workspace derived from Session update times;
- whether the current Session is cleared because it was archived;
- which blank Session New Session should reuse;
- which Session initial startup should select.
UI Workspace composition responsibilities
client/ui-workspace registers the Workspace list source as the root standard source workspaces, from which the renderer provides useWorkspaces.
Initial restoration, blank-Session reuse, new-session navigation, concurrent-create coalescing, and navigation after archival are UI policy. ui-workspace may read both Controllers, but it keeps the main target and reference in its own navigation owner instead of writing UI selection into a Controller snapshot.
Directory pickers, directory browsing, and openPath are separate directory capabilities and do not enter the Workspace Controller.
UI Conversation
Assembly core
client/ui-conversation contains both the React-free Conversation assembly core and the React adapter for the same domain.
The core owns ConversationSnapshot, the Definition registry, the View registry, the event assembler, the location index, per-Session bindings, target sources, and target activity.
The core obtains SessionEventSource from a Session binding. Append and prepend changes with continuous revisions use incremental assembly; replace changes or revision gaps rebuild from the complete window.
Definition or View roster changes rebuild only the Conversation binding; they do not rebuild a Session or reopen a Remote stream. The core does not import React and can test event folding, incremental updates, and registry lifetimes independently.
ConversationSnapshot does not copy SessionSnapshot or expose raw events. It publishes only the target-neutral View roster, target activity, and target-source lookup.
useSession and useConversation come from separate sources and are not guaranteed to publish atomically in one React commit. Components that read both compute purely from their current snapshots and do not treat notification order as business causality.
Definition and View registries
UiConversation.events is the sole registry for event Definitions, and UiConversation.views is the sole registry for target snapshot builders.
The registries reject duplicate keys, preserve registration order, and return idempotent disposers. Existing Conversation bindings rebuild from their current event windows when a roster changes; changes in one synchronous registration turn are coalesced into one microtask rebuild.
A target package extends snapshot and location-data maps through declaration merging, then registers its Definitions, builder, and View. Registrations follow Cordis effect disposal.
ui-conversation does not import concrete target packages.
Conversation React adapter
The React adapter registers each Conversation binding snapshot as the Session standard source conversation, from which the renderer provides useConversation.
The package also owns the shell, input, composer chain, queue UI, drafts, View navigation, and phase composition. The core reads no React Context, Slot props, or component state.
View selection order is a valid persisted selection, registered chat, then no View. An invalid selection does not overwrite the persisted value, and the system does not fall back to the first registered View.
Without ui-chat, the shell can still activate and mount but does not implicitly select Trajectory or another target.
The shell phase is a pure composition of Session lifecycle and Conversation target activity. An active Session or any target reporting visible content produces active; a failed first prompt remains engaging.
Input and composer
The composer chain belongs to ui-conversation; a concrete takeover belongs to its business package. ConversationRoot reads its bound Session's effective request through useSessionStatus and supplies it to chain selectors as ComposerChainProps.pendingInteraction.
A selector is a pure function of owner currency. Its non-null result reaches the selected component as matched. A stable composer entry and the default composer remain mounted together, while the chain selects one effective presentation.
Draft and input state belong to Conversation UI and do not enter the Session snapshot. Queue commands use a Session-scoped service for addressing and do not write queue UI into the Conversation core.
Chat and Trajectory targets
Chat owner
client/ui-chat registers target id chat and owns the Chat snapshot builder, Conversation Node definitions, keyed node renderers, selection, details, statistics, locale, and Tool-inspection collaboration.
It registers the chat target source through ctx.uiSession.provide(). ChatView uses useChat for aggregate order, navigation, and timeline reads; each ChatNodeSeat receives identity-stable Node and Turn-process sources from that snapshot and does not subscribe to the aggregate source.
Only visible non-command Chat Nodes activate Chat. Ordinary command-only history keeps the Hero visible; the /goal command-input Node activates a fresh Conversation.
The historical-image cache moved to ui-conversation (ctx.uiConversation.imageUrl), so Chat and Trajectory share one authorized read and one browser URL per session attachment (Trajectory durable image attachments); draft images remain part of Conversation input.
Trajectory owner
client/ui-trajectory registers trajectory through the same target protocol. It owns event-record projection, timelines, virtual rows, selection, and the inspection view, and exposes useTrajectory through a standard source.
Session lifecycle reads useSession, while Trajectory data reads useTrajectory. Trajectory does not obtain its own data through a Session or Chat snapshot.
Other targets use the same registration flow without modifying the renderer, Session Controller, or ui-session.
Approval and Question
Stable registration
Approval and Question plugin installation separates stable registrations from per-request handling. apply() registers locale data, calls registerPendingInteraction() once for its pending domain, and registers one stable entry in conversation.composer.
The stable Approval entry also declares its detail child Slot. Concurrent requests and Session count do not add composer entries or redeclare Slots, and every registration follows plugin-fiber disposal.
One waterfall request
A Remote Event listener resolves the Session from its own Agent Context. Without a Session scope it calls next() to continue the waterfall; with a Session scope it creates a PendingApproval or PendingQuestion.
The listener publishes the object through the registered domain publication function, waits for user completion, cancellation, or request-signal abortion, and removes the exact object in finally.
One request does not register a Slot, create another lifecycle effect, or mutate the Session snapshot.
Approval exposes allow and reject; Question exposes answer and cancel. User cancellation of a Question returns ASK_CANCELLED; interruption of a pending request by AbortSignal returns UserQuestionError(ASK_ABORTED) rather than leaking the carrier's AbortError or an ordinary Error.
The Gateway requires only that Remote Event arguments and results are valid JSON transport values. It does not duplicate domain validation of Question options.
One pending projection
The Sidebar and composer consume the same effective sessionStatus pending request. Navigation displays approval, plan-review, or question state from its kind; each composer entry selects its own panel by object identity.
The same request identity drives both UI surfaces. A request that replaces another request of the same type uses a new key, so selectors and subscribers observe the identity change.
ui-session implements only cross-domain precedence and does not interpret Approval or Question fields.
UI Renderer and Store
UI Renderer
client/ui-renderer owns the SlotRegistry service and React renderer. It is responsible for:
ctx.slots.register(),inject(),renderSlot(), and declaration lifetimes;- root, session, and session-maybe scope adapters;
- binding standard observable sources to selector hooks;
- Slot outlets, error isolation, root mount, and hydration;
- managing Slot store instance lifetimes by scope key.
The renderer may know generic scope names and binding protocols but does not read domain services. Rendering Session scope without an installed adapter is an assembly error that fails immediately.
Store
client/store is a plain React-free library owning ObservableSnapshot, SnapshotStore, defineStore, createSnapshotStore, and shallowEqual.
ui-slots references the Store contract; ui-renderer manages Store instances and supplies useStore.
Stores hold viewing and interaction state such as drafts, View selection, Chat selection, inspection requests, and panel size. Session, Workspace, Conversation, Remote streams, and connection generations do not enter Stores.
Registration and release order
When one plugin provides both a source and a Slot entry, it registers the source first and the entry second. Reverse Cordis disposal then removes the entry before the source, so a mounted entry never briefly loses a required hook.
Final Session-reference release cleans up UI materialization and scoped Stores through binding.ctx.effect(). Releasing a plugin fiber cleans up sources, listeners, and Slot entries through registration disposers.
Every disposer is idempotent and depends on no implicit callback outside the Cordis lifecycle.
Composition and dependency direction
The application bundle explicitly installs the required Controller, adapter, target, and renderer plugins. Each owner's apply() installs only its own service, listener, and contributions.
Runtime consumption flows as session-controller → ui-session → ui-conversation → target UI, workspace-controller → ui-workspace, and store → ui-slots → ui-renderer; Approval and Question depend only on the pending-registration point exposed by ui-session.
Arrows in this description represent runtime consumption and do not include type-only declaration-merge edges. Controllers do not depend back on UI adapters, the renderer does not depend back on domain packages, and the Conversation core does not depend on a concrete target.
UI components do not receive ctx. Cross-package collaboration uses Cordis services, standard sources, or Slot registrations without introducing an aggregate facade.
Developer guidance
Choose the data owner first
Before adding state, choose its sole owner from its consumption semantics: Host communication, commands, and entity lifecycle belong to an API Controller; data assembled from Session events but independent of a target belongs to the Conversation core; projections serving only one View belong to that target package; drafts, selections, and panel state belong to the UI package that owns the interaction.
The same fact must not be retained simultaneously in a Controller snapshot, Conversation snapshot, and Store. Cross-domain navigation reads sources at decision time. A UI-owned status source may compose independent running, pending-request, and completion-reminder facts, but must preserve domain ownership and object identity rather than duplicate those domains' state.
These are signs of incorrect ownership: a Controller imports React; the renderer branches on business types; a component traverses Session events; a Store holds Session or Workspace entities; changing one target requires changing the Session Controller.
Add Session-scoped data
- Provide a React-free observable source in the domain owner.
- Declaration-merge the standard prop type in the owning UI adapter.
- Declare a fixed roster through
ctx.uiSession.provide()and resolve its source from a Session binding. - Let the Slot component receive the generated hook through
PropsRuntime; do not passctxto a component. - Attach each binding resource's cleanup to
binding.ctx.effect()and leave registration cleanup to the plugin fiber. - Test missing values, duplicate names, roster replacement, Session changes, and binding disposal.
Only open key spaces use keyed hooks. Finite stable sources use ordinary hooks, and immutable identifiers use props. Do not hard-code business names in the renderer to save one registration.
Add a Conversation target
- Extend the Conversation snapshot or location-data map in the target package.
- Register the required event Definitions with
UiConversation.events. - Register the snapshot builder, target id, View, and activity rule with
UiConversation.views. - Expose the target's standard selector hook through
ctx.uiSession.provide(). - Register the renderer, locale, and target-specific Slot entries in the same package.
- Verify that unloading the target rebuilds only the Conversation binding without changing the Session, other targets, or Remote stream.
A target must not use another target's snapshot as its data source. Optional collaboration uses a narrow port or Slot; when a target is absent, the shell remains bootable and does not guess a fallback.
Add a pending-interaction domain
- Define the Pending object and its completion, cancellation, and interruption semantics in the business package.
- Add the object to
SessionPendingInteractionMapthrough declaration merging. - Call
registerPendingInteraction()once inapply()and register one stable composer entry. - Resolve the Session from the Agent Context in the Remote waterfall listener; call
next()when the listener cannot handle the request. - When it can handle the request, create the Pending object, publish it through the publication function, await its result, and remove it in
finally. - Test concurrent keys, precedence, user cancellation, transport abort, plugin disposal, and delegation without a Session.
A request does not register Slots, declare child Slots, mutate the Session snapshot, or create a separate state index. Sidebar and composer read the same effective object from useSessionStatus.
Review checks
- Every new source, registry contribution, listener, and cache has an explicit Cordis-fiber or Session-binding owner.
- Every public hook traces to one React-free source; no selector is forwarded through layers only to pass arguments.
- Every component obtains data and actions from standard props or the owning Slot's inject face.
- Every target has defined behavior when absent, dynamically registered, and unloaded.
- Every cross-layer import advances in the one-way Controller, adapter, renderer, component direction.
- Each error is classified by the earliest owner that can explain its semantics; carrier errors do not leak directly as business errors.
Verification
Tests owned by each layer pin Controller bindings and event sources, UI scopes and pending precedence, incremental Conversation assembly and View fallback, target projections, waterfall results, and renderer scope/Store lifetimes. Application-composition tests cover both the complete roster and startup without a concrete target; component tests do not replace object-layer, replay, and lifecycle tests.
Alternatives considered
- Keep a Runtime facade. One entry point would retain the dependency hub and let new code bypass domain owners, so the system provides neither the facade nor a compatibility export.
- Put all Client state in API Controllers. Protocol objects would then own React, Views, and presentation policy, so Controllers retain only React-free domain state.
- Let Controllers provide React hooks directly. Non-React consumers could not reuse the same objects, and transport and renderer lifetimes would become interdependent.
- Put Conversation in SessionSnapshot. This would expand the Session API and force ordinary Session consumers to understand event folding and target rosters.
- Let Chat and Trajectory replay Session events independently. Ordering, locations, and registry rebuild would be duplicated, so the shared assembly core stays in
ui-conversation. - Extract the Conversation core into another non-UI package. The core and adapter currently evolve together and have no other non-UI package consumer; directory separation within one package keeps the core React-free.
- Combine Workspace and Session into one snapshot. This would create another cross-domain owner, so cross-domain logic remains an immediate decision in
ui-workspace. - Build every standard hook into the renderer. Generic infrastructure would need to know every domain, so standard-source registration keeps the renderer independent from business types.
- Dynamically register a composer entry for every pending request. This would redeclare child Slots and make concurrent requests compete through registration order, so stable entries are separate from request publication.
- Write pending interactions into a Session projection. An unanswered waterfall is not a committed durable Session fact; Remote Event replay restores it after refresh, so it remains in a business UI source.
- Add a dedicated release callback to bindings. This would duplicate the Cordis lifecycle;
binding.ctx.effect()already attaches consumer cleanup to the same owner. - Pass the Session id from SessionProvider through a render function. This would create another data-injection path; ordinary children and the standard
sessionIdprop retain one entry for scoped data. - Keep Store in the renderer. The Store contract does not depend on React and is reused by objects and test infrastructure, so
client/storekeeps the engine separate from rendering lifetimes.
Consequences
Session, Workspace, Conversation, and each concrete target own one authoritative state. Non-React consumers can reuse Controllers and the assembly core directly. A new Conversation target registers its Definition, builder, View, standard source, and Slot entries; a new pending-interaction domain declares its type, registers its domain, and provides one stable composer entry.
The renderer and Session Controller gain no branch for a new business domain, while Session bindings and plugin fibers provide two explicit, composable release paths. The UI can observe independent Session and Conversation source publications, and consumers cannot depend on their notification order.
Composition packages must explicitly load the required adapter and target plugins. The shell remains operational without a concrete target but neither creates nor guesses that target's View. More packages and explicit registrations add assembly work, while dependency direction, test scope, and failure ownership become locally identifiable.
中文
问题
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、通用渲染机制和具体视图拆成单向依赖,同时保持既有应用行为。
决定
Client 采用“Controller 与领域对象 → UI adapter → renderer → Slot component”的分层。Controller 和领域对象发布不依赖 React 的 observable source;所属 ui-* package 声明标准 props 并注册 source;ui-renderer 在 Slot binding 点生成 selector hook;组件只从 Slot props 读取数据与操作。
[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 包,也没有替代的中央 facade。Session 历史与事件传输定义历史连续性。Client 会话引用拥有引用获取、精确代际生命周期、主区域所有权和统一 UI 状态;本篇拥有分层与数据源注册规则。
分层原则
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 | 已绑定会话快照 |
useProjection | client/ui-session | 已绑定会话的键控投影 |
useSessionStatus | client/ui-session | 运行状态、有效待处理请求和未读完成提醒 |
useSessionRetainInfo | client/ui-session | 控制器的只读引用来源计数 |
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 | 会话对象、目录、引用、来源计数、命令、投影、事件窗口与 Agent Context | 导航、完成提醒、Conversation target、React、Slot、Workspace |
api/workspace-controller/client | Workspace 对象、顺序、归档、命令和 snapshot | React、Session 导航策略、目录 UI |
client/ui-session | 显式会话作用域、标准数据源、SessionProvider 与统一 UI 状态 | 会话传输、引用所有权、Conversation 组装、Approval/Question 结果 |
client/ui-workspace | Workspace 钩子、浏览器 UI、主区域引用与跨控制器导航策略 | Workspace 传输、会话数据副本 |
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、历史图片 cache |
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:
[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 控制器,再由 ui-workspace 通过 useWorkspaces 提供。ui-workspace 为跨领域导航读取显式目标并持有主区域引用,不把它变为默认业务 Context。
Approval 与 Question 通过 ctx.remote.$on 从 Host waterfall 到达各自的 UI owner。各 owner 发布 Pending 对象;ui-session.sessionStatus 向 Workspace 标识和 Conversation composer 选择提供同一个有效对象。
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 对外提供三个互不替代的读取面:
- 会话目录与本地所有权数据源,由
useSessions和只读引用元数据消费方使用。 - 每个 Session 的逻辑 binding,包含
sessionId、SessionSnapshotsource、commands 与 projection sources。 - 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 变更描述。Append 以常数时间连接不可变片段;需要完整 entries 数组的消费者才为该 snapshot 物化并缓存数组。
首次打开、重连、gap repair 和无法证明连续性的更新发布 replace;历史分页发布 prepend;连续 live event 发布 append。Conversation core 依据 revision 与 change 选择增量更新或完整 rebuild。
MutableSessionEventSource 是 Session Controller 内部写端,消费者只依赖只读的 SessionEventSource。
Session binding 生命周期
每个活跃会话 generation 持有 Cordis Context 与 Fiber。控制器在获取引用时创建绑定,并在最后一份引用释放或根销毁时结束该绑定。
依赖会话的对象通过 binding.ctx.effect() 注册清理。generation 结束会清理 Conversation 绑定、UI 物化结果和作用域 Slot 存储,无需单独的 onBindingRelease 或 onRelease 回调协议。
这种清理方式不要求 Session Controller 了解上层消费者名册。
UI Session
服务职责
client/ui-session 是 Session Controller 与 React/Slot 系统之间唯一的 Session adapter。它提供 ctx.uiSession,并负责:
- 观察会话目录、本地引用元数据和显式提供的绑定;
- 安装 session 与 session-maybe scope adapter;
- 提供
SessionProvider的呈现语义; - 内建 session snapshot、projection 和 sessionId source;
- 接收其他领域 package 的 Session-scoped source contribution;
- 在
sessionStatus中组合领域持有的待处理交互、运行事实和完成提醒。
它不拥有 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 自身也走相同 materialization,renderer 不为 Session 名字写特殊分支。
Scope binding
session 与 session-maybe 使用同一个 adapter,但绑定语义不同:
- 严格会话作用域在没有显式提供的绑定时拒绝渲染;
- session-maybe 使用稳定 absent binding,保持 hook 调用顺序;
- 精确 Context generation 改变时重新挂载已绑定子树,同一 id 的替代 generation 也如此;
- 未绑定的 session-maybe 条目无需重新挂载即可接纳首个绑定;root 条目没有会话绑定。
每个 UI 物化结果借用控制器绑定的 Context。ui-session 通过 binding.ctx.effect() 移除该 generation 的缓存项并发布空值,不会 retain 会话。
Contribution roster 变化会重建已 materialize 的 binding 并发布新的 source 集合。同一 binding 生命周期内,source identity 保持稳定,以满足 useSyncExternalStore 的缓存要求。
SessionProvider
SessionProvider 是 PropsRenderSlots 根据 session-scoped child 声明派生的标准席,不是业务 component 直接 import 的 React Context。
它接收普通 ReactNode children 和必填的 session={reference | undefined}。Provider 借用调用方持有的引用,不获取或释放它;调用方直接包住 renderSlot('details', {})。
会话身份通过显式作用域绑定和标准 sessionId prop 到达组件。空 Provider 保持未绑定,嵌套 Provider 和 root 条目都不会回退到主区域会话。
Pending interaction
SessionPendingInteractionMap 由业务 package declaration merge 扩展。每个 pending object 至少携带稳定 key、领域 kind 和 sessionId;ui-session 不 import Approval 或 Question 的具体类型。
业务 plugin 在 apply() 中调用 registerPendingInteraction(precedence),为自己的 pending domain 建立稳定注册。该调用返回逐请求 publication function;publication function 同时发布精确对象及其 waterfall 委托回调,并返回移除该对象的幂等 disposer。Plugin teardown 会先移除所有已发布对象,再调用并等待其委托回调,避免 Client 回答者卸载后 Host 请求继续悬挂。
相同 key 的并发对象被拒绝,替换请求必须使用新 key。同一 Session 可以同时存在多个领域或多个请求。
ui-session 使用各 domain 的 precedence 选出每个 Session 当前生效的对象。较高 precedence 胜出,相同 precedence 下后遍历到的有效对象胜出。
待处理聚合是 ui-session 的私有实现;其有效请求原样出现在 sessionStatus.getSnapshot().get(id)?.pendingInteraction 中。useSessionStatus 是公开 UI 读取接口。
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。
启动恢复、空白会话复用、新会话导航、并发创建合并与归档后的导航属于 UI 策略。ui-workspace 可以读取两个控制器,但把主目标和引用保存在自己的导航 owner 中,不向控制器快照写入 UI 选择。
目录 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 重建;同一同步注册轮次中的变化会合并为一次 microtask 重建。
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,具体接管属于业务包。ConversationRoot 通过 useSessionStatus 读取已绑定会话的有效请求,并作为 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 协作。
它通过 ctx.uiSession.provide() 注册 chat target source。ChatView 使用 useChat 读取聚合 order、navigation 与 timeline;每个 ChatNodeSeat 从该 snapshot 接收身份稳定的 Node 与 Turn-process source,不订阅聚合 source。
Chat activity 只由可见且非 command 的 Chat Node 激活。普通 command-only history 保持 Hero,/goal 的 command-input Node 激活 fresh Conversation。
历史图片 cache 已移入 ui-conversation(ctx.uiConversation.imageUrl),Chat 与 Trajectory 对同一会话附件共享一次授权读取和一个浏览器 URL(Trajectory 持久化图片附件);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 与 reject,Question 暴露 answer 与 cancel。用户主动取消 Question 返回 ASK_CANCELLED;等待中的请求被 AbortSignal 中止时返回 UserQuestionError(ASK_ABORTED),不泄漏载体的 AbortError 或普通 Error。
Gateway 只要求 Remote Event 参数和结果是合法 JSON 传输值,不复制 Question 选项的领域校验。
单一 pending 投影
Sidebar 与 composer 消费 sessionStatus 中同一个有效待处理请求。导航根据其 kind 显示审批、计划审阅或问题状态;每个 composer 条目按对象身份选择自己的面板。
同一请求 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。
最后一份会话引用释放后,通过 binding.ctx.effect() 清理 UI 物化结果和作用域存储。插件 fiber 释放通过注册 disposer 清理数据源、监听器和 Slot 条目。
所有 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 adapter,renderer 不反向依赖领域 package,Conversation 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。
同一个事实不能同时保存在控制器快照、Conversation 快照和存储中。跨领域导航在决策时读取数据源。UI 持有的状态数据源可以组合独立的运行、待处理请求和完成提醒事实,但必须保留领域归属与对象身份,不能复制这些领域的状态。
以下信号表示 owner 选择错误:Controller 开始 import React;renderer 出现业务类型分支;组件遍历 Session events;Store 保存 Session 或 Workspace 实体;一个 target 的变化要求修改 Session Controller。
新增 Session-scoped 数据
- 在领域 owner 中提供 React-free observable source。
- 在所属 UI adapter 中 declaration-merge 标准 prop 类型。
- 通过
ctx.uiSession.provide()声明固定 roster,并从 Session binding 解析 source。 - 让 Slot component 从
PropsRuntime获得生成的 hook,不向组件传ctx。 - 把每个 binding 的资源清理挂到
binding.ctx.effect(),把 registration 清理留给 plugin fiber。 - 测试缺失值、重复名字、roster 替换、Session 切换和 binding disposal。
只有开放 key 空间使用 keyed hook;有限且稳定的 source 使用普通 hook;不会变化的标识使用 prop。不得为了减少一次注册而把业务名称硬编码进 renderer。
新增 Conversation target
- 在 target package 中扩展 Conversation snapshot 或 location data map。
- 向
UiConversation.events注册所需 event Definition。 - 向
UiConversation.views注册 snapshot builder、target id、View 与 activity 规则。 - 通过
ctx.uiSession.provide()暴露该 target 的标准 selector hook。 - 在同一 package 中注册 renderer、locale 和 target-specific Slot entry。
- 验证 target 卸载只重建 Conversation binding,不改变 Session、其他 target 或 Remote stream。
Target 不得读取另一个 target 的 snapshot 作为自己的数据源。可选协作通过窄 port 或 Slot 完成;缺失 target 时,shell 必须保持可启动且不得猜测 fallback。
新增 pending-interaction 业务
- 业务 package 定义 Pending 对象及其完成、取消和中止语义。
- 通过 declaration merge 把对象加入
SessionPendingInteractionMap。 - 在
apply()中调用registerPendingInteraction()一次,并注册唯一稳定的 composer entry。 - Remote waterfall listener 从 Agent Context 解析 Session;无法处理时调用
next()。 - 可处理时创建 Pending 对象,使用 publication function 发布,等待结果,并在
finally中移除。 - 测试并发 key、precedence、用户取消、transport abort、plugin disposal 和无 Session delegation。
请求不注册 Slot、不声明子 Slot、不修改会话快照,也不创建独立状态索引。Sidebar 与 composer 从 useSessionStatus 读取同一个有效对象。
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;系统因此不保留 facade 或兼容出口。
- 把所有 Client 状态放进 API Controller。 这会让协议对象承担 React、View 和 presentation policy;Controller 因而只保留无 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 与标准
sessionIdprop 保持 scope 数据只有一个入口。 - 把 Store 留在 renderer。 Store contract 不依赖 React,并被对象与测试基础设施复用;独立
client/store保持 engine 与渲染生命周期分离。
后果
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 均可局部确定。