Skip to content

fix(app): keep delegated results visible across session changes - #5170

Merged
huangruiteng merged 4 commits into
mainfrom
codex/app-delegation-return
Sep 27, 2026
Merged

huangruiteng merged 4 commits into
mainfrom
codex/app-delegation-return

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

An Agent may finish a delegation after the App has switched models or replaced its active session. The App previously polled only the newest session, so the saved result could remain invisible until reload. Reading that session could also erase another session's delegation metadata.

The conversation now reads its active session and older sessions with unresolved transcript or return obligations. A shared TypeScript read model scopes updates by session, deduplicates results, preserves streaming text and retires completed old reads. No model replay, execution driver or persisted contract is added.

Anchor: the accepted App conversation RFC, R3 and GQ02–04. Base: main. The RFC, roadmap and golden-query acceptance now explicitly include late returns after session replacement.

Scope And Continuation

This fixes late-return visibility in the existing App conversation. It does not certify the complete managed/attached journey or real two-cycle team adoption. Those existing product checkpoints remain open; acceptance-recovery and GoalRef work in #5139, #5130 and #5106 are reused rather than duplicated.

The bounded refactor removes the inline single-session merge from the large page module. Backend delivery, permissions and Lark transport are unchanged. Default readback now includes unresolved older sessions from the visible authorized conversation; completed historical sessions do not acquire permanent polling.

Validation

  • Tested revision: 2d23e810fdfa50b7515bb32edcc6d1d131531a4b
  • Run state: finished
  • Input classes: synthetic, public_fixture
Check kind Result Evidence / limitation
regression_parity passed New conversation-return-continuity browser scenario times out waiting for the late result on the original code; candidate displays it once without another model turn.
unit passed npm run test:conversation-returns: session isolation, colliding IDs, incomplete projection, deduplication, delivery uncertainty, streamed-text preservation and retiring old reads.
real_entrypoint passed Desktop/Chat production builds; new return-continuity scenario and existing chat-recovery scenario pass in source and packaged browser modes. Browser APIs use synthetic fixtures.
real_backend passed Isolated production ChatRequestHandler, File-backed Chat store, actual report/drain and TS reconciliation: an older session receives one verified result after a new session exists; repeated HTTP readback does not duplicate it. No model or external message call.
integration passed pytest tests/test_manager_context_roundtrip.py tests/test_manager_context_tracking.py: 27 passed.
premerge passed 16 selected checks and 3 direct checks; no failures or manual holds; exact-scope quality receipt verified. Initial missing repository TypeScript dependency repaired with npm ci; semantic scan and full gate then passed without source changes.
static passed Docs governance, candidate public-boundary scan (9 files), typecheck and diff hygiene. Existing bundle-size advisory remains.

An editing-time development run hit the duplicate-result assertion; the frozen final source and packaged recovery reruns pass. Full native managed/attached model execution and user adoption are not claimed by these deterministic checks.

Frontend / Visual Evidence

No layout, first-screen navigation, styling or new control changes. Existing delegation cards and Markdown results receive the previously missing data. The browser scenarios exercise the populated original conversation, a replaced session, interrupted read, delayed result and delivery verification. Existing recovery coverage also checks narrow-screen rendering. Source data is synthetic.

Attention review: results return to the existing conversation without another prompt or an additional panel. Previously observed receipts remain visible when projection metadata is temporarily unavailable; polling never invents execution or adoption.

Boundaries

  • No server permission, authority-store, scheduler or persisted schema changes.
  • No private runtime state, local evidence files, credentials or generated bundles are included.
  • Runtime/product merge remains with the maintainer.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

Reviewed exact head: 2d23e810fdfa50b7515bb32edcc6d1d131531a4b; immutable merge-base: 74d372c4925c1043f1bb4725bea4fdc3e99790cc.

未发现阻塞项。下面的结论来自本次独立验证,不把作者记录当作验收证据。

动机

App conversation RFC 的 Result 行要求结果回到原对话。旧实现只读取当前 session;更换 session 后,旧交办即使已经完成回传也可能必须刷新才能看见,而且新 session 快照会抹掉旧交办元数据。这不是“后端成功即交付完成”:用户能在原处获得结果,才是这条前端路径的可观察增量。

改动思路

复用现有 Chat transcript、session/message/Turn lineage 和 return-delivery 事实,不建第二份 inbox、执行器或工作状态。前端仅推导当前会话仍需读取的 session 集合,再按来源合并快照。直接刷新整个历史虽然代码更短,却会扩大读取、破坏 live stream 的文本归属,也无法说明旧轮询何时退休;把这组规则从大型页面提取成一个有实际调用者的 TS read model,是合适的局部简化。

具体改动

关键代码讲解

  • conversationReturnSessions:保留 active session,以及未得到结论、投递尚待验证或流式答案尚无存储身份的旧 session。集合去重排序,避免每次 delta 重启订阅;已经确认返回的旧记录不产生永久轮询。
  • reconcileConversationReturns:只刷新所读 session 的记录,按 message ID 或非 followup 的 Turn 对齐。暂时缺少元数据不代表撤回已有事实;去重追加 manager_followup,更新身份与回传状态,而不覆盖可见流式文本和 pending。
  • PersonalGoalHome 的 receive:每个尚有义务的 session 独立读取、失败重试;cleanup 取消旧回调与定时器,写回原 context。恢复、新 Turn 和 steer 路径补上来源 session,完成态快照带回 collaboration/returnDelivery。

九个变更文件均已检查:57 行 read model 替代页面中的内联合并;42 行单测和新 npm 脚本覆盖跨 session、重复回执、投递不确定及 stream 保留;63 行浏览器场景接入现有 runner;三份 RFC/roadmap/golden-query 文档补上 late-return 判据和交付边界。没有依赖升级、服务端权限、持久化 schema、布局或首屏导航变化。后端原有 project_chat_session_snapshot → project_collaboration / return_delivery 仍提供事实,前端没有把读取资格当成执行授权。

对主干的风险

独立浏览器脚本在 base 与 exact head 使用相同公开 fixture:替换 session 后,旧读取先失败一次、再返回空快照,另加入 80 条无关历史并反转消息顺序。base 的旧结果不可见,且旧交办元数据被清除;head 的源码与打包入口均显示结论恰好一次、保留交办卡、不新增模型 Turn。投递 verification_required 时继续读;改为 delivered 后,观察两轮间隔确认旧 session 不再被读取。回归判据来自 RFC 的原对话返回要求,而不是新 helper 的输出。

另外,通过真实 ChatHTTPServer、File-backed Chat store 和生产 deliver/report/drain 创建旧委托与新 session,执行并发 drain 和重启后的重复 drain,再从 HTTP 读回完整快照:只有原 session 得到一个关联原 Turn 的已验证结论,新 session 为空,没有新增 Turn。规范化随机身份及时间后,base/head 的完整后端观察相同。这验证真实后端数据形态;浏览器 fixture 本身仍不能证明真实 native worker 的执行与采用。

本地结果:两边的 27 项 manager roundtrip/tracking 测试通过;新 read-model 单测、Desktop/Chat production build、原有 chat-recovery 浏览器路径以及新增 return-continuity 均通过;独立源码和 packaged 反例通过。Ruff、mypy、控制面 typecheck、docs governance、候选公共边界与 diff hygiene 均通过。现有 bundle-size advisory 两边都有,未通过修改预算掩盖它。按当前 capability 不查询、不等待远端 CI。

剩余边界明确:本次没有资格认证完整 managed/attached native host journey、真实两周期团队 adoption 或大规模旧 session 的长期轮询成本;它们不是此修复新增的能力声明。后端当前 session snapshot 返回完整 transcript,不是截断页,所以空/缺失元数据只保留已有义务,不能被解释为“结果不存在”。

我的整体评价

APPROVE 当前 exact head,并以 author-owned COMMENTED review 记录。持续运行与用户体验均改善:异步结果跨 session 留在原对话,失败只重试读取,确认返回后释放旧读取成本。未来演进检查已应用在同一边界:移除大型页面的合并规则,复用既有身份和 owner,没有新增兼容版本或手工同步状态。它是可独立验收、回滚的前端增量,不等于完成整个 R3/G1;既有 #5139、#5130、#5106 及原有 native/team acceptance 路线继续承担后续验收。产品运行时变更仍交由 maintainer 合并。

English verdict: APPROVE - head 2d23e81. Independent source/packaged browser counterexamples prove late-return visibility, isolation, no replay and watch retirement; real HTTP/File return readback and 27 backend tests passed. Full native multi-agent acceptance remains separate.

@mergify

mergify Bot commented Sep 27, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @huangruiteng.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Sep 27, 2026
Resolve the roadmap conflict: main rewrote the App-first delivery paragraph
around the concrete pilot and exits, so keep main's newer plan text and fold
this PR's late-return clause into it rather than restoring the superseded
wording.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…nario

main's \"distinguish recipient disposition from reply delivery\" rename replaced the
collaboration-card copy, so the delivered conclusion now reads 回复已送达 /
Reply delivered instead of 结论已回传. Assert the current owner copy so the
late-return scenario keeps proving the delivered state after session
replacement instead of timing out on a retired label.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

PR #5170 复审 — exact head 9521f65467f55f5abfa69fb524e34dd8733c761d

动机

App 会话里交办出去的活可能在模型切换或活动会话被替换之后才完成。此前 App 只轮询当前最新
session,于是已经落盘的结果要等到手动刷新才可见;而且用一份单 session 快照重建消息列表时,
会把另一个 session 的交办元数据一起抹掉。本 PR 让会话读取覆盖「活动 session + 仍有未结
义务的旧 session」,把原来的内联合并抽成共享的 TypeScript 读模型:按 session 限定更新范围、
去重结果、保留正在流式输出的文本,并在义务结算后停止轮询旧 session。它不重放模型、不改执行
driver、不改持久化契约,方向与已接受的 App 会话 RFC(R3 与 GQ02–04)一致;同一次改动还把
EN 版 RFC 与中文版拉回对齐(中文版早已有该「最近可感知交付」小节)。

可观测的前后差异有真实场景支撑:新增的 conversation-return-continuity 浏览器场景在旧实现上
会一直等不到迟到结果,在候选实现上结果只显示一次且不产生新的模型回合。交付判断:这是一个
完整可回滚的切片——它修的是既有 App 会话路径上的可见性缺陷,不宣称已完成整段 managed/
attached 旅程或真实团队采纳,那些检查点仍然开放。

改动思路

入口是 App 个人工作区的会话视图;权威输入是后端返回的 session 快照(
/api/chat/sessions/<id>);决策边界被收敛到新模块
apps/presentation/dashboard/src/data/conversation-returns.ts:conversationReturnSessions
决定「要读哪些 session」,reconcileConversationReturns 决定「一个 session 的快照如何并入
本地列表」。副作用只发生在前端消息列表,不写后端、不触发回合。正路径是:活动 session 的
快照照旧合并;带未结义务的旧 session 也各自被读一次,把迟到结论接回原交办,义务结算后
该 session 从轮询集合中退出。

复用与归属:被删掉的正是 dashboard-page.tsx 里那段单 session 内联合并(约 55 行),抽出的
读模型与既有 src/data/chat.ts、chat-model.ts 同层,消费者仍是既有的
collaboration-card.tsx 与 return-delivery-status.tsx,没有新建第二套会话存储、状态权威或
调度器。同一个 2,700+ 行的页面模块因此不再承载这段合并规则,后续会话读取改动只需面对一个
可单测的模块。

具体改动

净变化 9 文件 +222/-50:生产代码新增 57 行读模型、页面模块净减少约 11 行(+28/-39),
新增 42 行单测与 63 行浏览器场景,其余是 RFC/路线图/golden query 文档与一条 npm script。

关键代码讲解

  1. conversation-returns.ts:12 conversationReturnSessions 是「该读哪些 session」的唯一判据:
    集合从活动 session 起,再加入仍欠结论(collaboration.returns 中没有 delivered 的
    conclusion)、仍欠投递(returnDelivery.status 不在 delivered/superseded)或仍欠
    transcript(有 sourceTurnId 但没有 sourceMessageId)的旧 session,最后排序返回。
    义务结算即退出轮询,避免历史 session 被永久读。
  2. conversation-returns.ts:27 reconcileConversationReturns 按 session 限定更新:
    seen 只取本 session 已见过的 sourceMessageId,byId/byTurn 也只用该 session 的快照,
    因此另一个 session 的同名 ID 不会互相覆盖(单测用 colliding IDs 固定了这条);
    只有 origin === "manager_followup" 的新行才会被追加,且返回原数组引用表示「没有变化」,
    让 React 状态更新可以被短路。它刻意保留本地 text/pending(流式文本不被快照替换),
    只做身份补水。
  3. conversation-returns.ts:44-47 把「投影缺失不等于撤回」写成显式规则:后端一时读不到
    collaboration 元数据时保留上一次观察到的回执与未结义务,直到有更新的观察到达。这是一次
    真实的行为变化(旧代码会把它清成 undefined),代码注释与正文都写明了理由。
  4. dashboard-page.tsx:1490 把轮询集合序列化成 conversationReturnSessionKey 作为 effect 的
    依赖:集合不变则流式增量不会重启 effect;1504 改为逐 session 调用共享读模型,
    1517-1523 为每个 session 维护独立的重试计时器,卸载时统一清理,失败只重试读取、
    绝不重放模型。
  5. dashboard-page.tsx:1692/2144/2180/2696 补上 sourceSessionId 等身份字段,让恢复中的
    回合、阶段回调、完成后的回答与纠偏回合都能被读模型归属到具体 session——这是新读模型
    能按 session 工作的前置条件。
  6. package.json:59 新增 test:conversation-returns,把读模型的单测接进既有 npm 脚本习惯。

对主干的风险

最强回归场景是「读一个 session 却改坏了另一个 session 的状态」,或迟到结果要么永远不出现、
要么重复出现。前者由按 session 限定的 seen/byId/byTurn 与新单测的 colliding-ID 用例覆盖;
后者由浏览器场景断言结果只渲染一次、交办卡只保留一份、且 turnRequests 数量不变(不重放
模型)覆盖。第二类风险是轮询面扩大:此前只读最新 session,现在会同时读仍有义务的旧 session,
请求数随未结义务数增长;这是有界的(义务结算即退出)但确实是对后端的额外读取,我把它记录为
已披露的代价而不是免费改进。

第三类风险由本次维护者修复直接暴露:主干的「distinguish recipient disposition from reply
delivery」把合作卡文案从「结论已回传」改成「回复已送达」,而新场景仍断言旧文案,于是该场景
在合并后必然超时。我按文案的当前所有者(collaboration-card.tsx 的 copy)改成「回复已送达」
并加注释说明它必须跟随该 copy,随后场景在开发与打包两种模式下都通过。顺带发现主干自身
chat-recovery.mjs:127 的否定断言仍在检查那个已被改名的字符串,因此它当前是空断言——
这是主干侧由改名引入的守卫缺口,不在本 PR 边界内,我在此如实记录而不顺手改动。

回滚与恢复:本 PR 只改前端读取与文档,回滚即恢复「只读最新 session、缺失即撤回」的旧行为;
没有持久化状态或权限影响。验证证据:npm run test:conversation-returns 通过;
npm run build:desktop(tsc + vite)通过;personal-workspace-browser-smoke 全 19 个场景在
development 模式下通过,新场景另在 packaged 模式通过;pytest manager_context 36 项通过;
docs governance 通过;loopx canary premerge --from-git-diff 报 status=passed、9 文件、
7 catalog canaries + 8 risk smokes + public boundary、0 manual holds。

语义与 CI 对齐

本 PR 把 RFC 的验收措辞与实现对齐:EN 版 RFC 新增的「Nearest user-visible exit」小节与中文版
既有小节现已等价,路线图的 App-first 段落保留 main 的新版计划并折入「迟到结果在活动会话被
替换后仍可见」,golden-queries 用 GQ02/GQ04/GQ08/GQ09 组合出同一条试点。也就是说,这条
可见性修复不是自证的:它对应 RFC 里已冻结的「刷新或更换会话后仍在」可观测出口。

我的整体评价

这是一条边界清楚的产品修复加一次合理的本地面板重构:它修的是「交办结果在会话被替换后看不见」
这个真实缺口,把重复的单 session 合并规则收敛成一个可单测的共享读模型,并在义务结算后停止
轮询;同时坦白披露了两处行为变化(投影缺失不再撤回回执、轮询面覆盖有义务的旧 session)。
维护者两次修复也记录在案:路线图冲突按 main 的新计划折入本 PR 的迟到结果条款,以及新场景
改断言当前文案「回复已送达」(旧文案已由 main 改名,保留它只会得到假红)。剩余风险是
main 侧 chat-recovery.mjs 的否定断言因同一次改名而失去意义,以及本 PR 未声明的整段旅程
资格,二者都不影响这次的修复边界。建议合入。

English verdict: APPROVE - 9521f65 makes the App conversation read its active session plus older
sessions that still owe a conclusion, delivery or transcript, through one session-scoped
TypeScript read model that deduplicates results, preserves streaming text and retires settled
sessions; the maintainer merge folded this PR's late-return clause into main's newer roadmap
paragraph and re-pointed the new scenario at the renamed delivered-reply copy (main's own
chat-recovery negative assertion is now vacuous and is reported, not repaired here). Unit test,
desktop/chat builds, all 19 browser scenarios in development mode, the new scenario in packaged
mode, manager-context pytest, docs governance and a premerge gate with 0 manual holds all pass.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

PR #5170 复审 — exact head 9521f65467f55f5abfa69fb524e34dd8733c761d

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

动机

App 会话里交办出去的活可能在模型切换或活动会话被替换之后才完成。此前 App 只轮询当前最新
session,于是已经落盘的结果要等到手动刷新才可见;而且用一份单 session 快照重建消息列表时,
会把另一个 session 的交办元数据一起抹掉。本 PR 让会话读取覆盖「活动 session + 仍有未结
义务的旧 session」,把原来的内联合并抽成共享的 TypeScript 读模型:按 session 限定更新范围、
去重结果、保留正在流式输出的文本,并在义务结算后停止轮询旧 session。它不重放模型、不改执行
driver、不改持久化契约,方向与已接受的 App 会话 RFC(R3 与 GQ02–04)一致;同一次改动还把
EN 版 RFC 与中文版拉回对齐(中文版早已有该「最近可感知交付」小节)。

可观测的前后差异有真实场景支撑:新增的 conversation-return-continuity 浏览器场景在旧实现上
会一直等不到迟到结果,在候选实现上结果只显示一次且不产生新的模型回合。交付判断:这是一个
完整可回滚的切片——它修的是既有 App 会话路径上的可见性缺陷,不宣称已完成整段 managed/
attached 旅程或真实团队采纳,那些检查点仍然开放。

改动思路

入口是 App 个人工作区的会话视图;权威输入是后端返回的 session 快照(
/api/chat/sessions/<id>);决策边界被收敛到新模块
apps/presentation/dashboard/src/data/conversation-returns.ts:conversationReturnSessions
决定「要读哪些 session」,reconcileConversationReturns 决定「一个 session 的快照如何并入
本地列表」。副作用只发生在前端消息列表,不写后端、不触发回合。正路径是:活动 session 的
快照照旧合并;带未结义务的旧 session 也各自被读一次,把迟到结论接回原交办,义务结算后
该 session 从轮询集合中退出。

复用与归属:被删掉的正是 dashboard-page.tsx 里那段单 session 内联合并(约 55 行),抽出的
读模型与既有 src/data/chat.ts、chat-model.ts 同层,消费者仍是既有的
collaboration-card.tsx 与 return-delivery-status.tsx,没有新建第二套会话存储、状态权威或
调度器。同一个 2,700+ 行的页面模块因此不再承载这段合并规则,后续会话读取改动只需面对一个
可单测的模块。

具体改动

净变化 9 文件 +222/-50:生产代码新增 57 行读模型、页面模块净减少约 11 行(+28/-39),
新增 42 行单测与 63 行浏览器场景,其余是 RFC/路线图/golden query 文档与一条 npm script。

关键代码讲解

  1. conversation-returns.ts:12 conversationReturnSessions 是「该读哪些 session」的唯一判据:
    集合从活动 session 起,再加入仍欠结论(collaboration.returns 中没有 delivered 的
    conclusion)、仍欠投递(returnDelivery.status 不在 delivered/superseded)或仍欠
    transcript(有 sourceTurnId 但没有 sourceMessageId)的旧 session,最后排序返回。
    义务结算即退出轮询,避免历史 session 被永久读。
  2. conversation-returns.ts:27 reconcileConversationReturns 按 session 限定更新:
    seen 只取本 session 已见过的 sourceMessageId,byId/byTurn 也只用该 session 的快照,
    因此另一个 session 的同名 ID 不会互相覆盖(单测用 colliding IDs 固定了这条);
    只有 origin === "manager_followup" 的新行才会被追加,且返回原数组引用表示「没有变化」,
    让 React 状态更新可以被短路。它刻意保留本地 text/pending(流式文本不被快照替换),
    只做身份补水。
  3. conversation-returns.ts:44-47 把「投影缺失不等于撤回」写成显式规则:后端一时读不到
    collaboration 元数据时保留上一次观察到的回执与未结义务,直到有更新的观察到达。这是一次
    真实的行为变化(旧代码会把它清成 undefined),代码注释与正文都写明了理由。
  4. dashboard-page.tsx:1490 把轮询集合序列化成 conversationReturnSessionKey 作为 effect 的
    依赖:集合不变则流式增量不会重启 effect;1504 改为逐 session 调用共享读模型,
    1517-1523 为每个 session 维护独立的重试计时器,卸载时统一清理,失败只重试读取、
    绝不重放模型。
  5. dashboard-page.tsx:1692/2144/2180/2696 补上 sourceSessionId 等身份字段,让恢复中的
    回合、阶段回调、完成后的回答与纠偏回合都能被读模型归属到具体 session——这是新读模型
    能按 session 工作的前置条件。
  6. package.json:59 新增 test:conversation-returns,把读模型的单测接进既有 npm 脚本习惯。

对主干的风险

最强回归场景是「读一个 session 却改坏了另一个 session 的状态」,或迟到结果要么永远不出现、
要么重复出现。前者由按 session 限定的 seen/byId/byTurn 与新单测的 colliding-ID 用例覆盖;
后者由浏览器场景断言结果只渲染一次、交办卡只保留一份、且 turnRequests 数量不变(不重放
模型)覆盖。第二类风险是轮询面扩大:此前只读最新 session,现在会同时读仍有义务的旧 session,
请求数随未结义务数增长;这是有界的(义务结算即退出)但确实是对后端的额外读取,我把它记录为
已披露的代价而不是免费改进。

第三类风险由本次维护者修复直接暴露:主干的「distinguish recipient disposition from reply
delivery」把合作卡文案从「结论已回传」改成「回复已送达」,而新场景仍断言旧文案,于是该场景
在合并后必然超时。我按文案的当前所有者(collaboration-card.tsx 的 copy)改成「回复已送达」
并加注释说明它必须跟随该 copy,随后场景在开发与打包两种模式下都通过。顺带发现主干自身
chat-recovery.mjs:127 的否定断言仍在检查那个已被改名的字符串,因此它当前是空断言——
这是主干侧由改名引入的守卫缺口,不在本 PR 边界内,我在此如实记录而不顺手改动。

回滚与恢复:本 PR 只改前端读取与文档,回滚即恢复「只读最新 session、缺失即撤回」的旧行为;
没有持久化状态或权限影响。验证证据:npm run test:conversation-returns 通过;
npm run build:desktop(tsc + vite)通过;personal-workspace-browser-smoke 全 19 个场景在
development 模式下通过,新场景另在 packaged 模式通过;pytest manager_context 36 项通过;
docs governance 通过;loopx canary premerge --from-git-diff 报 status=passed、9 文件、
7 catalog canaries + 8 risk smokes + public boundary、0 manual holds。

语义与 CI 对齐

本 PR 把 RFC 的验收措辞与实现对齐:EN 版 RFC 新增的「Nearest user-visible exit」小节与中文版
既有小节现已等价,路线图的 App-first 段落保留 main 的新版计划并折入「迟到结果在活动会话被
替换后仍可见」,golden-queries 用 GQ02/GQ04/GQ08/GQ09 组合出同一条试点。也就是说,这条
可见性修复不是自证的:它对应 RFC 里已冻结的「刷新或更换会话后仍在」可观测出口。

我的整体评价

这是一条边界清楚的产品修复加一次合理的本地面板重构:它修的是「交办结果在会话被替换后看不见」
这个真实缺口,把重复的单 session 合并规则收敛成一个可单测的共享读模型,并在义务结算后停止
轮询;同时坦白披露了两处行为变化(投影缺失不再撤回回执、轮询面覆盖有义务的旧 session)。
维护者两次修复也记录在案:路线图冲突按 main 的新计划折入本 PR 的迟到结果条款,以及新场景
改断言当前文案「回复已送达」(旧文案已由 main 改名,保留它只会得到假红)。剩余风险是
main 侧 chat-recovery.mjs 的否定断言因同一次改名而失去意义,以及本 PR 未声明的整段旅程
资格,二者都不影响这次的修复边界。建议合入。

English verdict: APPROVE - 9521f65 makes the App conversation read its active session plus older
sessions that still owe a conclusion, delivery or transcript, through one session-scoped
TypeScript read model that deduplicates results, preserves streaming text and retires settled
sessions; the maintainer merge folded this PR's late-return clause into main's newer roadmap
paragraph and re-pointed the new scenario at the renamed delivered-reply copy (main's own
chat-recovery negative assertion is now vacuous and is reported, not repaired here). Unit test,
desktop/chat builds, all 19 browser scenarios in development mode, the new scenario in packaged
mode, manager-context pytest, docs governance and a premerge gate with 0 manual holds all pass.

The contract smoke greps dashboard-page.tsx for deliveryByMessage, which this
refactor removed when the single-session merge moved into
conversation-returns.ts. Assert the new owner instead: the page delegates to
reconcileConversationReturns, and the read model still refreshes delivery state
from source.return_delivery while keeping the last observation. Without this the
Frontstage Pages and Desktop 'Verify packaged Personal Workspace is current'
jobs fail on a retired implementation detail.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

PR #5170 复审 — exact head 5c31549226da2b6f9f23fb64b350b6e0689f27c4

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

动机

App 会话里交办出去的活可能在模型切换或活动会话被替换之后才完成。此前 App 只轮询当前最新
session,于是已经落盘的结果要等到手动刷新才可见;而且用一份单 session 快照重建消息列表时,
会把另一个 session 的交办元数据一起抹掉。本 PR 让会话读取覆盖「活动 session + 仍有未结
义务的旧 session」,把原来的内联合并抽成共享的 TypeScript 读模型:按 session 限定更新范围、
去重结果、保留正在流式输出的文本,并在义务结算后停止轮询旧 session。它不重放模型、不改执行
driver、不改持久化契约,方向与已接受的 App 会话 RFC(R3 与 GQ02–04)一致;同一次改动还把
EN 版 RFC 与中文版拉回对齐(中文版早已有该「最近可感知交付」小节)。

可观测的前后差异有真实场景支撑:新增的 conversation-return-continuity 浏览器场景在旧实现上
会一直等不到迟到结果,在候选实现上结果只显示一次且不产生新的模型回合。交付判断:这是一个
完整可回滚的切片——它修的是既有 App 会话路径上的可见性缺陷,不宣称已完成整段 managed/
attached 旅程或真实团队采纳,那些检查点仍然开放。

改动思路

入口是 App 个人工作区的会话视图;权威输入是后端返回的 session 快照(
/api/chat/sessions/<id>);决策边界被收敛到新模块
apps/presentation/dashboard/src/data/conversation-returns.ts:conversationReturnSessions
决定「要读哪些 session」,reconcileConversationReturns 决定「一个 session 的快照如何并入
本地列表」。副作用只发生在前端消息列表,不写后端、不触发回合。正路径是:活动 session 的
快照照旧合并;带未结义务的旧 session 也各自被读一次,把迟到结论接回原交办,义务结算后
该 session 从轮询集合中退出。

复用与归属:被删掉的正是 dashboard-page.tsx 里那段单 session 内联合并(约 55 行),抽出的
读模型与既有 src/data/chat.ts、chat-model.ts 同层,消费者仍是既有的
collaboration-card.tsx 与 return-delivery-status.tsx,没有新建第二套会话存储、状态权威或
调度器。同一个 2,700+ 行的页面模块因此不再承载这段合并规则,后续会话读取改动只需面对一个
可单测的模块。

具体改动

净变化 9 文件 +222/-50:生产代码新增 57 行读模型、页面模块净减少约 11 行(+28/-39),
新增 42 行单测与 63 行浏览器场景,其余是 RFC/路线图/golden query 文档与一条 npm script。

关键代码讲解

  1. conversation-returns.ts:12 conversationReturnSessions 是「该读哪些 session」的唯一判据:
    集合从活动 session 起,再加入仍欠结论(collaboration.returns 中没有 delivered 的
    conclusion)、仍欠投递(returnDelivery.status 不在 delivered/superseded)或仍欠
    transcript(有 sourceTurnId 但没有 sourceMessageId)的旧 session,最后排序返回。
    义务结算即退出轮询,避免历史 session 被永久读。
  2. conversation-returns.ts:27 reconcileConversationReturns 按 session 限定更新:
    seen 只取本 session 已见过的 sourceMessageId,byId/byTurn 也只用该 session 的快照,
    因此另一个 session 的同名 ID 不会互相覆盖(单测用 colliding IDs 固定了这条);
    只有 origin === "manager_followup" 的新行才会被追加,且返回原数组引用表示「没有变化」,
    让 React 状态更新可以被短路。它刻意保留本地 text/pending(流式文本不被快照替换),
    只做身份补水。
  3. conversation-returns.ts:44-47 把「投影缺失不等于撤回」写成显式规则:后端一时读不到
    collaboration 元数据时保留上一次观察到的回执与未结义务,直到有更新的观察到达。这是一次
    真实的行为变化(旧代码会把它清成 undefined),代码注释与正文都写明了理由。
  4. dashboard-page.tsx:1490 把轮询集合序列化成 conversationReturnSessionKey 作为 effect 的
    依赖:集合不变则流式增量不会重启 effect;1504 改为逐 session 调用共享读模型,
    1517-1523 为每个 session 维护独立的重试计时器,卸载时统一清理,失败只重试读取、
    绝不重放模型。
  5. dashboard-page.tsx:1692/2144/2180/2696 补上 sourceSessionId 等身份字段,让恢复中的
    回合、阶段回调、完成后的回答与纠偏回合都能被读模型归属到具体 session——这是新读模型
    能按 session 工作的前置条件。
  6. package.json:59 新增 test:conversation-returns,把读模型的单测接进既有 npm 脚本习惯。

对主干的风险

最强回归场景是「读一个 session 却改坏了另一个 session 的状态」,或迟到结果要么永远不出现、
要么重复出现。前者由按 session 限定的 seen/byId/byTurn 与新单测的 colliding-ID 用例覆盖;
后者由浏览器场景断言结果只渲染一次、交办卡只保留一份、且 turnRequests 数量不变(不重放
模型)覆盖。第二类风险是轮询面扩大:此前只读最新 session,现在会同时读仍有义务的旧 session,
请求数随未结义务数增长;这是有界的(义务结算即退出)但确实是对后端的额外读取,我把它记录为
已披露的代价而不是免费改进。

第三类风险由本次维护者修复直接暴露,而且是 CI 先发现的两处真实红灯:主干的「distinguish
recipient disposition from reply delivery」把合作卡文案从「结论已回传」改成「回复已送达」,
而新场景仍断言旧文案,于是该场景在合并后必然超时;同时
personal-workspace-contract.test.mjs 用源码正则钉住页面里的 deliveryByMessage 变量,而这次
重构正是把它删掉并搬进共享读模型,使 Frontstage Pages 与 Desktop Release Artifacts 的
「Verify packaged Personal Workspace is current」失败。两处都按契约的当前所有者修复:场景
断言改为合作卡 copy 的「回复已送达」并注明必须跟随该 copy;合约测试改为断言页面委托给
reconcileConversationReturns,并在读模型里断言 source.return_delivery ?? row.returnDelivery
这条刷新规则仍然存在——原来的行为意图保留,只是不再钉死实现细节。修复后我按 CI 的同一步骤
本地跑过:npm run build:chat、chat_bundle.py verify --source、
npm run smoke:personal-workspace-packaged(含 19 个场景)全部通过。顺带发现主干自身
chat-recovery.mjs:127 的否定断言仍在检查那个已被改名的字符串,因此它当前是空断言——
这是主干侧由改名引入的守卫缺口,不在本 PR 边界内,我在此如实记录而不顺手改动。

回滚与恢复:本 PR 只改前端读取与文档,回滚即恢复「只读最新 session、缺失即撤回」的旧行为;
没有持久化状态或权限影响。验证证据:npm run test:conversation-returns 通过;
npm run build:desktop(tsc + vite)通过;personal-workspace-browser-smoke 全 19 个场景在
development 模式下通过,新场景另在 packaged 模式通过;pytest manager_context 36 项通过;
docs governance 通过;loopx canary premerge --from-git-diff 报 status=passed、10 文件、
7 catalog canaries + 8 risk smokes + public boundary、0 manual holds。

语义与 CI 对齐

本 PR 把 RFC 的验收措辞与实现对齐:EN 版 RFC 新增的「Nearest user-visible exit」小节与中文版
既有小节现已等价,路线图的 App-first 段落保留 main 的新版计划并折入「迟到结果在活动会话被
替换后仍可见」,golden-queries 用 GQ02/GQ04/GQ08/GQ09 组合出同一条试点。也就是说,这条
可见性修复不是自证的:它对应 RFC 里已冻结的「刷新或更换会话后仍在」可观测出口。

我的整体评价

这是一条边界清楚的产品修复加一次合理的本地面板重构:它修的是「交办结果在会话被替换后看不见」
这个真实缺口,把重复的单 session 合并规则收敛成一个可单测的共享读模型,并在义务结算后停止
轮询;同时坦白披露了两处行为变化(投影缺失不再撤回回执、轮询面覆盖有义务的旧 session)。
维护者三次修复也记录在案:路线图冲突按 main 的新计划折入本 PR 的迟到结果条款;新场景改断言
当前文案「回复已送达」(旧文案已由 main 改名,保留它只会得到假红);以及把合约测试里钉死
deliveryByMessage 的源码断言改到新所有者(页面委托 + 读模型刷新规则),这正是 CI 在
Frontstage Pages 与 Desktop 上先报出来的红灯。剩余风险是 main 侧 chat-recovery.mjs 的否定
断言因同一次改名而失去意义,以及本 PR 未声明的整段旅程资格,二者都不影响这次的修复边界。
建议合入。

English verdict: APPROVE - 5c31549 makes the App conversation read its active session plus older
sessions that still owe a conclusion, delivery or transcript, through one session-scoped
TypeScript read model that deduplicates results, preserves streaming text and retires settled
sessions; the maintainer merge folded this PR's late-return clause into main's newer roadmap
paragraph, re-pointed the new scenario at the renamed delivered-reply copy, and moved the
return-polling contract assertion off the deleted deliveryByMessage variable onto the shared read
model (the two CI reds this PR caused; main's own chat-recovery negative assertion is now vacuous
and is reported, not repaired here). Unit test, desktop/chat builds, the chat-bundle source
verification, all 19 browser scenarios in development and packaged modes, manager-context pytest,
docs governance and a premerge gate with 0 manual holds all pass.

@huangruiteng
huangruiteng merged commit 473263c into main Sep 27, 2026
50 of 61 checks passed
@huangruiteng
huangruiteng deleted the codex/app-delegation-return branch September 27, 2026 14:41
@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Merged as 473263cbfd7d877e058941c9a770de4c7953d3bd (second parent = reviewed head 5c31549226da2b6f9f23fb64b350b6e0689f27c4).

  • Exact-head review: fix(app): keep delegated results visible across session changes #5170 (review) (author-owned PR, so the record is a COMMENTED review carrying the approval conclusion and an English verdict; an earlier, invalid one on the same head is superseded).
  • Maintainer repairs on the head (three signed commits on top of the author's 2d23e810f):
    1. Conflict repair. docs/architecture/rfcs/loopx-overall-roadmap-v0.md conflicted because main rewrote the App-first paragraph around the concrete pilot and exits. Kept main's newer plan text and folded this PR's clause in: readable results "including late returns after the active session changes". No runtime file conflicted.
    2. Retired copy in the new scenario. The scenario asserted 结论已回传, but main's "distinguish recipient disposition from reply delivery" renamed the collaboration-card copy, so a delivered conclusion now reads 回复已送达 / Reply delivered. Unrepaired, the scenario waits 30s and fails; it now asserts the current owner copy and says so in a comment. Verified in development and packaged browser modes.
    3. Contract smoke on a retired implementation detail. personal-workspace-contract.test.mjs greps dashboard-page.tsx for deliveryByMessage, the variable this refactor deletes. Frontstage Pages and Desktop Release Artifacts both failed on it ("Verify packaged Personal Workspace is current"). It now asserts the page delegates to reconcileConversationReturns and that the read model still refreshes delivery state (source.return_delivery ?? row.returnDelivery), so the original behavior intent is kept without pinning implementation.
  • Validation at the merged head: npm run test:conversation-returns; npm run build:desktop (tsc + vite); npm run build:chat + python scripts/chat_bundle.py verify --source + the full smoke:personal-workspace-packaged chain (all 19 browser scenarios, matching the CI step that was red); the 19-scenario development run; pytest tests/test_manager_context_roundtrip.py tests/test_manager_context_tracking.py (36 passed); examples/docs-governance-smoke.py; and loopx canary premerge --from-git-diff (status=passed, 10 files, 7 catalog canaries + 8 risk smokes + public boundary, 0 manual holds). check-merge-readiness returned ready=true.
  • CI on this exact head after one rerun: every job green except test-shard (2) and test-shard (3), which fail on test_project_registry_io_census.py::test_checked_in_project_registry_io_manifest_is_current and test_goal_instance_binding_inventory.py::test_goal_instance_inventory_does_not_replace_the_registry_io_census (unregistered project registry I/O sites). Those reproduce on the current main head itself (its own Python Tests run is red for the same reason), so they are a pre-existing main-side gap and not this PR's regression; the two earlier flakes on this head (typescript-core (3/3) host-process timeout and test-shard (4) effect-runtime startup) both passed on rerun, and the host-process one also reproduces on main.
  • Admin bypass was required by the protect main ruleset (and by the pre-existing main reds above). The reviewed head is unchanged from the one that was approved.

Two known gaps are recorded rather than silently patched here: main needs an owning change to refresh the stale project_registry_io_manifest_v1.json / goal_instance_binding_inventory_v1.json, and examples/personal-workspace-browser/chat-recovery.mjs:127 still negates the renamed 结论已回传, which makes that negative assertion vacuous.

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

Labels

needs-rebase Mergify: the pull request has merge conflicts with its base branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant