Skip to content

fix(chat): recover interrupted App turn acceptance - #5139

Open
Duang777 wants to merge 5 commits into
loopx-project:mainfrom
Duang777:codex/app-continuity-ts-next
Open

Duang777 wants to merge 5 commits into
loopx-project:mainfrom
Duang777:codex/app-continuity-ts-next

Conversation

@Duang777

@Duang777 Duang777 commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

  • Goal/source and gap: Continue the App-first conversation work from fix(app): preserve conversational intent and plan App-first continuity #5064 by making managed Chat turn acceptance recoverable when the process or response fails between durable writes.
  • Observable before -> after, with the validation row that proves it: An accepted request could leave a Turn without its transcript or queued event and exact retry could not recover it. The TypeScript acceptance planner now validates the durable prefix, repairs only the missing writes, and separates acceptance from dispatch. The App retries transport, 5xx, and transient adapter-resume failures once with the same client_turn_id.
  • Issue/task and intended base: Related to fix(app): preserve conversational intent and plan App-first continuity #5064. Base: main.

Scope And Continuation

  • Completed scope and remaining work: Complete within this scope. Managed Chat acceptance, same-process dispatch deduplication, restart handling, JSONL tail repair, and App retry behavior are covered. Attached Chat routing and collaboration activation are unchanged.
  • Slice boundary / successor: No successor is required for this acceptance-recovery slice.

Validation

  • Tested revision: 97dd19b3f7f871092ed50727b3abaf9d20f2bcea
  • Run state: finished
  • Input classes: synthetic, public_fixture
Check kind Result Public-safe evidence / limitation
unit passed npm run test:control-plane: 3175 tests, 3145 passed, 30 skipped, 0 failed on the equivalent patch before a governance-only main rebase; the exact head's 19 acceptance reducer tests also passed.
unit passed Focused Chat, LoopX, attached-session, manager, handoff, and collaboration Python suite: 180 passed.
static passed npm run typecheck:control-plane, changed-file Ruff, and git diff --check.
real_entrypoint passed Dashboard desktop production build and npm run smoke:chat-turn-acceptance-retry.
integration passed scripts/loopx canary premerge --from-git-diff --tier standard --no-progress: 18 selected checks passed with no warnings or manual holds.
static passed Docs governance and control-plane maintainability ratchet.
regression_parity failed examples/repository-hygiene-smoke.py reports the missing v1.2.0 release-timeline entry on both this revision and an isolated origin/main checkout; this diff does not touch that surface.
  • Coverage and gaps: Fault injection covers every acceptance write boundary, incomplete JSONL tails, prepared-Turn resume, unowned starting recovery, completion publication, same-key concurrency, response-body loss, HTTP 424/5xx retry, and 409 non-retry. No UI layout changed, so visual browser evidence is not applicable.

See validation disclosure guidance.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • States and viewports shown: N/A
  • Source data: synthetic
  • Attention review: N/A; this changes the data transport and recovery path only.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

Shared-authority RFC fixture impact

  • Production-scale fixture schema: unchanged.
  • Semantic dimensions changed, or reviewed no-impact rationale: No shared Goal authority dimensions changed. The new TypeScript owner is limited to Chat request identity, durable-prefix repair, replay conflict precedence, and dispatch classification.
  • Provider conformance arms run: Not applicable; this path uses the owner-local Chat store.
  • Read-only legacy/file/PostgreSQL three-arm rehearsal: Not applicable; no authority-provider routing or compatibility projection changed.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

@Duang777
Duang777 force-pushed the codex/app-continuity-ts-next branch 3 times, most recently from d0da692 to 9fb2927 Compare September 26, 2026 19:08

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English verdict: REQUEST_CHANGES

Reviewed head: 9fb2927f56ead0a0edca8c903d02c13755464895;immutable merge base: 39f2fd15e45987a01769bd9bd5a4da42e89e12d9。发现两处可复现的 P2 回归,结论来自真实存储/控制器的 base/head 对照,不来自远端 CI 颜色。

动机

App 在接受请求过程中断开或崩溃后,重试应恢复同一个逻辑请求,而不是复制用户消息或重新启动已终结的 Turn。这个范围是有用、完整且可单独验证的交付边界;不需要把整个 App 异步会话 RFC 都完成才交付。但当前 head 会在取消后阻止下一轮,并在 home 身份拒绝前写入 Turn,因此长期持续推进和用户体验均存在回归,尚不能证明这一个恢复切片完成。

改动思路

浏览器数据入口重用同一个 client_turn_id 和请求 body,有限重试;Python 在真实文件锁内采集完整 Turn/消息/事件事实,调用新增 TypeScript planner,再按 prepare Turn → active Session → 用户消息 → queued event → settle capsule 的顺序落盘。planner 决定恢复写入与 dispatch,Python 保留存储和 adapter 副作用权。

临时 _acceptance capsule 保存原始展示文本、附件和身份,不只是可删的投影:第一次消息落盘前,仅凭旧 Turn 不能恢复这份意图。现有 ingress receipt 属于另一种待处理输入,Goal settlement journal 也不属于 Chat Session,不能直接冒充这里的恢复来源。不过 capsule 的退休必须与已有取消/终结状态共同建模;仅凭字段存在就认定“仍待接受”产生了下面的真实错误。

具体改动

完整差异 12 个文件,+2697/-137:三个 App 数据/构建/smoke 文件,六个 Python/TypeScript runtime 文件,两个测试文件及 TypeScript 构建入口。不是 coverage-only:生产 HTTP submit、Session 恢复和 App 发送路径都会调用它。没有新权限或新可选 capability;现有 managed Chat 默认获得恢复和一次自动重试,attached-host 的入队边界仍独立保留。

关键代码讲解

  • planChatTurnAcceptance / planNewAcceptance:类型化事实负责同 key 重放、前缀补齐和 dispatch。新请求的 521–522 行 把任何 prepared capsule 作为 active conflict,却未排除已经 interrupted 的旧 Turn。
  • accept_managed_turn:锁内读取真实全量存储、应用 planner 写入序列;旧请求不凭分页列表或 UI 摘要恢复。消息与 queued event 的精确一次逻辑由这个路径承载。
  • submit_turn:先接受请求再准备 adapter,可以保留可重试的接受结果;但现有 home 身份检查仍在后面的 adapter 准备路径,非可重试拒绝也被推迟到副作用之后。
  • _repair_incomplete_jsonl_tail:追加前修补未完成 JSONL 尾部;它也影响共享消息/事件追加路径,已核对 event buffer、cursor 和 retention 等原生测试,而不只测新 planner。
  • acceptChatTurn:稳定 body/key 加一次有限重试,区分传输/响应体丢失、503、424 resume_failed 与不可重试 409,并保留 AbortSignal。compiled data-path smoke 通过,不等于完整浏览器用户旅程通过。

对主干的风险

  1. [P2] 取消已部分接受的 Turn 后,残留 capsule 永久拦住下一轮。 在真实 append_event(kind="turn.queued") 已写入 turn.queued 后注入 OSError、重新打开 Store,再调用 controller 的 interrupt_turn:旧 Turn 变成 interrupted,Session 为 ready、active_turn_id 为 null,但 capsule 仍在。不同 key 的下一轮在 base 正常创建,在 head 抛出指向旧终结 Turn 的 active conflict;原 key 在 base 重放 interrupted,在 head 报 acceptance state inconsistent。健康取消路径和先完成恢复再取消路径均通过,说明问题是持久化前缀与取消交叉,而非普通取消。最小修复应在既有锁和类型化状态边界退休/收束终结 capsule,保留终结重放且不 redispatch;不要简单忽略所有 capsule 或放宽身份冲突。补充“写入各前缀后取消 → 同 key 重放/新 key 继续”的真实存储回归测试。

  2. [P2] home 不匹配在拒绝前已经消费 Session。 原生 test_restart_home_mismatch_refuses_before_resume_or_turn_write 在 immutable base 通过,在 head 失败。错误仍是 codex_home_mismatch,也没有启动上游 adapter;但 Session 已由 ready 变 busy、active Turn 已写入,消息/事件也已落盘。相同错误码不代表无副作用拒绝契约仍成立。应在 durable acceptance 前验证捕获的 Session/host 身份和不可重试 admission 条件,同时保留 transient adapter failure 的接受后恢复。重跑该原生测试及 accepted adapter-start retry 测试。

语义与 CI 对齐

本 PR 扩展现有 Chat 请求接受语义,不创建更广的 Agent/Goal authority。对齐来源是已有 home 拒绝测试、取消后可继续的生产路径,以及当前 App RFC 的 durable accept/dispatch/correction 边界;没有把后续 RFC 展望升级为新审批门槛。typed-state 缺口是将 capsule 字段存在误分类为可恢复 queued 请求;修复应留在现有 TS 判定 owner,而不是新增文本指令。Chat 专用错误名与领域匹配,写入/dispatch 是机器执行后果,不是 guidance。

本地验证保留 passed/failed/untested 区分:

  • 新 TS acceptance 19/19;Python active-turn 47/47。另十个相关 Python 文件 59 passed / 1 failed,唯一失败为上述 home 回归;独立故障探针验证上述取消反例。
  • control-plane typecheck、改动 Python Ruff、App retry smoke、desktop build、diff whitespace 和 12 文件 public-boundary 扫描均通过。标准 premerge 的 18 个选定检查及 5 个直接检查通过,但不能推翻负例。
  • 全量 TS 为 3156 passed / 1 failed / 30 skipped。失败为未改动路径上的 NoKV “native/done current eligibility supersedes history”,isolated base/head 同一个 case 均通过,因此本次不能确认其因果或宣称既有故障;单独记录为 unresolved,不把它包装成第三个 PR 代码缺陷。30 个跳过属于无隔离 PostgreSQL 配置的可选 suite,本次 Chat 文件存储改动不触及该 authority backend。未查询或等待远端 CI。

我的整体评价

REQUEST_CHANGES。问题本身值得解决,原始意图 capsule 和单一 TS 判定边界有必要;量级与完整恢复链路相称,不能按新增行数机械否决。面向后续改动的 bounded refactor 建议是集中“可恢复 queued / 已终结 capsule”的类型分类与退休,不扩张为另一套通用 journal。旧持久 Turn/无 capsule 重放仍有真实兼容消费者,不能为减代码删除。

长期推进与用户体验判断都为 regression:用户取消后看到 ready 却不能继续,以及身份拒绝留下 busy 请求,均抵消正常重试的收益。请先修复两条现有契约并补实存储交叉负例;再复核全量 NoKV 失败,保留其独立归因。完整浏览器交互、真正进程强杀和跨机器恢复本次未验证,不能以 planner 单测或 build 代替。head 不变时上述结论有效,后续改动需要重新核对 exact head。

if (facts.preparedTurn.kind === "ambiguous") {
return rejected("durable_state_conflict");
}
if (facts.preparedTurn.kind === "single") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Retire terminal acceptance capsules before rejecting a new Turn

在真实 Store 的 append_event(kind="turn.queued") 已落盘后注入 OSError,再重开 Store 并调用 interrupt_turn:旧 Turn 已 interrupted,Session ready 且 active_turn_id=null,但 _acceptance 仍存在。这里把它直接判为 active_turn_conflict,导致下一条不同 client_turn_id 的请求永久被旧终结 Turn 拦住;同 key 重放又被 planPreparedAcceptance 的 queued-only 检查拒绝。immutable base 下相同新 key/终结重放都通过,head 下都失败。请在现有锁和 typed acceptance owner 内收束/退休终结 capsule,保留终结重放且不 redispatch,并补充各 durable prefix 后取消 × 新/旧 key 的真实存储回归覆盖;不能简单忽略所有 capsule 来放宽冲突边界。

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.

Fixed at exact head 0a6ac76f7. interrupt_turn now serializes with submit/resume, completes the original durable acceptance from its capsule before the terminal transition, and never dispatches that repaired Turn. The TS planner models terminal capsules explicitly: an exact retry validates and repairs the durable prefix, retires the capsule, and returns dispatch=terminal; terminal capsules are excluded from the live prepared-Turn conflict census, so a new key can proceed. Added real-store coverage for all five write boundaries, each with direct retry and interrupt-before-retry, asserting same-key terminal replay has no dispatch and a new key is accepted. Focused results: Python 10/10 prefix cases; TS acceptance 22/22; all Chat Python 430/430.

Comment thread loopx/chat_runtime.py Outdated
session = self.loopx_mode.activate_tools(session, work_dir=work_dir, objective=objective)
adapter = self._ensure_adapter_locked(
session,
accepted = self.store.accept_managed_turn(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Validate permanent home admission before durable acceptance

submit_turn 现在先调用 accept_managed_turn,再由 _ensure_adapter_locked 执行既有 _check_codex_home。当 Session 记录的 Codex home 与控制器不同,错误仍为 codex_home_mismatch,也没有启动 adapter,但 Turn/消息/事件已经写入,Session 已变 busy 并持有 active_turn_id。原生 test_restart_home_mismatch_refuses_before_resume_or_turn_write 在本 PR 的 immutable base 通过、head 失败,证明原本拒绝前无副作用的契约改变。请把 captured Session/home 等不可重试 admission 验证放在 durable acceptance 前,同时保留 transient adapter failure 的接受后恢复;重跑该测试及 accepted adapter-start retry 测试。

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.

Fixed at exact head 0a6ac76f7. submit_turn now reloads the Session while holding _session_adapter_lock and runs _check_codex_home before LoopX tool activation or accept_managed_turn, preserving a side-effect-free permanent identity rejection while leaving transient adapter startup failures after durable acceptance. The native test_restart_home_mismatch_refuses_before_resume_or_turn_write passes, as do the accepted adapter-start retry coverage and the full 430-test Chat Python set.

@Duang777

Copy link
Copy Markdown
Collaborator Author

Updated exact head: 0b928873543eb34edda0c092a286228943c88fe9.

  • Rebased both commits without patch drift onto main@af3e7f1f0 (feat(runtime): fence first-party Host state by GoalRef #5141); range-diff is exact.
  • Fixed both requested P2s: terminal acceptance capsules are repaired/retired before interrupt and never redispatched; Codex home admission now rejects before any durable acceptance or tool activation.
  • Preserved the rebase-composed JSONL contract (read_bytes with exact LF framing) and extended the completion event protocol so queue-preparation failure is not observed before Session release.

Exact-head local evidence:

  • Chat Python: 430 passed (1 existing zip duplicate-name warning)
  • TypeScript control plane: 3254 passed, 30 optional PostgreSQL skips, 0 failed
  • acceptance planner: 22/22; durable-prefix matrix: 10/10
  • typecheck, mypy, CI-scope Ruff, App retry smoke: passed
  • premerge: 18/18 selected checks, no holds
  • git diff --check: passed

loopx check still reports the two credential-shape fixture literals at tests/control_plane/test_public_safety_credential_shape_owner.py:51,54; that file is unchanged from main and outside this PR diff. No merge action taken; requesting exact-head re-review.

@Duang777
Duang777 force-pushed the codex/app-continuity-ts-next branch from 0b92887 to e8aec33 Compare September 27, 2026 04:44
@Duang777

Copy link
Copy Markdown
Collaborator Author

Main advanced again while CI was queued. Rebased without patch drift onto main@70b3cca010 (#5154); new exact head is e8aec3379ab77f5f3d169d3a4149459f6b21a088. The intervening main change is reward-memory-only apart from the shared effect-handler registry. Exact-head reruns passed: acceptance TS 22/22, focused Python 65/65, typecheck, mypy, CI-scope Ruff, App retry smoke, diff check, and premerge 18/18. Prior full Chat 430/430 and full TS 3254/30 skip remain patch-equivalent by exact range-diff.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

动机

本次重新审阅 exact head e8aec3379ab77f5f3d169d3a4149459f6b21a088,没有沿用上一次的判断。我以 #5064 的 App-first 交付方向及 App conversation RFC 的「accept/retry/restart 回到同一逻辑 Turn」来判断这个修复,而不是要求它完成全部会话平台。旧实现先写多个文件再返回响应,中断会留下 busy/queued 状态且丢失原请求;用户侧需要手工清理才能继续。这个 PR 在持久恢复上确有改善,但实际 App 入口的中途 I/O 故障仍不能自动走进新恢复路径,所声明的完整恢复切片还没有闭合。

改动思路

决策集中在 TypeScript chat.turn.accept planner:Python 从 canonical Turn、Session、完整 transcript/events 收集事实并执行计划,不把 UI 有限视图当作全量状态。新的 private acceptance capsule 先保存不可从缺失 transcript 推导的原请求意图,再有序完成 busy ownership、稳定 user message、queued event,最后退役 capsule。这样恢复依靠同一 key 和真实 durable facts,不是仅在内存里 rollback。

正向路径是「App 固定 body/key 的最多两次 POST → HTTP → runtime admission → store 排他锁 → typed planner → 五个持久步骤 → dispatch」。已有 terminal、冲突和 home admission 边界需要继续拒绝,不能为恢复而放宽权限。新 head 对上次的 capsule 取消问题和 home mismatch 先后顺序都补了测试,我分别验证通过;本轮的新阻塞发生在错误分类与实际 App retry 的衔接处。

具体改动

关键代码讲解

  • apps/presentation/dashboard/src/data/chat.ts:794 的 acceptChatTurn 在循环外序列化 body,复用 key、attachments 和 abort signal。:830–838 只重试 transport unavailable、5xx、424/resume_failed,409 和一般 400 不重试;这个边界本身合理,但必须和服务端可恢复故障分类一致。
  • loopx/chat_store.py:747 的 accept_managed_turn 在真实文件锁内读取 canonical facts,交给 planner,再执行 durable prefix repair。user message ID 固定,complete queued append 可识别,不会在相同请求重放时重新制造一条消息。JSONL tail 修复区分完整未带 LF 的合法记录和不完整尾部。
  • loopx/control_plane/turn_driver/chat_turn_acceptance.ts:711 的 planChatTurnAcceptance 统一新接受、prepared 恢复、已 settled 重放等决策,冲突/损坏拒绝而不是猜意图。terminal capsule 修复后退役,不重新取得 active ownership,也不重新 dispatch。
  • loopx/chat_runtime.py:884 的 submit_turn 将 home admission 放到接受写入之前;接受与 worker dispatch 分开,resume 暂时失败不应抹掉已接受 Turn。done event 也覆盖 queue preparation failure,避免 wait 先返回 terminal 而 Session 尚未释放。
  • loopx/chat_server.py:659 的 _session_turn 是实际 HTTP 错误 owner。其通用异常分支仍调用默认 400 的 _send_error;它与新的 App retry 规则组合,正是下面的缺口。

13 文件,2994 additions / 177 deletions;其中 TS planner 743 行、Python bridge 315 行,另有大幅场景覆盖。这个量级不是单纯换一个默认值,但多文件写入无法只靠进程内 rollback 可靠恢复;capsule 的原始意图在 transcript 尚未落盘时确实不可推导。未来改动友好性检查认为 typed planner 的集中所有权有价值,当前最有价值的 bounded companion 是补齐 HTTP 错误合同和真实 App 集成回归,不是再加一套恢复框架。

对主干的风险

[P2,阻塞] 持久接受过程中可恢复的 I/O 异常被服务端编码为 400,实际 App 因而不重试,留下不能直接继续的 busy/queued Turn。定位到新增 retry 分支 chat.ts:830–838,并沿未改动的 HTTP 通用异常分支确认因果链。

我使用构建出的真实 acceptChatTurn、真实 HTTP handler/runtime/file store 和真实 TS planner,注入一次「先真正追加 turn.queued,再抛 OSError」:App 请求状态只有 [400],结果为失败;store 已 busy/queued,capsule 还在,user transcript 一条,worker 启动零次。同 key 的手动重试得到 202 / created=false / 同一 Turn,而普通新 key 得到 409。现有 UI 每次 sendChatTurnStreaming 调用会生成新的 key,接受未返回时也没有已接受 Turn 可供 UI 正常续接,因此这不是后端“能手动恢复”就足够的用户旅程。

最小修复是在 HTTP owner 把接受持久化/可恢复 runtime I/O 分类成可重试的 5xx 或等价 typed recovery response,再让现有有界同 key retry 接上。不要 blanket 重试所有 400,更不要放开 409、validation 或 home admission。仅把该合成 I/O 分类成 503 的受控对照中,同一个真实 App 自动产生 [503,202] 并返回 created=false,说明该修复边界可行;这不是已实现的修复。请增加 App↔实际 HTTP/store 集成回归,至少覆盖 transcript 写入前与 queued append 后,并断言一个 Turn、一条 user message、一个 queued event、一次 dispatch,以及 invalid/409/home refusal 仍不扩大重试或写入。

基线/当前 head 同样存在通用 400 分类,所以我没有把它说成新引入的主干回归;它是当前 PR 明确承诺的 App 恢复结果尚未完成。前缀 transcript 故障对照也证明有效增量:immutable base 手动同 key retry 返回 original request unavailable,当前 head 返回同一 Turn 的 202,但 App 自动路径仍缺错误分类。

语义与 CI 对齐

本地 focused Chat suite 63 passed;完整 TS suite 3256 passed、30 个可选 PostgreSQL skip;typecheck、App retry smoke、packaged dashboard build、CI 范围 ruff/mypy 通过。风险 canary 为 5 direct checks 加 18 selected checks,全部通过。没有查询远端 CI,也没有拿非本 PR 造成的红 CI 发 Request changes。

完整 Chat 诊断首次为 408 passed / 2 failed,两项失败发生在 Chat 创建前的 delegation acceptance revalidation;同一 immutable base 的 targeted suite 2 passed,head targeted 为 1 failed / 1 passed,再串行重跑 head file case 通过。不能因此标成“基线已有无关失败”,也不能宣称完整 Chat suite 全绿;归因仍未彻底解决。我的阻塞结论依赖上面单独复现的真实入口 I/O 问题,不依赖这两个间歇诊断。typed capsule、精确 identity/状态判断、domain-neutral error 和 authority 排除都已核对;本 PR 不授予 Goal/quota/lease 或 attached driver 的新权限。

我的整体评价

持久前缀恢复、terminal capsule 退役和 home admission 的修正值得保留,也没有发现需要另外引入通用框架的理由。可是 long-horizon 的「故障后继续有用工作」和 user experience 的「由实际 App 入口自动恢复」在一个可恢复故障上仍未实现,不能把后端成功与 mocked fetch smoke 当作完整交付。请补 HTTP 错误边界及真实 App 集成验证后重新审阅,同时保留上述完整 Chat suite 的 passed/failed 区别。这个评审覆盖整个当前 head,而不是只检查上次两条评论。

English verdict: REQUEST_CHANGES - Durable prefix recovery is improved, but recoverable acceptance I/O failures still return HTTP 400 and strand the actual App caller instead of reaching its same-key retry path.

@Duang777
Duang777 force-pushed the codex/app-continuity-ts-next branch from e8aec33 to 8e61631 Compare September 27, 2026 11:55
@Duang777

Copy link
Copy Markdown
Collaborator Author

Updated exact head: 8e616319fd82a489cc59f06e7d381940dbcfc617, based on main@d23f1c87d05dce02ed6368ff91ff8b2d0c163213.

The blocking App/HTTP retry gap is now closed:

  • accept_managed_turn OSErrors become a dedicated runtime error. The HTTP owner returns 503, error_code=chat_turn_acceptance_unavailable, and turn_replay_safe=true without exposing the underlying I/O detail.
  • The compiled App acceptChatTurn now runs against the real HTTP handler, runtime, file store, and TypeScript planner for failures before transcript append and after durable turn.queued append. Both cases observe [503, 202] with the same key/body, then one Turn, one user message, one queued event, one dispatch, and a retired capsule.
  • Validation 400, active-turn 409, and Codex-home 424 remain single-attempt refusals and do not add durable records. The real smoke now runs in the chat-bundle CI job.

New DCO-signed commits:

  • 23afc69f3 fix(chat): classify acceptance write failures
  • 8e616319f test(chat): cover HTTP acceptance recovery

Exact-tree evidence:

  • real App/HTTP acceptance smoke: passed
  • relevant Python: 527 passed
  • TypeScript: 3310 passed, 30 optional PostgreSQL skips
  • typecheck, mypy, CI-scope Ruff, dashboard desktop build, and premerge 19/19: passed
  • git diff --check: passed

The targeted manifest suite remains 7 passed / 2 failed on the same eight pre-existing validation findings reproduced on main@d23f1c87d; this patch only updates the three shifted chat_server.py line entries. Requesting exact-head re-review. No merge action taken.

@Duang777

Copy link
Copy Markdown
Collaborator Author

Exact-head CI is complete for 8e616319f: 29 passed, 6 skipped. The real chat-bundle recovery smoke, all three TypeScript shards, static checks, dashboard acceptance, Windows, Stage2c, DCO, desktop artifacts, and provider checks passed.

The four red checks reduce to two pre-existing manifest tests plus their aggregates:

  • test-shard (1): test_goal_instance_inventory_does_not_replace_the_registry_io_census
  • test-shard (3): test_checked_in_project_registry_io_manifest_is_current
  • pytest and merge-gate: downstream failures from those shards

Both tests report the same eight registry-I/O findings as the main@d23f1c87d push run: https://github.com/loopx-project/loopx/actions/runs/36314490721. Current PR run: https://github.com/loopx-project/loopx/actions/runs/36317313733. No additional PR-side fix is warranted for that unrelated baseline drift.

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
@Duang777
Duang777 force-pushed the codex/app-continuity-ts-next branch from 8e61631 to 8d64afd Compare September 27, 2026 12:39
@Duang777

Copy link
Copy Markdown
Collaborator Author

Main advanced to 9eaacfbf2 via #5168. I rebased the four PR commits without patch drift; git range-diff is exact. New exact head: 8d64afd8adc8cdbb8510d29a559b395d76516161.

Post-rebase checks passed:

  • real App/HTTP acceptance recovery smoke
  • focused Python: 156 passed
  • CI-scope Ruff
  • git diff --check

The intervening main commit only changes issue-fix metrics vocabulary files and does not overlap this Chat patch. CI has been retriggered. No merge action taken.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Exact reviewed head: 8d64afd8adc8cdbb8510d29a559b395d76516161; immutable baseline: 9eaacfbf2ff93d5386cee82ddf0847d9c739e78a.

发布前发现 head 更新后,我核对了两段 commit range:四个 PR 提交的 patch 全部相同,是无差异 rebase;随后在上述新 head/base 重新执行了 Python、TS、真实编译 HTTP smoke、故障恢复 oracle 和静态检查。下文结论绑定新 head,没有沿用旧 head 的验证身份。

动机

评审依据是 #5064 和 App conversation RFC 的 Acceptance / Dispatch / Managed 行:响应丢失或接受过程中断后,同一个请求必须能恢复,不能产生第二次执行,也不能永久卡在 busy。本次不是只看上轮修正;我重新检查了完整 PR。最新两个提交补齐了真实 HTTP 错误分类和 App 调用,属于有实际用户收益的完整恢复切片,但不等于整个异步 inbox 或 attached-host 生命周期已经交付。

改动思路

App 继续发送原来的 scoped client_turn_id 和相同请求体;HTTP、runtime、file store 再经过 typed TS planner 判定接受、恢复或冲突。Session/Turn/原始 transcript/event 是事实来源,Python 负责读取和执行计划,TS 拥有决策;没有新增 Todo authority 或通用队列。短暂 _acceptance capsule 保留尚未完整写入的原始请求意图,完成各持久步骤后退役,不能用一份缺失的 transcript 猜测用户请求。

真实 smoke 在 transcript 写入前和 queued event 已落盘后各注入一次 OSError,实际编译的 App 调用得到 [503, 202],独立回读只有一个 Turn、一条用户消息、一个 queued event 和一次 dispatch。上轮的“HTTP 400 使 App 不重试”问题在这个 head 已修正;不能沿用旧 blocker。

具体改动

关键代码讲解

  1. planChatTurnAcceptance(loopx/control_plane/turn_driver/chat_turn_acceptance.ts:711)区分 prepared、settled、运行和终态,校验原始意图以及 transcript/event 前缀;冲突不会转成新执行。新的 typed owner 由 chat.turn.accept 真正调用,不是测试专用框架。
  2. ChatSessionStore.accept_managed_turn(loopx/chat_store.py:747)在 session 锁内读取完整来源、逐步持久化,并以稳定消息标识修复合法前缀;JSONL 修复只处理不完整尾部,保留完整末条和 Unicode。终态 capsule 的清理不占住下一 Turn,也不重新 dispatch。
  3. ChatRuntimeController.submit_turn(loopx/chat_runtime.py:889)先做 home admission,再接受请求、恢复 adapter 和启动唯一 worker;adapter 启动失败仍保留 queued 请求。队列准备失败会真实结算,完成/中断不释放后继 Turn 的 ownership。HTTP 将接受写入 OSError 映射为可重放的 typed 503,隐藏底层 IO 细节。
  4. acceptChatTurn(apps/presentation/dashboard/src/data/chat.ts:794)最多两次、原请求体重试;网络/响应体中断、服务不可用以及已接受请求的 resume failure 可恢复,validation 400、冲突 409、home 424 不会盲目重试。新增 HTTP fixture、编译 smoke、CI job 和 Python/TS 回归测试覆盖该链路;manifest 仅调整三处行号,tsconfig 纳入新 owner。

对主干的风险

[P2] 新 smoke 再次绕过统一的 Python 发现机制

位置:smoke 的解释器选择第 87–89 行。它自行选择 LOOPX_PYTHON、POSIX venv 或 bare python3,不消费仓库已经支持的 LOOPX_TEST_PYTHON,也不检查版本。实际给定有效的测试 Python 3.13 和旧的 legacy Python 3.9 时,resolveTestPython 正确选择 3.13,但这个编译 smoke 启动 3.9 并退出,报 acceptance HTTP fixture exited before returning a result。这不是 CI 颜色,也不是推测的兼容问题。

最小修复是直接复用 scripts/test-python.mjs 的 resolveTestPython({repoRoot: repositoryRoot}),删除本地 discovery。现有 guard 在这个 head 仍通过,因为只识别直接启动/赋值和 ?? "python3",漏掉了括号内 ternary;请给 guard 加该形态的反例,并测试 test override 对 legacy override 的优先级,避免下一次又漏过。复验统一 guard 和 npm run smoke:chat-turn-acceptance-retry,包括这个有效 test override 场景。

语义与 CI 对齐

独立验证:相关 Python 234 passed;acceptance planner 与 Python guard 27 passed;真实编译 App/HTTP smoke 在正确解释器下通过;control-plane/dashboard typecheck、mypy、相关 Ruff、diff check 通过。同一真实 store 故障/retry oracle 在基线因 original request unavailable 失败,在 head 恢复为一个 message/event,证明测试对历史缺陷敏感。HTTP validation/conflict/home 的无副作用拒绝也已验证。

按照当前 review 配置没有查询或等待远端 CI。registry-I/O census 在相同依赖准备后的 base/head 都是 5 passed / 1 failed,失败用例相同,直接 validator 回读的八项诊断完整相等;这处既有漂移不作为本 PR 的 blocker。真实上游模型/attached host、完整跨平台和长期并发 soak 未在本次独立验证中运行,不能拿这个 synthetic upstream fixture 宣称它们合格。

我的整体评价

REQUEST_CHANGES。核心恢复链路改善了持续工作和 App 用户体验,原始意图 capsule 的持久兼容有必要;743 行 planner 和 Python IO bridge 必须保持单一规则 owner,后续不应再复制接受状态机。我的 bounded future-facing pass 的具体建议是这次就收回 smoke 的重复 discovery,并补 guard 的漏检,而不是扩展新框架或重新实现整个恢复方案。新的开发验证入口仍有可复现的体验回归,先修这个小而明确的缺口即可复审;不要求这份 PR 修无关主干问题,不建议合并或 bypass。

English verdict: REQUEST_CHANGES - head 8d64afd. The real App/HTTP acceptance recovery now passes, but the new smoke bypasses resolveTestPython and ignores a valid LOOPX_TEST_PYTHON, reproducibly starting Python 3.9. Reuse the existing resolver and cover the ternary/override regression. Independent validation: 234 Python tests, 27 TS tests and compiled HTTP smoke passed with the intended interpreter after revalidating the unchanged-patch rebase; no remote CI was queried.

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
@Duang777

Copy link
Copy Markdown
Collaborator Author

Addressed the Python discovery blocker in signed commit 5fcbde61e (exact head 5fcbde61edd0af4a416a6cd4be782960b9fe565f).

  • The compiled acceptance smoke now calls resolveTestPython({ repoRoot: repositoryRoot }); the local .venv / bare python3 discovery path is removed.
  • The shared discovery guard now rejects parenthesized ternary fallbacks such as condition ? resolved : "python3".
  • The precedence test now pins LOOPX_TEST_PYTHON ahead of both LOOPX_PYTHON_BIN and LOOPX_PYTHON.

Reproduced the reported environment locally: /usr/bin/python3 is 3.9.6 and the worktree test Python is 3.11.15. With LOOPX_TEST_PYTHON=<worktree 3.11> and LOOPX_PYTHON=/usr/bin/python3, the real compiled App/HTTP/store/planner smoke passed. The shared resolver/guard suite passed 5/5, control-plane typecheck passed, and git diff --check passed. uv.lock remains untracked and untouched.

Please re-review exact head 5fcbde61edd0af4a416a6cd4be782960b9fe565f.

This branch has not been deployed

No deployments
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.

2 participants