fix(client): address conversation assembly review feedback

This commit is contained in:
imccyu
2026-08-09 17:28:42 +08:00
parent 8723a0e15f
commit 990f700d3c
20 changed files with 73 additions and 49 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
README.md: b2bb06e50ecd791d74cb609404dd219e5a21913e
README.zh.md: 72cd99ac765875c828cc9163d978e0b8fb7f44e4
README.md: 46a383f22032c8da7e4bb9b5fb445980661c670f
README.zh.md: 73578a8f249472abfdd7c539e3eaee6de767ad60

View File

@@ -38,7 +38,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and
Each `Session` gives its contiguous event window to a `ConversationNodeAssembler`. Plugins register business Definitions that map one event to a stable `{kind, id}`, create State at the unique start event, fold correlated updates, and build final nodes for registered view targets. The assembler owns the Context index, read-only predecessor lookup, and a reference-stable Turn/Step Location index. A live append evaluates each Definition once and updates only the matched Context; loading an older page preserves existing Context and node identities, matches only the newly prepended events, and replays Contexts whose predecessor or Location facts changed. Full replacement is reserved for open, resync, and gap repair.
`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. `ConversationSnapshot.nodes`, `partial`, and `runningCalls` are compatibility slices derived from the same materialized Chat nodes for consumers that have not moved to `ConversationSnapshot.chat`; Session does not run a second business fold.
`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. StatsLine reads `ConversationSnapshot.chat.legacy.nodes`, while Session mirrors that legacy slice into the top-level `nodes`, `partial`, and `runningCalls` public compatibility fields without running a second business fold. Trajectory consumes neither compatibility surface; its activated `session-history` inspection keeps an independent fold until it gains its own registered target.
The Chat builder keeps one mutable keyed store per Session. Content updates notify only the affected node key, structural changes rebuild order and Location membership, and a prepend adds rows without replacing existing keyed values. Assistant chunks update Definition State for every event but request at most one materialization per animation frame; final messages and Turn/Step closure publish immediately. See the [client Tool presentation decision](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md).

View File

@@ -38,7 +38,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸
每个 `Session` 都把连续事件窗口交给 `ConversationNodeAssembler`。插件注册业务 Definition把单个事件映射为稳定的 `{kind, id}`,在唯一 start 事件处创建 State折叠有关联的 update再为已注册的视图目标构造最终节点。Assembler 负责 Context 索引、只读前序 Context 查询,以及引用稳定的 Turn/Step Location 索引。实时 append 只对每个 Definition 求值一次,并且只更新命中的 Context加载更早分页时保留已有 Context 与节点身份,只匹配新 prepend 的事件,并重放前序依赖或 Location 事实发生变化的 Context。完整替换仅用于 open、resync 和 gap repair。
`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chatcompaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。`ConversationSnapshot.nodes``partial``runningCalls` 是从同一批已物化 Chat 节点派生的兼容切片,供尚未迁移到 `ConversationSnapshot.chat` 的消费者使用Session 不再运行第二套业务 fold。
`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chatcompaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。StatsLine 读取 `ConversationSnapshot.chat.legacy.nodes`Session 则把该 legacy slice 镜像到顶层 `nodes``partial``runningCalls` 公共兼容字段,无须运行第二套业务 fold。Trajectory 不消费这两种兼容表面;在它获得独立注册 target 之前,已激活的 `session-history` inspection 继续维护独立 fold。
Chat builder 为每个 Session 保留一个 mutable keyed store。内容更新只通知受影响的 node key结构变化才重建顺序和 Location 成员关系prepend 只增加行,不替换既有 keyed value。每个 Assistant chunk 都会更新 Definition State但最多每个 animation frame 请求一次物化final message 与 Turn/Step 关闭会立即发布。参见 [Client Tool 展示所有权决策](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md)。

View File

@@ -743,6 +743,8 @@ export class ConversationNodeAssembler {
context.locationData[scope] = data
if (data !== null) entries.push({ owner: context.key, data })
}
// Turn publishers may read Step data from this same flush, so each phase
// installs the cumulative replacement before the next phase builds.
this.locationIndex.replaceData(entries)
}
}

View File

@@ -1,7 +1,9 @@
// ConversationSnapshot / ConversationNode: the only data shape the logic layer feeds the UI.
// Immutability contract: every change swaps the top-level object; unchanged
// substructures keep their references (the React.memo premise). callId/approvalId stay plain
// string here (narrow to real brands when convenient).
// Publication contract: every change swaps the top-level object; unchanged
// substructures keep their references (the React.memo premise). Chat node and
// Location stores are stable live readers, so old snapshots are not time-point
// views. callId/approvalId stay plain string here (narrow to real brands when
// convenient).
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
@@ -336,7 +338,10 @@ export interface PromptError {
error: RpcError
}
/** Stable per-key reader for final Chat view Nodes. */
/**
* Stable live per-key reader. An old ChatSnapshot observes later flushes
* through this store.
*/
export interface ChatNodeStore {
/** @param key - stable Conversation Context key. @returns current Node, when visible or hidden. */
get(key: string): ChatConversationViewNode | undefined
@@ -344,7 +349,10 @@ export interface ChatNodeStore {
values(): readonly ChatConversationViewNode[]
}
/** Stable per-Location membership index for turn-local and step-local consumers. */
/**
* Stable live Location index. An old ChatSnapshot observes later membership
* changes through this index.
*/
export interface ChatLocationNodeIndex {
/** @param turn - owning turn. @returns ordered Chat Node keys in the turn. */
getTurn(turn: number): readonly string[]
@@ -352,7 +360,7 @@ export interface ChatLocationNodeIndex {
getStep(turn: number, step: number): readonly string[]
}
/** Temporary projection consumed by Trajectory and unmigrated readers. */
/** Compatibility projection backing StatsLine and the legacy top-level snapshot fields. */
export interface LegacyConversationSlice {
readonly nodes: readonly ConversationNode[]
readonly turnTimings: ReadonlyMap<number, { readonly startTime: number; readonly endTime?: number }>
@@ -361,7 +369,7 @@ export interface LegacyConversationSlice {
readonly runningCalls: readonly RunningToolCall[]
}
/** Incremental Chat target snapshot: stable keyed stores plus structural order. */
/** Incremental Chat publication with immutable order and stable live keyed readers. */
export interface ChatSnapshot {
readonly order: readonly string[]
readonly nodes: ChatNodeStore
@@ -399,7 +407,7 @@ export interface ConversationSnapshot {
sessionId: SessionId
/** Final Chat target assembled from independently registered business Definitions. */
chat: ChatSnapshot
/** Legacy Trajectory slice derived from the registered Chat Definitions. */
/** Legacy top-level compatibility field mirrored from the registered Chat Definitions. */
nodes: readonly ConversationNode[]
/** Exact in-window `turn/start` time and optional matching `turn/end` time. */
turnTimings: ReadonlyMap<number, { readonly startTime: number; readonly endTime?: number }>