Bladeren bron

fix(chat): revert historical timing recovery from #3880

_Kerman 3 weken geleden
bovenliggende
commit
251ef01acf

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.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-09-06-embedded-stream-record-readers.md
-2026-09-06-embedded-stream-record-readers.md: 76e109de577d070093bb7123aa50d9a4ae7fcbe5
-2026-09-06-embedded-stream-record-readers.zh.md: 33f4b31117197c47e7f2ff1637bc13fa2cbf1a8c
+2026-09-06-embedded-stream-record-readers.md: 49bf354fbe99c7935872f86ab37250c30154dc03
+2026-09-06-embedded-stream-record-readers.zh.md: b8379fad89b10adfa2e6ac9d3726c0a8bc7d33f8

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md

@@ -20,7 +20,7 @@ After v2 embedded streams settlement widened with the message content and Chat a
 - Run readers: `runFirstTokenTime` and `runFirstVisibleTime` reconstruct the first qualifying member's time from `time0` and the `dt` gaps and stop scanning there; a name-bearing Tool-call run yields `time0` without reading a fragment.
 - Stream readers: `assistantStreamFirstTokenTime`, `assistantStreamHasVisibleContent`, `assistantStreamHasVisibleText`, `lastAssistantStreamChunk(stream, type)` (backward scan), `assistantStreamChunks(stream, type)`, `joinAssistantStreamText`, and `assembleAssistantStream`, which feeds a `BlockAssembler` one joined delta per run (assembly only concatenates, so blocks, usage, finish, and replay state equal the per-member result). `RawStreamChunkType` excludes the delta types, so a raw-chunk lookup can never silently skip packed members.
 
-Session Stats, Chat, and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Chat and Trajectory settle content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
+Session Stats and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Trajectory settles content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. Chat retains its [live-only timing policy](../bug-fix/2026-09-14-chat-presentation-defaults.md). The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
 
 `expandAssistantStream` keeps its strict validation and its remaining callers, which need every member or validate the stream at a durable boundary: Session restore validation, the v1-to-v2 migration validator and publication Worker replay, the reconnect baseline, and test support.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md

@@ -20,7 +20,7 @@ Session 格式 v2 将每次模型尝试的紧凑流(`AssistantStreamRecord[]`
 - Run 读取器:`runFirstTokenTime` 与 `runFirstVisibleTime` 从 `time0` 与 `dt` 间隔重建首个合格成员的时间并停止扫描;带名称的 Tool-call run 直接产出 `time0`,不读片段。
 - 流读取器:`assistantStreamFirstTokenTime`、`assistantStreamHasVisibleContent`、`assistantStreamHasVisibleText`、`lastAssistantStreamChunk(stream, type)`(逆向扫描)、`assistantStreamChunks(stream, type)`、`joinAssistantStreamText` 与 `assembleAssistantStream`(每个 run 向 `BlockAssembler` 喂入一个拼接后的 delta;组装只做拼接,因此 blocks、usage、finish 与 replay state 与逐成员结果一致)。`RawStreamChunkType` 排除 delta 类型,因此原始 chunk 查找不可能静默跳过打包成员。
 
-Session Stats、Chat 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Chat 与 Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
+Session Stats 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。Chat 保留[仅使用实时计时的策略](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
 
 `expandAssistantStream` 保留其严格校验与其余调用方(需要每个成员或在持久边界校验流):Session 恢复校验、v1-to-v2 迁移校验器与发布 Worker 重放、重连基线、测试支撑。
 

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/bug-fix/2026-09-14-chat-presentation-defaults.md
+2026-09-14-chat-presentation-defaults.md: 6a00b1d541d3f2bae28fd71bd3706a95c402ecee
+2026-09-14-chat-presentation-defaults.zh.md: 83aab3bee7ec958369141e1ae67ec25b467982ec

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.md

@@ -0,0 +1,25 @@
+# Agent Note: Keep Chat presentation independent of Trajectory inspection
+
+Status: implemented
+
+English | [中文](2026-09-14-chat-presentation-defaults.zh.md)
+
+## Problem
+
+Trajectory inspection benefits from exposing complete recorded reasoning. Applying that default to Chat expands the live transcript during reasoning and changes its height when an answer or Tool call arrives. The Trajectory inspection change also added historical first-token recovery to Chat without a separate Chat behavior decision.
+
+## Decision
+
+[Chat](../../../../packages/client/ui-chat/README.md#turn-process-folding) starts each reasoning row collapsed and retains the reader's manual disclosure choice through subsequent output and settlement. Its reply timing uses observed live chunks; cold history does not recover first-token time from embedded streams. Turn-level process folding remains independently owned.
+
+[Trajectory inspection](../feature/2026-09-09-ptc-trajectory-code-inspection.md) keeps its expanded reasoning default, recorded timing, JSON controls, and PTC code inspector. The [compact stream readers](../architecture/2026-09-06-embedded-stream-record-readers.md) remain available to Trajectory and other consumers. These decisions partially supersede the Chat presentation additions while preserving both notes' independent rationale.
+
+## Alternatives considered
+
+**Keep Chat's automatic expansion and historical timing recovery.** These change Chat behavior beyond the requested Trajectory inspection work. Reintroducing either requires a separate Chat product decision and its own verification.
+
+**Revert the entire inspection change.** That would remove the requested Trajectory behavior along with the unintended Chat changes.
+
+## Consequences
+
+Chat reasoning requires a click to inspect in full, and reopened replies can lack first-token and decoding metrics. Component tests cover collapsed streaming and reasoning-only replies, manual disclosure across answer and Tool-call arrival, and cold-history timing. The recorded lifecycle and turn-tail browser scenarios verify the assembled Chat behavior; Trajectory tests retain its separate defaults and timing.

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: 保持 Chat 展示与 Trajectory 查看独立
+
+Status: implemented
+
+[English](2026-09-14-chat-presentation-defaults.md) | 中文
+
+## 问题
+
+Trajectory 查看适合直接展示完整的已记录思考。将该默认行为应用到 Chat 会在思考期间展开实时对话,并在回答或工具调用到来时改变其高度。Trajectory 查看改动还为 Chat 增加了历史首 token 计时恢复,却没有独立的 Chat 行为决策。
+
+## 决策
+
+[Chat](../../../../packages/client/ui-chat/README.zh.md#turn-process-folding) 的每个思考行初始都折叠,后续输出和结算保留用户手动选择的展开状态。回复计时使用观测到的实时 chunk;冷加载历史不从内嵌流中恢复首 token 时间。轮次级过程折叠仍独立维护。
+
+[Trajectory 查看](../feature/2026-09-09-ptc-trajectory-code-inspection.zh.md) 保留思考默认展开、已记录计时、JSON 控件和 PTC 代码查看器。[紧凑流读取器](../architecture/2026-09-06-embedded-stream-record-readers.zh.md) 仍供 Trajectory 和其他消费方使用。这些决策部分取代了 Chat 展示增量,同时保留两份记录各自独立的理由。
+
+## 考虑过的替代方案
+
+**保留 Chat 自动展开和历史计时恢复。** 这些改变了所请求的 Trajectory 查看工作之外的 Chat 行为。重新引入任一行为都需要独立的 Chat 产品决策及相应验证。
+
+**撤回整个查看改动。** 这会在撤回意外 Chat 改动的同时移除所请求的 Trajectory 行为。
+
+## 影响
+
+Chat 思考需要点击才能查看全文,重新打开的回复可能缺少首 token 和解码指标。组件测试覆盖流式与仅思考回复的默认折叠、回答和工具调用到来时的手动展开状态,以及冷加载历史计时。已记录的生命周期与轮尾浏览器场景验证组装后的 Chat 行为;Trajectory 测试保留其独立默认值和计时。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.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/feature/2026-09-09-ptc-trajectory-code-inspection.md
-2026-09-09-ptc-trajectory-code-inspection.md: 477d199f519b5d515e5d58430bd902d9d209e46b
-2026-09-09-ptc-trajectory-code-inspection.zh.md: f305569176214c63ac549b0ec5103289e3e57a88
+2026-09-09-ptc-trajectory-code-inspection.md: 84fbc1334683fb70efc28c5cce312c0c75a81b2a
+2026-09-09-ptc-trajectory-code-inspection.zh.md: a0e82d6b4ac2f135d1fb685647e9b1a82cd8ce73

+ 1 - 1
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md

@@ -26,4 +26,4 @@ The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlemen
 
 ## Consequences
 
-Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover body arrival, manual disclosure, and switching records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.
+Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover [Chat disclosure defaults](../bug-fix/2026-09-14-chat-presentation-defaults.md), manual disclosure, and switching Trajectory records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.

+ 1 - 1
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md

@@ -26,4 +26,4 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
 
 ## 后果
 
-读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖正文到达、手动展开折叠及记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。
+读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖 [Chat 展开默认值](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)、手动展开折叠及 Trajectory 记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。

+ 2 - 2
apps/web/tests/turn-tail-actions.e2e.ts

@@ -201,8 +201,8 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
     await timeTrigger.click()
     const timeDialog = page.getByRole('dialog', { name: 'Turn time and speed' })
     expect(await timeDialog.count()).toBe(1)
-    expect(await timeDialog.getByText(/tok\/s/).count()).toBeGreaterThan(0)
-    expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(1)
+    expect(await timeDialog.getByText(/tok\/s/).count()).toBe(0)
+    expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(0)
     await page.keyboard.press('Escape')
     await trigger.click()
 

+ 2 - 2
packages/client/ui-chat/README.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 packages/client/ui-chat/README.md
-README.md: ddc3a5da9440c107a57a12bc9894b66f7dd3f17b
-README.zh.md: 695bd9bca836e2bc81c1b3dd48b730fbc824f402
+README.md: 9a351736c5377800235b28a4fa274ebb5dc63b93
+README.zh.md: 5a7df32488a06dd09c47a453f0d99f3520d04fb7

+ 0 - 2
packages/client/ui-chat/README.md

@@ -41,8 +41,6 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a
 
 A completed Turn shows an expandable usage row only when the loaded window includes `turn/start` and every started model attempt reports safe, exact usage. The row omits unavailable optional buckets. Incomplete or contradictory accounting hides the complete disclosure instead of presenting a partial total.
 
-Reply timing survives reopening history: recorded token timestamps supply first-token latency and generation duration, including the Step's first token from an earlier retry attempt. Missing Step starts or token timestamps leave the affected metrics unavailable.
-
 <a id="completed-turn-footer"></a>
 ## Completed-turn footer
 

+ 0 - 2
packages/client/ui-chat/README.zh.md

@@ -43,8 +43,6 @@ kind: "package-reference"
 
 只有当已加载窗口包含 `turn/start`,且每次已启动的模型尝试都报告安全、精确的用量时,已完成轮次才显示可展开的用量行。该行会省略不可用的可选用量桶。记账不完整或相互矛盾时,整个详情都不显示,避免把部分总量冒充完整结果。
 
-重新打开历史后仍保留回复计时:记录中的 token 时间戳提供首 token 延迟和生成耗时,包括步骤中更早重试尝试的首个 token。缺少步骤起点或 token 时间戳时,受影响的指标保持不可用。
-
 <a id="completed-turn-footer"></a>
 ## 已完成轮次的页脚
 

+ 2 - 21
packages/client/ui-chat/src/client/conversation-nodes/assistant.ts

@@ -4,7 +4,6 @@ import type {
   ConversationNodeContext, ConversationNodeDefinition,
 } from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type { StreamChunk } from '@deepseek-ai/dsh-llm'
-import { assistantStreamFirstTokenTime } from '@deepseek-ai/dsh-llm/assistant-stream'
 import type {} from '@deepseek-ai/dsh-llm-retry/types'
 import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
 import type { AssistantChatData } from '../contract/chat-nodes.ts'
@@ -162,17 +161,6 @@ function updateChunk(
   }
 }
 
-/** The Step retains its first token across live chunks and settled retry attempts. */
-function settleTiming(
-  state: AssistantState,
-  event: SessionEvent<'assistant/message' | 'assistant/attempt'>,
-): AssistantState {
-  return {
-    ...state,
-    firstTokenTime: state.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream),
-  }
-}
-
 function settleMessage(
   state: AssistantState,
   match: ConversationMatch,
@@ -180,7 +168,7 @@ function settleMessage(
 ): AssistantState {
   const blocks = toAssistantBlocks(event.data.message.content)
   return {
-    ...settleTiming(state, event),
+    ...state,
     blocks,
     visibleBlocks: countVisibleBlocks(blocks),
     hidden: false,
@@ -248,11 +236,6 @@ function fallbackState(context: ConversationNodeContext<AssistantState>): Assist
       state = updateChunk(state, match.event.data.chunk, match.event.seq, match.event.time)
       continue
     }
-    if (match.event.type === 'assistant/attempt') {
-      state ??= initialState(match.event.data.turn, match.event.data.step)
-      state = settleTiming(state, match.event)
-      continue
-    }
     if (match.event.type === 'assistant/message') {
       state ??= initialState(match.event.data.turn, match.event.data.step)
       state = settleMessage(state, match, match.event)
@@ -313,7 +296,6 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
   match: (event) => {
     if (event.type === 'step/start') return { id: `${event.data.turn}:${event.data.step}`, role: 'start' }
     if (event.type === 'assistant/live-chunk'
-      || event.type === 'assistant/attempt'
       || (event.type === 'assistant/message' && event.surfaceOp === 'append')) {
       return { id: `${event.data.turn}:${event.data.step}`, role: 'update' }
     }
@@ -331,14 +313,13 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
       return updateChunk(context.state, match.event.data.chunk, match.event.seq, match.event.time)
     }
     if (match.event.type === 'assistant/message') return settleMessage(context.state, match, match.event)
-    if (match.event.type === 'assistant/attempt') return settleTiming(context.state, match.event)
     if (match.event.type === 'llm/retry') {
       return resetForRetry(context.state)
     }
     return context.state
   },
   publication: (match) => {
-    if (match.event.type === 'step/start' || match.event.type === 'assistant/attempt') return 'none'
+    if (match.event.type === 'step/start') return 'none'
     if (match.event.type !== 'assistant/live-chunk') return 'immediate'
     const type = match.event.data.chunk.type
     return type === 'usage' || type === 'finish' ? 'none' : 'animation-frame'

+ 2 - 9
packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts

@@ -1056,16 +1056,9 @@ describe('built-in conversation node Definitions', () => {
     const finalNode = (node(finalizedPacked, 'assistant-step')?.data as AssistantChatData).finalNode
     expect(finalNode).toMatchObject({
       blocks: [{ kind: 'text', text: 'done' }],
-      timing: { firstTokenTime: 1_999 },
+      timing: { firstTokenTime: null },
     })
 
-    const windowed = assembler(finalizedInputs.slice(2), true)
-    const timing = () => (node(snapshot(windowed), 'assistant-step')?.data as AssistantChatData).finalNode?.timing
-    expect(timing()).toMatchObject({ stepStartTime: null, firstTokenTime: 1_999 })
-    windowed.prepend(finalizedInputs.slice(0, 2), false)
-    windowed.flush()
-    expect(timing()).toEqual(finalNode?.timing)
-
     const namedToolHistory = [
       at(40, 'turn/start', { turn: 3 }),
       at(41, 'step/start', { turn: 3, step: 1 }),
@@ -1090,7 +1083,7 @@ describe('built-in conversation node Definitions', () => {
     const namedTool = (node(namedToolPacked, 'assistant-step')?.data as AssistantChatData).finalNode
     expect(namedTool).toMatchObject({
       blocks: [{ kind: 'tool-call', callId: 'call-2', name: 'read', argsRaw: '' }],
-      timing: { firstTokenTime: 4_000 },
+      timing: { firstTokenTime: null },
     })
   })