Skip to content

fix(flowchat): recover aux pane tabs whose child session is missing - #3375

Open
czs914893385 wants to merge 2 commits into
GCWing:mainfrom
czs914893385:fix/web-ui-aux-pane-child-session-recovery
Open

czs914893385 wants to merge 2 commits into
GCWing:mainfrom
czs914893385:fix/web-ui-aux-pane-child-session-recovery

Conversation

@czs914893385

@czs914893385 czs914893385 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Symptom

The auxiliary pane (the right-hand pane of the session scene) can hold a side-thread tab
(btw-session) whose child session record is gone. Instead of recovering, the pane shows the
generic empty copy — 暂未打开子会话 / No side thread open — forever: the tab stays mounted,
clicking it does nothing, and the only way out is to find and close the tab elsewhere in the
canvas. The state is reachable from ordinary user actions: deleting, archiving, or rolling back
the child session, or resetting the workspace, while the tab is still open.

Root cause

Confirmed, with the pre-fix line numbers on main (aed8c001):

  1. The tab remembers only the child id. buildBtwSessionPanelContent
    (src/web-ui/src/flow_chat/services/btwSessionPane.ts:104) stores childSessionId /
    parentSessionId in the panel data and a duplicate key in the tab metadata
    (btwSessionPane.ts:123). The child's identity — its kind, title, parentToolCallId,
    subagentType — is not part of the tab.
  2. Hydration happens only when the tab is opened. The one path that recreates a missing
    record and loads its history is ensureBtwSessionAvailableInternal
    (btwSessionPane.ts:218), reached from openBtwSessionInAuxPane (btwSessionPane.ts:292).
    Nothing re-runs it later, so a record that disappears after the tab was opened is never
    noticed.
  3. The failures are swallowed. That path discards the load outcome —
    void loadBtwSessionHistory({...}).catch(() => undefined) (btwSessionPane.ts:280-284) — and
    the fire-and-forget entry point ensureBtwSessionAvailable (btwSessionPane.ts:288) returns
    nothing a caller could observe.
  4. The record really can disappear while the tab stays. FlowChatStore.deleteSession
    (FlowChatStore.ts:5368), removeSession (:5418) and removeSessionsForWorkspace
    (:5520) drop the record from the store; canvas tabs are unaffected.
  5. So the pane renders the empty state, not a failure. BtwSessionPanelContent hits
    if (!childSessionId || !childSession) (BtwSessionPanel.tsx:1051) and renders
    btw.emptyThreadLabel with data-openbitfun-view="empty"
    (BtwSessionPanel.tsx:1053, :1055). There is no retry, no close action, and no distinction
    between "no tab is open" and "this tab's session is gone".

The fix

  • btwSessionHydration.ts (new) — a pure tracker that allows exactly one load attempt per
    absence, keyed by the child id. It re-arms when the child id changes, when the record comes
    back, or for an explicit user retry; it refuses to arm a pane that cannot start an attempt
    (the MiniApp customize chat embeds this panel with a child id but no parent, so it must
    explain the absence instead of loading forever), and it keeps an attempt that finished without
    a usable record in the actionable state rather than looping.
  • btwSessionPane.ts — ensureBtwSessionAvailableAndSettle exposes the history-load promise
    so a caller can render the failure; openBtwSessionInAuxPane records the child identity in the
    tab (BtwSessionPanelChildIdentity) and keeps its fire-and-forget semantics;
    hasBtwSessionAuxPaneTab tells an embedded pane whether a tab actually owns the child.
  • BtwSessionPanel.tsx — a wrapper (useBtwSessionHydration) owns the recovery, so a tab
    that is activated later still heals while an inactive tab keeps the folder's
    "no subscription, no work" contract. A mounted child tab with no record now renders a loading
    state (btw.loadingThreadLabel) and, on failure, an actionable state
    (btw.unavailableThreadLabel) with 重新加载 (only when this pane can start an attempt) and
    关闭标签 (only when a tab really owns the child). Healing passes the recorded identity, so a
    healed subagent child is recreated as sessionKind: 'subagent' with its title,
    parentToolCallId and subagentType instead of degrading into a plain side thread. Recovery
    never hides a transcript that is already renderable.
  • Locales — four new btw.* keys in en-US, zh-CN, zh-TW.
  • Appearance contract — the view facet gains loading and unavailable.

How to reproduce

Before this PR (on main):

  1. Run a task that spawns a subagent and open the side thread in the auxiliary pane
    (a btw-session tab).
  2. Delete, archive or roll back that child session (or reset its workspace) while the tab stays
    mounted.
  3. The pane keeps showing 暂未打开子会话, the tab does nothing when clicked, and there is no
    retry and no close action in the pane.

After this PR: the pane shows a loading state, the child record is recreated from the identity
the tab carries and its history reloads; if the load fails the pane shows 子会话加载失败
with 重新加载, plus 关闭标签 when a canvas tab owns that child. BtwSessionPanel.missingSession.test.tsx
(13 tests) and the new btwSessionHydration.test.ts cover the once-only guard, the retry, the
close-tab affordance, the restored subagent identity, and the "do not hide a renderable
transcript" rule; btwSessionPane.navigation.test.ts and openBtwSession.test.ts cover the
production wiring end to end.

Verification

Everything below ran on a fresh single-branch git clone of main
(aed8c001a72c051ca74607e7eed8d84f6c2aee85) with only these 12 files overlaid — no
pane-resize file is present — using node v22.23.2 and pnpm 10.15.0.

command result
node scripts/i18n-audit.mjs exit 0 — "Passed with 0 warning(s)"
pnpm run lint:web exit 0 — 0 errors, 10 pre-existing warnings (none in the changed files)
pnpm --dir src/web-ui exec tsc --noEmit -p tsconfig.json exit 0
pnpm --dir src/web-ui exec vitest run src/flow_chat/components/btw src/flow_chat/services exit 0 — 59 files / 725 tests passed
pnpm --dir src/web-ui exec vitest run src/app/components/panels src/app/scenes/session src/app/scenes/miniapps exit 0 — 39 files / 210 tests passed
pnpm --dir src/web-ui run test:run (default --maxWorkers=50%) 973 files / 10786 tests passed, exit 1 on one unhandled jsdom teardown error (window is not defined raised while useFlowChatVirtualizer.aim.test.tsx tore down). A control run of the same command on pristine main at the same worker count also fails (1 failed file / 1 failed test, 970 passed), so this is environmental, not caused by this change.
pnpm --dir src/web-ui exec vitest run --maxWorkers=4 exit 0 — 973 files / 10786 tests passed
pnpm run build:web exit 0
node scripts/audit-appearance-contracts.mjs exit 0 — 246 surfaces, 2995 DOM contracts, 284 styled component owners

tsc needs the generated api barrel (@/generated/api), which is produced by
pnpm --dir src/web-ui run gen:types and is gitignored; it was generated before type-checking.

CI note: the Frontend Build job stops at Check repository hygiene, whose
product:identity:audit step flags design-system/apps/design-lab/src/pages/OverviewPage.tsx
(retired product name). That failure is inherited from main, not introduced here — the same
command fails with the identical output on a pristine main checkout, and the CI run for main
at this PR's base commit (aed8c001) is already red. The other jobs are green
(Repository Object Sizes; Shell Deploy Scripts, Build Impact, Rust / CLI Validation).

Relationship to the in-flight pane-resize PR #3371

BtwSessionPanel.tsx and FlexiblePanel.tsx are also modified by the in-flight
perf(web-ui): stop re-measuring transcripts while a pane divider is dragged (#3371). This PR
was split out of that work to be independently reviewable and shippable:

Accepted residual

A heal that resolves while leaving a record with zero turns still renders the generic
empty-thread copy for that child. The tracker treats an absent record as the recoverable state,
and a deliberately kept empty shell record is not treated as a recovery target. This is recorded
as an accepted residual rather than papered over.

中文说明

问题现象

辅助面板(会话场景右侧面板)里可能挂着一个子会话标签页(btw-session),而它对应的子会话记录已经不存在。此时面板会一直显示「暂未打开子会话」这个通用空状态:标签页还在,点击它没有任何反应,用户只能在画布别处找到并关闭标签页。常见触发方式:在标签页仍然打开时删除/归档/回滚该子会话,或重置所在工作区。

根因(已确认,行号基于修复前的 main)

  1. 标签页只记住子会话 id:buildBtwSessionPanelContent(btwSessionPane.ts:104)只在面板数据里保存 childSessionId / parentSessionId,标签页元数据里只有去重键(:123),子会话的身份(类型、标题、parentToolCallId、subagentType)没有被保存。
  2. 补齐记录只在「打开标签页」那一刻发生:唯一会重建记录并加载历史的入口是 ensureBtwSessionAvailableInternal(:218),由 openBtwSessionInAuxPane(:292)调用;之后没有任何逻辑会重新检查。
  3. 失败被吞掉:该路径用 void loadBtwSessionHistory({...}).catch(() => undefined)(:280-284)丢弃加载结果,而 ensureBtwSessionAvailable(:288)也不返回任何可观察的东西。
  4. 记录确实会在标签页存活期间消失:FlowChatStore.deleteSession(FlowChatStore.ts:5368)、removeSession(:5418)、removeSessionsForWorkspace(:5520)会删除记录,而画布标签页不受影响。
  5. 于是面板落到 if (!childSessionId || !childSession)(BtwSessionPanel.tsx:1051),渲染 btw.emptyThreadLabel(:1053、:1055),既没有重试也没有关闭入口,也不区分「没有打开任何标签页」和「这个标签页的会话丢了」。

修复内容

  • 新增 btwSessionHydration.ts:按子会话 id 记录状态的纯逻辑追踪器,每次「记录缺失」只允许一次加载尝试;id 变化、记录恢复或用户显式重试时才重新武装;对于无法发起加载的面板(例如只带子会话 id、没有父会话的 MiniApp 内嵌面板)不武装尝试,而是直接给出可解释的状态。
  • btwSessionPane.ts:新增 ensureBtwSessionAvailableAndSettle 返回加载 Promise 以便面板渲染失败;openBtwSessionInAuxPane 把子会话身份写进标签页(BtwSessionPanelChildIdentity),同时保持原有的「即发即忘」语义;新增 hasBtwSessionAuxPaneTab 判断当前子会话是否真的有面板标签页。
  • BtwSessionPanel.tsx:由外层组件(useBtwSessionHydration)负责自愈,未激活的标签页仍然保持「不订阅、不做功」的约定,被激活后立即自愈。缺失记录的标签页先显示加载状态(btw.loadingThreadLabel),失败后显示可操作状态(btw.unavailableThreadLabel),按面板能力给出「重新加载」(可以发起加载时)和「关闭标签」(确实有标签页时)。自愈时会带上标签页记录的身份,使子代理子会话恢复为 sessionKind: 'subagent' 并保留标题、parentToolCallId、subagentType,而不会退化成普通子会话。已经可以渲染的会话内容不会被自愈状态遮挡。
  • 文案:en-US、zh-CN、zh-TW 各新增 4 个 btw.* 键。
  • 外观契约:view facet 增加 loading、unavailable。

验证

全部在 main(aed8c001a72c051ca74607e7eed8d84f6c2aee85)的全新单分支克隆上、仅覆盖本 PR 的 12 个文件(不含任何 pane-resize 文件)完成,使用 node v22.23.2 与 pnpm 10.15.0:i18n-audit 0 警告;lint:web 0 error / 10 个既有 warning;tsc --noEmit 通过;flow_chat/components/btw + flow_chat/services 59 文件 / 725 测试通过;app/components/panels + app/scenes/session + app/scenes/miniapps 39 文件 / 210 测试通过;test:run(默认 --maxWorkers=50%)测试全绿但在 jsdom 拆卸阶段出现 1 个环境性未捕获异常(原始 main 对照组同样失败),改用 --maxWorkers=4 后 exit 0(973 文件 / 10786 测试);build:web 与 audit-appearance-contracts 均通过。

补充说明:本 PR 的 CI Frontend Build 任务停在 Check repository hygiene,其 product:identity:audit 步骤报 design-system/apps/design-lab/src/pages/OverviewPage.tsx(retired product name)。该失败继承自 main:在干净的 main 上执行同一命令会得到完全相同的报错,且本 PR 基线提交(aed8c001)在 main 上的 CI 本来就是红色。其余任务均通过(Repository Object Sizes;Shell Deploy Scripts、Build Impact、Rust / CLI Validation)。

与在途 PR #3371 的关系

BtwSessionPanel.tsx 与 FlexiblePanel.tsx 同时被在途的面板拖拽性能 PR #3371 修改。本 PR 已把自愈修复拆分出来独立提交:diff 中不含任何 #3371 的改动(没有新增 resizeSuspended / measurementSuspended),可以在干净的 main 上独立编译与通过测试。未来合并在临时克隆中演练过:main 加上 #3371 的补丁后再合并本分支,BtwSessionPanel.tsx 自动合并成功,FlexiblePanel.tsx 只有一处纯文本冲突(两边都在 displayTitle={content.data?.displayTitle} 之后插入 props),两边都保留即可解决;解决后的合并结果没有冲突标记,相对 main + #3371 只改动这 12 个文件,tsc 与两个聚焦测试套件依然通过。

已知残留

自愈「成功返回」但记录里没有任何轮次时,该子会话仍然显示通用的空会话文案。追踪器把「记录缺失」当作可恢复状态,而故意保留的空壳记录不视为恢复目标。这一点作为已接受的残留记录在案。

@GCWing
GCWing requested a review from wsp1911 October 19, 2026 02:55
…recovery

Brings the aux-pane child-session recovery work up to current main.

Conflict (only one file, src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx,
case 'btw-session'): both sides appended a prop to <BtwSessionPanel>. Kept both - upstream's
resizeSuspended={resizeSuspended} (perf(web-ui) 6f2ec2b, the merged pane-resize PR GCWing#3371)
and this branch's four child-identity props (sessionKind / sessionTitle / parentToolCallId /
subagentType). No other file needed manual work; the pane-resize plumbing is untouched.
@czs914893385

Copy link
Copy Markdown
Contributor Author

Refreshing the PR state: closing and reopening so GitHub recomputes the base it diffs against (it was showing the changes against a stale base). No code change — the branch is simply rebased onto current main.

刷新一下 PR 状态:先关后开,让 GitHub 重算 diff 的基线(之前对着一个陈旧基线在算)。代码没有变化,分支已基于当前 main。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant