diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml index d19cd86c7d..b76951347b 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md -2026-08-09-client-conversation-node-assembly.md: 9952f8fac0f2a3b0fe72cc97ae9a8fb4f96ace08 -2026-08-09-client-conversation-node-assembly.zh.md: f27a82459cdacdb4a2da27cf12b30c93f2dfdf64 +2026-08-09-client-conversation-node-assembly.md: 16a39539064644e5467f701789a7e2ef1f7ff172 +2026-08-09-client-conversation-node-assembly.zh.md: 0e0fbdf8f3320393022528e6e3fe2cf0d492a1d3 diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md index 9952f8fac0..16a3953906 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md @@ -394,6 +394,8 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping- A new business node can register its matcher, State transitions, optional Location data, final target Node, and renderer locally without changing Session's business switch. `ChatNodeDataMap` and the Location data maps let a business package merge strongly typed data into the contract; every related Event must still expose a stable ID derivable from that Event alone. +Host business packages declaration-merge their durable Event members into `@deepseek-ai/dsh-session/types`, while Client Definitions type-only import the corresponding business package `/types` subpaths. Augmenting the declaring interface rather than a re-export barrel gives the independent Host and Client TypeScript programs the same Event narrowing without pulling Host runtime into the Client graph. + Initial tail, older prepend, and live append share one set of Context invariants. Missing starts, Reader window gaps, unknown Locations, and high-frequency deltas are explicit engine states and require no direction-specific business cache. Append does not scan historical Contexts; prepend replays only Contexts whose Matches, Locations, or Reader answers actually changed. A structural Chat change may still recompute visible order and indexes, but does not rerun unrelated business folds or replace unchanged Node identity. diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md index f27a82459c..0e0fbdf8f3 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md @@ -394,6 +394,8 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏 新增业务节点可以局部注册自己的 matcher、State 转换、可选 Location data、最终 target Node 和 renderer,不再修改 Session 的业务 switch。`ChatNodeDataMap` 和 Location data maps 允许业务 package 通过 declaration merging 合入强类型 data;所有相关 Event 仍须暴露可单 Event 推导的稳定 ID。 +Host 业务 package 把自己的持久 Event 成员 declaration-merge 到 `@deepseek-ai/dsh-session/types`,Client Definition 则通过对应业务 package 的 `/types` 子路径进行 type-only import。增强实际声明接口而不是重导出 barrel,使 Host 和 Client 的独立 TypeScript Program 都能获得相同的 Event narrowing,同时不把 Host runtime 带入 Client 图。 + 初始尾页、older prepend 和 live append 共享一套 Context 不变量。缺 start、Reader window gap、Location unknown 以及高频 delta 都是引擎明确表达的状态,不需要业务另建方向相关 cache。 Append 不扫描历史 Context;prepend 只 replay Match、Location 或 Reader 答案真正受影响的 Context。Chat 结构变化仍可能重算 visible order 和索引,但不会重跑无关业务 fold 或替换未变化 Node identity。 diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml index fd8dbdd1b5..532750f069 100644 --- a/docs/subsystems/compaction.i18n.yaml +++ b/docs/subsystems/compaction.i18n.yaml @@ -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 docs/subsystems/compaction.md -compaction.md: 1aca48d677a83a488d9590edad3f8a68ca662b70 -compaction.zh.md: e2cfabc0891126d9f2165e47dffcf485f4a8a339 +compaction.md: 11ee6eae797db1c57bf2ffbb90f1f37f47b8400c +compaction.zh.md: a560e47dbb7aa92f0757179534803bbcaaebc6ba diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md index 1aca48d677..11ee6eae79 100644 --- a/docs/subsystems/compaction.md +++ b/docs/subsystems/compaction.md @@ -20,7 +20,7 @@ The lock brackets the **whole** operation: `compact/start` is appended first, th The markers are lock time points, not an exclusive container. An unrelated idle injection can appear between a standalone manual start and end while summarization is pending. The manual path revalidates only its selected positional span, so that injected context survives after the replacement checkpoint. A live unmatched start blocks every entry point; an unmatched start before a newer `session/end-seed` is stale evidence from a prior lifecycle and is ignored. -These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes. +These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes. ## `CompactionResult` diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md index e2cfabc089..a560e47dbb 100644 --- a/docs/subsystems/compaction.zh.md +++ b/docs/subsystems/compaction.zh.md @@ -20,7 +20,7 @@ 这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。 -这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。 +这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。 ## `CompactionResult` diff --git a/packages/client/ui-conversation/src/client/chat/TurnTailNodeView.tsx b/packages/client/ui-conversation/src/client/chat/TurnTailNodeView.tsx index bf48fc75c6..444389e6ea 100644 --- a/packages/client/ui-conversation/src/client/chat/TurnTailNodeView.tsx +++ b/packages/client/ui-conversation/src/client/chat/TurnTailNodeView.tsx @@ -9,9 +9,11 @@ type TurnTailNodeViewProps = ChatNodeViewProps<'turn-tail'> & PropsRenderSlots<' /** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */ export const TurnTailNodeView = memo(function TurnTailNodeView({ - node, openFile, forkAt, renderSlotChain, t, + node, openFile, forkAt, renderSlotChain, t, useSession, }: TurnTailNodeViewProps) { const data = node.data + const hasLaterChatNode = useSession(snapshot => + snapshot.chat.locations.getTurn(data.turn).at(-1) !== node.key) const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined @@ -34,7 +36,7 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({ tokensPerSecond={data.tokensPerSecond} clock="end" onBranch={() => { forkAt(closing.finalNode.seq) }} - branchUnavailable={data.branchUnavailable} + branchUnavailable={data.branchUnavailable || hasLaterChatNode} className={css.actions} t={t} /> diff --git a/packages/client/ui-conversation/src/client/contract/chat-nodes.ts b/packages/client/ui-conversation/src/client/contract/chat-nodes.ts index 787f391006..c057d15d1b 100644 --- a/packages/client/ui-conversation/src/client/contract/chat-nodes.ts +++ b/packages/client/ui-conversation/src/client/contract/chat-nodes.ts @@ -57,7 +57,7 @@ export interface TurnTailChatData { readonly time: number /** Last finalized content-bearing Assistant in this Turn. */ readonly closing: FinalAssistantChatData | null - /** Whether later Assistant/Step material makes the closing seq non-tail. */ + /** Whether non-rendered later evidence makes the closing seq non-tail. */ readonly branchUnavailable: boolean readonly ttftMs?: number readonly tokensPerSecond?: number diff --git a/packages/client/ui-conversation/tests/chat-view.spec.tsx b/packages/client/ui-conversation/tests/chat-view.spec.tsx index ca6223826e..6b2072ddfb 100644 --- a/packages/client/ui-conversation/tests/chat-view.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-view.spec.tsx @@ -181,14 +181,12 @@ function makeHarness(init?: Partial) { if (key !== 'conversation.chat.node') return opts?.fallback ?? null const nodeOwner = owner as RoutedChatNodeOwner const nodeKey = opts?.hookContext as string | undefined - const useTurnData = ((dataKey: string) => { - return props.useSession((snapshot) => { - const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location - return location?.kind === 'turn' || location?.kind === 'step' - ? location.turn.data.get(dataKey as never) - : undefined - }) - }) as UseChatNodeTurnData + const useTurnData: UseChatNodeTurnData = dataKey => props.useSession((snapshot) => { + const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location + return location?.kind === 'turn' || location?.kind === 'step' + ? location.turn.data.get(dataKey) + : undefined + }) const nodeProps = (): ChatNodeViewProps => ( { ...props, ...nodeOwner, useTurnData } as unknown as ChatNodeViewProps ) @@ -722,6 +720,28 @@ describe('ChatView', () => { expect(h.forkAt.mock.calls).toEqual([[2]]) }) + it('disables fork when the indexed Turn has a later steering Node', () => { + const base = chatSnapshotFixture({ + nodes: [user(1, 'question'), assistant(2, 'answer')], + turnEnds: new Map([[1, 4]]), + }) + const chat = { + ...base, + locations: { + getTurn: (turn: number) => turn === 1 + ? [...base.locations.getTurn(turn), 'fixture:steering:later'] + : base.locations.getTurn(turn), + getStep: (turn: number, step: number) => base.locations.getStep(turn, step), + }, + } + const h = makeHarness({ chat }) + const view = render() + const branch = view.getByRole('button', { name: '在新对话中分支' }) + expect(branch.getAttribute('aria-disabled')).toBe('true') + fireEvent.click(branch) + expect(h.forkAt).not.toHaveBeenCalled() + }) + it('keeps final content actions but disables branch when Tool and interrupted Think follow it', () => { const interruptedThink: AssistantMessageNode = { kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2, diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts index 0227b34261..9151f88869 100644 --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts @@ -36,7 +36,9 @@ interface DeliverablesState extends DeliverablesTurnData { * Paths a call view reports having created or changed, by render intent rather * than tool name: a diff card, or a generic card whose kind is `edit` (the * shape `str_replace_editor`'s insert presents). Every other card produces - * nothing to open — a read looked, a delete removed, a terminal ran. + * nothing to open — a read looked, a delete removed, a terminal ran. Only + * root call views enter this Turn accumulator; nested Code Mode dispatches + * preserve the pre-assembly behavior and do not contribute independently. */ function producedPaths(view: ToolResultNode['callView']): readonly string[] { if (view === null) return [] diff --git a/vitest.config.ts b/vitest.config.ts index e07ae43ec3..33512f4898 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -211,6 +211,7 @@ export default defineConfig({ 'packages/client/ui-workspace/src/client/index.ts', 'packages/client/test-runtime/src/translate.ts', 'packages/client/ui-primitives/src/JsonTree.tsx', + 'packages/client/ui-deliverables/src/client/turn-deliverables.ts', // Typert generator: correctness is pinned by its fixture suites and // the byte-for-byte catalog reproduction test; per-file coverage // would put whole-workspace compiler analysis under v8