Skip to content

fix(desktop): fence stale transcript range failures - #3406

Merged
M4n5ter merged 2 commits into
apache:mainfrom
M4n5ter:fix/3394-transcript-close-race
Aug 21, 2026
Merged

fix(desktop): fence stale transcript range failures#3406
M4n5ter merged 2 commits into
apache:mainfrom
M4n5ter:fix/3394-transcript-close-race

Conversation

@M4n5ter

@M4n5ter M4n5ter commented Aug 21, 2026

Copy link
Copy Markdown
Member
English

Summary

Fence Desktop transcript range reads to the exact renderer registration and Runtime Host source generation that started them. Closing a session or replacing its Host now cancels stale range failures at the registry boundary, while failures from the current registration and source still propagate normally. Same-source subscription recovery is fenced independently by the exact consumer, replica, and generation owned by the observer.

Also prevent an obsolete transcript controller from presenting a history-load error after the user has moved to another session or returned through an A → B → A navigation.

This keeps teardown immediate and preserves delivery ACK/backpressure semantics; it does not add consumer reuse, request replay, string-matched cancellation, or a second lifecycle state machine.

Fixes #3394

Verification

  • npm run lint
  • npm run format:check
  • npm --workspace @maka/desktop run typecheck
  • npm --workspace @maka/desktop test — 1,019 tests passed
  • Added deterministic lifecycle regressions covering consumer close, Runtime Host source replacement, same-source replica recovery, and real failures from the current owner.
  • Not run: Windows Electron real-window smoke. There is no visual change; the reported race is covered at the owning lifecycle boundary.

Root cause

RuntimeHostSessionObservationRegistry already fenced transcript open/restore work by registration and source identity, but loadTranscriptBefore and loadTranscriptAround returned the physical observer promises directly. A close or Host replacement could therefore reject an operation that no longer belonged to the current logical consumer, leaking it through Electron IPC and the renderer toast path. Same-source subscription recovery can also replace the observer replica without changing the outer Registry identities, so the Observer now fences range completion by consumer, replica, and generation.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex investigated the lifecycle race, developed the registration/source fencing design, implemented the change and regression coverage, and prepared this draft PR. The human contributor remains responsible for review and submission.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — stale transcript operations are cancelled instead of surfacing as current-session failures
  • No
中文

概述

将 Desktop transcript range read 绑定到发起它的精确 renderer registration 和 Runtime Host source generation。会话关闭或 Host replacement 后,Registry 会在生命周期边界取消旧 range operation 的失败;当前 registration 和 source 的真实错误仍然正常传播。同一 source 内部发生 subscription recovery 时,Observer 也会用精确 consumer、replica 和 generation 独立 fence 旧操作。

同时,过期 transcript controller 在用户切换会话或经历 A → B → A 后,不再有权提交历史读取错误。

该方案保持 teardown 立即执行,也保留 delivery ACK/backpressure 语义;不增加 consumer 复用、请求重放、错误字符串匹配或第二套生命周期状态机。

对应并关闭 #3394

验证

  • npm run lint
  • npm run format:check
  • npm --workspace @maka/desktop run typecheck
  • npm --workspace @maka/desktop test — 1,019 项测试通过
  • 新增确定性生命周期回归测试,覆盖 consumer close、Runtime Host source replacement、同一 source 内的 replica recovery,以及当前 owner 的真实失败。
  • 未运行 Windows Electron real-window smoke。本次没有视觉变更;报告中的竞态已在拥有该生命周期的边界完成覆盖。

根因

RuntimeHostSessionObservationRegistry 已经用 registration/source identity fence 保护 transcript open 和 restore,但 loadTranscriptBeforeloadTranscriptAround 直接返回物理 observer Promise。因此 close 或 Host replacement 可以拒绝一个已经不属于当前逻辑 consumer 的 operation,最终通过 Electron IPC 和 renderer toast 泄漏为当前错误。同一 source 内部的 subscription recovery 也可以在不改变 Registry 外层 identity 的情况下替换 observer replica,因此 Observer 现在按 consumer、replica 和 generation fence range completion。

AI 使用

  • 没有生成式工具作出实质贡献
  • 生成式工具作出了实质贡献

工具和范围:Codex 排查生命周期竞态、设计 registration/source fencing、实施修改与回归覆盖,并准备本 draft PR。最终审查与提交责任仍由人类贡献者承担。

Checklist

  • 测试覆盖本次变更,并会在修复前失败
  • 本地 lint、format、typecheck 和受影响测试套件均已通过

本 PR 是否改变行为?

  • 是——过期 transcript operation 会被取消,不再作为当前会话错误呈现

Bind range reads to the registration and Host source generation that started them, so teardown and replacement errors cannot leak as failures of the current consumer. Preserve real errors while preventing stale controllers from presenting feedback on a newer surface.

Generated-by: Codex

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review of exact head 2a57d69547d943f1f7c479f482a8f71efb9e33f3.

I confirmed the current main@efe381f4fd registry still returns physical transcript range promises directly, so a close or Host-source replacement can leak a stale rejection through IPC. The patch fences each operation by both transcript-registration object identity and source object identity, while preserving failures from the still-current pair. The renderer guard uses both active Session and controller identity, which correctly handles A → B → A rather than relying on Session ID alone. The deterministic regression covers consumer close, source replacement, and propagation of a real current-source failure. I found no P0-P2 defect.

Required CI is green on this exact head. Merge readiness: not ready yet because the PR is draft and still needs independent human review.

@M4n5ter
M4n5ter marked this pull request as ready for review August 21, 2026 09:27
Keep range failures scoped to the consumer, replica, and generation that started them. Same-source subscription recovery can replace the physical replica without changing the outer registry lease, so stale operations must settle at the observer boundary while current failures continue to propagate.

Generated-by: Codex
@M4n5ter
M4n5ter force-pushed the fix/3394-transcript-close-race branch from 40aae49 to d146f12 Compare August 21, 2026 09:27
@M4n5ter
M4n5ter requested a review from jackwener August 21, 2026 09:29

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing this to stale operation ownership rather than reopening or reusing a closed transcript consumer. I reviewed exact head d146f122b373ce25eada0c4c5e260fa5512335cb, including the latest same-source replica recovery change. The Registry, Observer, and renderer fences each protect a distinct existing ownership boundary, preserve failures from the current owner, and avoid introducing a parallel lifecycle authority. The deterministic regressions cover consumer close, Host-source replacement, replica recovery, and current-owner failures. I found no P0–P3 findings, and required CI is green.

AI-assisted review disclosure: Codex performed the code analysis. The human reviewer reviewed the analysis and made the final approval decision for this exact head.

@M4n5ter
M4n5ter merged commit cabd68f into apache:main Aug 21, 2026
1 check passed
@M4n5ter
M4n5ter deleted the fix/3394-transcript-close-race branch August 21, 2026 09:45
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.

[desktop] Switching between sessions fails: transcript consumer closed (sessions:transcript:load-before)

3 participants