Skip to content

fix(chat): keep a completed Turn's event history when a stop races it - #7477

Merged
loopx-agent merged 8 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-m8v3
Oct 27, 2026
Merged

loopx-agent merged 8 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-m8v3

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Delivered through the maintainer integration #7505. GitHub recognized this PR as merged when its original head became part of main through that qualified integration. Original @JasonBuildAI commits/DCO and both review discussions remain. The original last-push readiness refusal remains historical evidence; the maintainer-owned integrated head had its own full self-review,265tests,CQR andready=true before merge. No review rules or independent-review claims were changed.

Problem and resulting behavior

A stop request racing completion could append stop intent to a Turn that had already completed, suppressing legitimate unfinished-work recovery. The store now checks the current status, persists admitted intent, and enters interrupting under the existing Turn lock. A terminal loser leaves the ledger unchanged; intent still precedes status for crash safety.

Attached-host queued claims, queue expiry and prepared acceptance settlement share the same Turn serialization boundary and re-read current state. They preserve successful claim identity and cannot overwrite a stop from an older queue snapshot. This resolves the claim-authority blocker in reviews #5666309659 and #5670407863, independently reproduced during maintainer review. No new execution permission, state schema, model policy or UI is added.

Closes #7476.

Validation

Exact head: b7bd894bf6772d492d486c641888e01b7608cff1.

  • Seven complete local Chat/store/queue/recovery/presentation modules: 265 passed, zero skipped. This includes terminal loser, interrupt-first claim/expiry, expiry-first terminal-reason preservation through the public claimant and Runtime stop, claim-first identity preservation and crash after durable intent before status write.
  • The pre-fix expiry counterexample at 7fdf3cade6dd51900bcfa6f43cdb60d76bc73447 failed and remains in the final regression suite.
  • Final risk-based premerge, required Ruff, configured mypy, diff hygiene and isolated changed-path boundary passed. CI is not consulted under the release Goal's local-validation policy.
  • Actual local JSON, locks, sessions and event ledgers are exercised with synthetic Host adapters. Windows and live remote-model cancellation are not qualified by these results.
  • The same measured module workload is chat_store.py 2,003 -> 2,066 lines and chat_runtime.py 2,319 -> 2,320. The ceilings settle the reviewed atomicity work in this diff; no safety clause, authority field or unique regression assertion is removed to fit an earlier size.

Frontend / Visual Evidence

UI impact: none. Existing stop and queue interfaces retain their caller contract; installed release/UI qualification is separate.

@cocolord cocolord 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.

Reviewer: model_agent; GPT-5; OpenAI; self_reported

动机

这个 PR 修复的是一个真实且会扩散到所有 Chat 表面的竞态:用户请求停止时,旧实现先写 turn.interrupt_requested,再比较并更新 Turn 状态;如果 Turn 在两步之间已经完成,终态事件账本仍会残留一个从未被接受的停止请求,后续 unfinished-work continuation 也会被错误抑制。PR 把“检查状态、记录停止意图、写入 interrupting”收进 store transition,目标 completion-race 从失败变为通过,方向和主要正路径都明确正向。

但 interrupt_turn 同样允许停止 queued Turn,而 queued Turn 可以被 attached host 同时 claim。新 helper 在 Turn lock 内读取旧 payload、追加事件,再把这份旧 payload 整体写回;claim_next_queued_turn 只持有 Session lock,不取得 Turn lock。exact-head 的确定性 interleaving 中,claim 调用已经返回 running 和 host_claim_id=host-claim,随后 interrupt 把持久化文件覆盖为 interrupting 且 host_claim_id=None,Session 还把这个 Turn 标为 active。也就是说,修复 completed Turn 的同时新增/放大了 queued claim 的执行权丢失,当前 slice 不能作为完整安全的原子 transition 交付。本 PR 不需要修改 UI、planner 协议或新增状态词汇,也不需要拆分整个 Chat store。

改动思路

ChatRuntimeController.interrupt_turn 仍是所有 Chat 表面的停止入口,真正的状态决策被下沉到 ChatSessionStore.begin_turn_interrupt。对所有遵守 Turn file lock 的 completion/finalization writer,这个边界是合适的:先在锁内确认非终态,再追加 durable intent,最后写 interrupting,既阻止终态之后追加 stop event,也保留 crash-safe 的 intent-before-status 顺序。

缺口在于仓库现有 queued claim authority 不共享这条 serialization boundary。claim_next_queued_turn 在 Session lock 下直接 _atomic_write_json Turn,再写 turn.claimed_by_attached_host 并把 Session 指向 active Turn。新 helper 的锁因此不能使三步 transition 对该 caller 原子化,而且回写旧 snapshot 会丢掉 claim 的字段。最小修复不是增加新状态机或通用事务框架,而是让 queued claim 和 interrupt 对同一 Turn 参加同一个序列化边界,或采用不会用旧快照覆盖并发字段的等价原子协议;同时保留 terminal loser 不写事件和 intent-before-status 两个既有目标。

具体改动

全 PR 修改 chat_store.py、chat_runtime.py 和 module metric baseline,共 +40/-6。runtime 用新 store API 替代原来的 append-event + update_turn 两步;store 新增 33 行 transition;canary 只更新两个已审 module ceiling,没有改变运行时协议。

关键代码讲解

  • ChatRuntimeController.interrupt_turn:把 queued/starting/running/interrupting 四种可停止状态交给新 store transition;这意味着 queued claim 竞态属于实际 production caller,不是不可达的 helper 误用。
  • ChatSessionStore.begin_turn_interrupt:在 chat_store.py:1442-1449 读取 payload、检查状态、追加 intent 后写回同一份 snapshot。对取得 Turn lock 的 writer 有效,但“锁住旧快照”不等于对绕过这把锁的 writer 原子。
  • ChatSessionStore.claim_next_queued_turn:在 chat_store.py:1325-1358 将 queued Turn 更新为 running,写入 host_claim_id/started_at 并追加 claim event;它只在 Session lock 内直接写 Turn file,未取得 begin_turn_interrupt 使用的 Turn lock。
  • module_metric_baseline.json:只把 chat_runtime.py 和 chat_store.py 的行数 ceiling 更新到当前值;它能让 maintainability ratchet 反映增长,但不验证并发语义。

规范依据是 issue #7476,spec_revision 2026-10-27T09:11:31Z:

  • I7476-TERMINAL-LOSER — implemented:停止请求输给 completion 时不再向 completed Turn 追加 interrupt event,目标测试通过。
  • I7476-INTENT-ORDER — implemented:被接受的停止仍先持久化 intent,再写 interrupting。
  • I7476-ATOMIC-TURN-TRANSITION — not_met:issue 要求状态检查、intent 和状态写入成为一个 atomic Turn transition;queued host claim 是同一 Turn 的实际 writer,却不参加这把锁,exact head 会覆盖已提交的 claim authority。

对主干的风险

已执行 exact-head 目标测试:1 passed;tests/test_chat_session_active_turn.py 52 passed;tests/test_chat_queue_preparation.py 41 passed;修改模块 Ruff 与 git diff --check 均通过。这些结果证明 completed-Turn 主路径已修复,也证明普通 queue 行为没有直接退化,但现有测试没有覆盖 queued claim 与 interrupt 的交错。

最强反例使用真实 ChatSessionStore 和两个受控线程:interrupt 在锁内读到 queued 后暂停,attached-host claim 写入并返回 status=running, host_claim_id=host-claim,再恢复 interrupt。exact head 最终持久化 status=interrupting, host_claim_id=None,Session 的 active_turn_id 仍指向这个 Turn,事件序列同时包含 turn.claimed_by_attached_host 和 turn.interrupt_requested。在共同 base 的旧 append + update_turn 路径,同一 interleaving 最终仍保留 host_claim_id=host-claim;因此这不是把一个已知 pre-existing race 当成本 PR 缺陷,而是当前实现用 stale snapshot 回写造成的可区分回归。

语义与 CI 对齐

这里的 queued/running/interrupting 是明确状态集合,没有 substring denylist;文案也保持 domain-neutral。阻断点是 machine-enforced obligation 与实现不一致:docstring 和 PR body 声称这是“one Turn transition”,并声称被接受的 interrupt 不会丢失 intent,但 attached-host claim 的执行权字段反而可以被静默丢失。host_claim_id 是已提交执行归属的一部分,不是可丢弃的显示字段;claim caller 已收到成功回执后,持久化 authority 必须与该回执一致。

最小修复是让 queued claim 与 interrupt 共享 Turn serialization/CAS 边界,或者证明并实现等价的不丢字段协议;新增确定性 queued-claim-vs-interrupt 回归,至少断言 persisted status/claim、Session active pointer 和 event ledger 相互一致。还应重跑目标 completion loser,确认 terminal loser 仍不写事件,并保留 crash-safe intent-before-status 顺序。PR body 已提到三个 unlocked queued writer,但把它们称为 issue 外并不能消除当前 helper 对 queued 的实际支持;若另外两个 writer 不受影响,需要用调用路径和负向证据界定,而不是靠范围声明。

当前 hosted checks 仍有 Release Artifacts 红项和若干运行中的 required jobs;没有完成同 base 归因,因此不拿它们作为本次 REQUEST_CHANGES 的理由。future-facing pass 考虑的是同一 store owner 内的 writer serialization:这是与本修复同 change reason、可局部验证的 companion repair,不需要趁机拆分整个 2k 行模块或引入通用事务框架。

我的整体评价

结论是 fragmented。long_horizon=not_yet_proven:completed Turn 的账本污染已关闭,但同一个停止 API 对 queued Turn 会破坏 attached-host claim 的持久化 authority,Chat 状态机还不能长期可信。user_experience=not_yet_proven:用户的 stop 请求和 host 的成功 claim 可以同时得到表面成功,最终却留下 active Session、interrupting Turn 与丢失 claim id 的矛盾状态,恢复与重试责任不清晰。

这个 PR 选择了正确 owner,代码量也与原问题大体相称;无需推翻设计。请补齐同一 Turn 的 writer 序列化,并加入上述确定性并发回归后再复审。当前 exact head 不能 APPROVE;除这个原子性 blocker 外,我没有发现需要扩大到 UI、planner 或新协议的第二个阻断项。

English verdict: REQUEST_CHANGES - b91a349 fixes the completed-Turn loser, but begin_turn_interrupt can stale-overwrite a concurrent queued host claim, dropping host_claim_id after claim success; serialize both writers and add a deterministic claim-vs-interrupt regression.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

@cocolord 感谢 review。queued claim 这条我按 blocker 接受,你的交错描述和最终持久化状态(status=interrupting、host_claim_id=None,同时 Session 仍把该 Turn 标为 active)和我对代码路径的判断一致:claim_next_queued_turn 只在 Session lock 内直接 _atomic_write_json,而 begin_turn_interrupt 在 Turn lock 内读旧 payload 再整体写回,两者不在同一条序列化边界上,拿旧 snapshot 回写就会覆盖 claim 已经提交的字段。

接下来我按最小修复做,不加新状态机、不拆 store:

  • 先把你说的交错写成一个确定性回归(两个受控线程,interrupt 在锁内读到 queued 后暂停,claim 完成并返回,再恢复 interrupt),在修复前确认它失败;
  • 让 queued claim 与 interrupt 对同一个 Turn 走同一条 Turn 序列化边界(同一把 Turn lock 或等价 CAS),写回时只合并 intent 字段,不用旧 snapshot 覆盖 claim 已提交的字段;
  • 回归断言 persisted status / host_claim_id、Session 的 active_turn_id 与 event ledger 三者一致,同时保留 I7476-TERMINAL-LOSER(终态输家不写事件)和 I7476-INTENT-ORDER(intent-before-status)两个已通过目标,重跑目标 completion-race 测试确认不回退。

PR body 里把另外两个 queued 写者说成 issue 外的部分,我会改成按调用路径界定的说法,不再靠范围声明。

另外这个分支落后 main 27 个提交(GitHub 已标 BEHIND),我会一并 rebase 到最新 main 后再请求复审。

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

@cocolord 已按 blocker 修完,head cedadd7c2(rebase 到 a704725d6)。

  • claim_next_queued_turn 现在和 begin_turn_interrupt 共用同一把 Turn 写锁:队列扫描只当候选列表,真正的状态判定移到锁内重读。claim 在锁内写 status=running / host_claim_id 并追加 turn.claimed_by_attached_host;如果 interrupt 已经提交,它看到的状态不是 queued,就返回 None,既不会抢走 Turn,也不会再用旧快照覆盖 claim 已提交的字段。
  • 新增确定性交错回归 test_queued_claim_and_interrupt_share_one_turn_transition:interrupt 在锁内读完 queued 后暂停,claim 在这期间尝试,最后断言持久化的 status 与 host_claim_id、Session 的 active_turn_id、event ledger 三者一致。修复前该用例失败(claim 拿到 running + host_claim_id、Session 指向 active,interrupt 随后覆盖成 interrupting 且丢 claim id),修复后通过。
  • tests/test_chat_session_active_turn.py + tests/test_chat_queue_preparation.py:85 passed / 9 failed。这 9 个失败我用同一命令在没有本改动的 base 上跑过,失败集合完全一致(Windows 环境既有问题),本改动没有新增失败。
  • I7476-TERMINAL-LOSER(终态输家不写事件)与 I7476-INTENT-ORDER(intent-before-status)保持不变,目标 completion-race 用例仍通过。

PR body 里那三个 queued writer 的措辞我等这轮 CI 结果出来一并收窄,不再用范围声明替代调用路径说明。

interrupt_turn reads the Turn, appends the durable interrupt-intent event,
and only then compare-and-sets the status. When the Turn completed in
between, the CAS refuses the change but the intent event remains in the
completed Turn's ledger, because loopx-project#7191 moved the intent ahead of the status
write without keeping the terminal-state check that write used to provide.
That breaks the terminal event history pinned by
test_interrupt_does_not_overwrite_a_completed_turn and makes the completed
Turn report stop_observed, which suppresses the unfinished-work
continuation in the external-conversation planner.

Make the status check, the intent event and the status write one store
transition under the Turn write lock. The intent still lands before the
status change, so a crash cannot turn a requested stop into an automatic
review, and a Turn that already reached a terminal state can no longer gain
a stop event after its terminal outcome.

Fixes loopx-project#7476

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The stop-intent transition adds 33 lines to chat_store.py and one net line to chat_runtime.py. Refresh the reviewed module metric ceilings in the same diff so the maintainability ratchet settles the accepted growth.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
`claim_next_queued_turn` wrote the Turn from the snapshot the queue scan had
read, under the Session lock only, while `begin_turn_interrupt` read and
wrote the same Turn under the Turn write lock. The two writers therefore did
not share a serialization boundary: an interrupt that read `queued` could
resume after the claim had committed `running` with a `host_claim_id` and
had published the Session active pointer, then write its older snapshot back
as `interrupting` with no claim id.

Take the Turn write lock for the claim too, and re-read the Turn inside it.
An interrupt that already committed now wins the transition, so the claim
returns `None` instead of starting a Turn the stop request owns, and a claim
that commits first is never rewritten from a snapshot that predates it. The
queue scan stays a candidate list; the authoritative status check moves
under the lock that the interrupt also takes.

Cover the interleaving deterministically: the interrupt is paused after its
read, the claim is attempted while the interrupt still holds the lock, and
the test asserts the persisted status and claim id, the Session active
pointer and the event ledger agree.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
…to codex/review-chat-interrupt-7477-20261027

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>

@loopx-agent loopx-agent 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.

Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max

[P2] 队列过期仍可被停止操作的旧快照覆盖。 精确 head 203590e5b75530c9f0df403566683927623df764:begin_turn_interrupt 在锁内读到 queued 后,真实 claim_next_queued_turn 的过期扫描仍不取这把锁,已提交 timed_out/session_queue_expired;随后1471行写回旧 payload,最终 runtime 返回 interrupted、错误变为 null。相同输入和交错在不可变基线 19e1525a8babdd6bae5d505d72747f64ec21bc8d 保留超时结果。请让过期 writer 同样锁内重读/CAS,并补两种胜出顺序;不应删除过期逻辑或放宽终态断言。

动机

从现有 Dashboard、Desktop 或 Lark 对话停止排队任务的用户,以及同时领取队列的宿主。

旧实现会给已经完成的轮次追加停止事件;新实现把检查与停止意图放进同一 Turn 锁,但排队任务先被过期清理后仍会被旧快照覆盖。

完成竞态和 queued claim 原反例已有通过证据;队列过期竞态仍改写终态,当前不能完整兑现停止不污染结果的承诺。

不新增 UI、planner、状态机或执行权限,不要求实时模型、全仓重构或整个父路线图完成。

Turn 是一次对话执行。错误的停止事件会影响后继恢复判断;这次更进一步覆盖了已经持久化的超时结果,使用户无法从读回获知真正失败原因。112项原套件通过,仍不能代替这个可达 writer 的交错证明。

改动思路

停止与队列过期都属于现有 Store 的状态不变量,应复用同一 Turn 锁;无需新状态机、observer 或配置。

当前边界为既有停止入口与同一 Turn 的实际并发 writer,保留终态、claim、事件和 Session 一致性。

runtime 仍是共同停止入口,Store 负责状态、事件和 claim,adapter 只执行真正获准的停止。把检查移入原 Turn 锁是正确方向;本轮也确认上次 queued host claim 的旧快照问题已由共同锁收敛。但同一 claim API 在进入该锁之前先执行过期扫描,那里仍直接写 Turn 文件。Session 锁与 Turn 锁保护不同对象,不能相互替代。

相同触发 基线 当前 head
stop暂停,队列过期先提交,再恢复stop 保留timed_out及session_queue_expired 改为interrupted并清掉错误
普通完成竞态/queue相关套件 原完成缺陷有既有反例 112项通过;过期探针单独失败

具体改动

完整差异四文件+225/-38:两个生产模块+92/-36、既有测试+131、metric配置+2/-2,全部diff及未改动的过期/接受/完成调用方已检查。没有新的持久schema或权限字段。

规范依据 #7476,spec_revision 2026-10-27T09:11:31Z。按该问题及上轮公开框架映射:I7476-TERMINAL-LOSER not_met,普通完成已修但过期终态仍被覆盖;I7476-INTENT-ORDER implemented,接受停止时intent仍先于status;I7476-ATOMIC-TURN-TRANSITION not_met,过期writer未参加同一序列化边界。

关键代码讲解

  1. begin_turn_interrupt 读payload、检查状态、追加intent,然后整体写回;1471行不会发现未遵守Turn锁的writer已提交终态。
  2. claim_next_queued_turn 新增锁内重读保护host claim,但其前置 _settle_expired_queued_turns 在1247行仍直接写同一个文件。这个分支由实际公开队列调用触发,不能以名称或范围声明排除。
  3. interrupt_turn 消费这个错误的成功transition,随后将其写成interrupted并清除错误;不是只影响helper内部返回值。
  4. 新head将chat_store ceiling从2036补到实际2058,并增加claim先赢和intent后状态写失败两项测试。当前两生产blob与7ab逐字一致;重新执行112项与过期探针,预算问题已解除,过期覆盖仍复现。

对主干的风险

独立探针使用真实Runtime、Store、锁、JSON和完整event ledger;只暂停首次stop intent前的交错,没有mock状态、CAS或最终结果。过期调用先成功写timed_out,然后当前head的停止将其覆盖。基线也有原问题的多余interrupt事件,本次仅认定新增的终态/错误覆盖回归,没有把基线说成所有目标都通过。

本地PASS:四个所属模块112项(52.83秒)、改动Python Ruff、diff/compile/双语检查。新head标准premerge完成6直接+13选中:直接、module ceiling和ratchet均通过;interaction catalog仍红,已在不可变19e基线复现相同缺context_delivery/explore_harness,后者不作为本PR运行时缺陷。当前准确scope CQR cqr_5d12aba964681fec8e90 已在首评前记录并物理读回,结论FAIL,仅保留实际expiry反例;旧7ab收据不沿用。未查询或等待CI。

语义与 CI 对齐

复用现有状态词汇和Store owner,无新actor、执行权或领域专用义务;这是一项默认正确性修复,没有opt-in声明。阻塞是当前终态不变量被真实writer破坏,不是未来架构偏好。最小修复为同一Store内的过期锁/CAS,回归至少核原超时原因、claim、Session active pointer和event ledger一致,并重跑原completion/claim用例。无需拆整个Store或新建协调框架。

我的整体评价

REQUEST_CHANGES — 203590e。 上次queued claim finding的修复有价值,当前完整slice仍fragmented;long_horizon与user_experience均有上述具体regression。请补同一接受路径中的过期writer序列化并保留已修预算,再以新head完整复审。现有锁和CAS足以表达修复,不需要新兼容分支、模型调用或额外用户确认。

检查 chat_runtime.py 的既有停止入口和 chat_store.py 持久化链;没有渲染或布局变化,视觉评审不适用。 视觉未验证范围:none。实际宿主、安装态和其它平台并发未认证;本评审不授予merge/dismiss或安装权限,旧评审讨论保持。

English verdict: REQUEST_CHANGES — 203590e. The queued-claim lock repair passes112owning tests, but a real Runtime/Store expiry race overwrites committed timed_out/session_queue_expired with interrupted/error-null; the same immutable-base probe preserves the timeout. Serialize expiry with the same Turn boundary, cover both orders; the current module ceiling is now settled. No remote CI was consulted.

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>

@loopx-agent loopx-agent 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.

Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max

APPROVE,精确 head b7bd894bf6772d492d486c641888e01b7608cff1。本轮独立复审并修复了旧评审指出的claim竞争,以及复审新发现的expiry竞争。

动机 / Motivation

在Chat、桌面或Lark会话停止一个正在结束或排队的Turn的用户与宿主。 停止请求输给已完成Turn时仍追加stop intent,使未取消的工作被当作用户取消;早期修复又可能用旧快照抹掉成功领取的host_claim_id。 同一Turn锁内复查状态、先持久化停止意图再变更状态,终态败者不追加事件;领取、排队超时和acceptance结清共享该锁并重新读取当前状态。 不授予新的执行或自动继续权限,不改变宿主取消API、队列顺序、模型、配额或用户界面。

改动思路 / Approach

复用ChatSessionStore原有Turn文件锁、事件账本及状态集合,将同一Turn的竞争写入纳入现有owner,不另建事务框架或Python/TS决策副本。 本PR修复既有Chat停止路径及同一store中直接竞争的queued writer,保留原宿主与恢复责任。 不是把事件移回状态写之后,而是把最新状态检查、durable intent和状态写入收在同一Turn锁内;这样终态败者无新增事件,崩溃时已接受的停止意图仍先于状态存在。相邻queued claim、expiry和acceptance结清也必须复用此边界,否则只锁interrupt仍会读旧快照覆盖执行归属。锁顺序保持Session后Turn,interrupt持Turn期间不反向取得Session。

具体改动 / Changes

逐项按 #7476 的 2026-10-27T09:11:31Z 版本核对 I7476-TERMINAL-LOSER、I7476-INTENT-ORDER、I7476-ATOMIC-TURN-TRANSITION:ChatRuntimeController.interrupt_turn调用store helper;begin_turn_interrupt在原Turn文件锁内重新读取,终态直接返回,允许状态才append intent并写interrupting。queued host claim锁内重新读取,只有仍queued才能提交claim并发布Session active pointer;claim先提交时完整claim字段保留,interrupt先赢时不领取其他queued头。排队expiry也锁内重读,不能在已接受停止后写timed_out/turn.failed;prepared acceptance结清仅在同锁内去掉capsule,保留当前Turn字段。

旧 CHANGES_REQUESTED #5666309659 的claim blocker已逐项核对:原来成功claim后的host_claim_id可能被旧queued快照抹掉,现在两个writer共享Turn锁,控制线程测试同时断言persisted status、claim、Session pointer和event ledger。新评审 #5670407863 的expiry blocker也逐项核对:我方另以7fdf3cade6dd独立反例实际复现expiry额外追加turn.failed,再修复并保留该例;另经真实claim_next_queued_turn让expiry先提交,再从Runtime stop,完整timed_out/session_queue_expired、claim/Session和ledger保持不变。claim先赢与intent后状态写前崩溃两例核验claim identity和原intent顺序。最终head七个完整模块265通过、0跳过;终态败者、interrupt先赢、claim先赢、intent后状态写前崩溃及排队超时均覆盖。先前7fdf3cade6dd的expiry反例1失败1通过,最终修复保留该反例。最终premerge、规定Ruff、mypy和四文件隔离边界通过。 模块大小在相同base/head测得chat_store 2003→2066、chat_runtime 2319→2320;只结清实测增长,不删锁、字段或安全语义来满足旧上限。

对主干的风险 / Risks

主要风险是锁序反转或把旧候选当作最新authority。逐条检查了Session→Turn顺序、lock内重新读取与终态返回;没有Turn→Session的新增持锁路径,事件仍走原账本owner。265例覆盖实际本地JSON、文件锁、队列、进程恢复及原UI队列后端,适配器仍为合成Host,不能把这些结果称为真实模型取消或Windows资格。本机真实JSON存储、文件锁和事件账本已验证;适配器使用合成Host,未执行Windows或真实远端模型取消。发布版完整安装与UI旅程由统一候选完成。 CI依Goal策略未查询,先前失败保持历史失败,只有最终source的通过支持当前结论。

我的整体评价 / Verdict

复用ChatSessionStore原有Turn文件锁、事件账本及状态集合,将同一Turn的竞争写入纳入现有owner,不另建事务框架或Python/TS决策副本。 本PR修复既有Chat停止路径及同一store中直接竞争的queued writer,保留原宿主与恢复责任。 三项issue验收和旧claim blocker在当前head都有代码与独立执行证据;same-owner companion修复关闭了expiry的实际反例,acceptance结清也不再越过Turn锁。没有新增通用事务框架、影子状态或外部权限。当前改动可以独立回滚,无状态迁移;仍需在正式发布候选完成完整安装和渠道核验。发布APPROVE后只按原生closeout核对已解决旧review,未核验的异议不会被删除或忽略。

English verdict: APPROVE b7bd894bf6772d492d486c641888e01b7608cff1. The terminal loser adds no stop intent; admitted intent stays durable before status; claim,expiry andacceptance settlement serialize onthe sameTurnlock.265cases andrequiredlocalgates passed. Priorclaim blocker is resolved;realremoteHost/Windows qualification isnotclaimed.

@loopx-agent
loopx-agent dismissed cocolord’s stale review October 27, 2026 16:27

Maintainer-authorized reconciliation after exact-head review 5670640409 at b7bd894. The original queued-claim stale-snapshot finding is resolved: claim and interrupt now share the Turn lock and re-read current authority; controlled-thread tests retain persisted status, host_claim_id, Session active pointer and event ledger. Both claim-first and interrupt-first orders pass. Adjacent expiry and acceptance writers share the boundary; expiry-first public-claimant/Runtime-stop readback preserves timed_out/session_queue_expired, and crash after durable intent retains claim identity and intent-before-status. All265 owning cases, required local premerge, Ruff, mypy and public boundary passed. Original discussion is retained; the later expiry finding was independently reproduced and fixed, and its historical review is preserved.

@loopx-agent
loopx-agent merged commit c184163 into loopx-project:main Oct 27, 2026
11 of 32 checks passed
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.

[Bug]: a stop request racing Turn completion writes turn.interrupt_requested after turn.completed and suppresses unfinished-work recovery

3 participants