Skip to content

fix(scheduler): fence Python follow-ups on superseded heartbeat receipts - #8647

Open
gcl-coder wants to merge 5 commits into
loopx-project:mainfrom
gcl-coder:fix/scheduler-heartbeat-receipt-fence
Open

gcl-coder wants to merge 5 commits into
loopx-project:mainfrom
gcl-coder:fix/scheduler-heartbeat-receipt-fence

Conversation

@gcl-coder

@gcl-coder gcl-coder commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem / 问题

Fixes #8605. The Python scheduler follow-up route accepted a heartbeat receipt that a newer Turn already superseded: it called _run_native_followup with turn_instance_id: None/require_heartbeat_receipt: False, so the native fence never ran. Moving the fence into the native owner exposed two further limits: the shared settlement readback decodes rollout rows strictly, so one damaged audit row made a valid return exit 1 with zero writes, and the fence identified a receipt by Goal slug/Agent/Turn only, so a same-name receipt from another Goal instance could authorize a write.

Fix / 修复

  • record_quota_scheduler_ack and record_quota_scheduler_failure_for_decision carry turn_instance_id and goal_ref into _run_native_followup, which sets require_heartbeat_receipt. The native entry check and the lock-held commit re-check are the single freshness owner; no Python-side freshness scan is left.
  • heartbeatReceiptStatus takes an optional exact goalRef. When the caller supplies one, both the originating receipt lookup and the newer-receipt scan require an exact goal_id + goal_instance_id match; a receipt without goal_ref belongs to the legacy, no-instance scope. heartbeat_followup.ts validates goal_ref with parseExactGoalRef, rejects a goal_id that disagrees with the host facts, and passes the same ref to the preflight check and to SchedulerHeartbeatCommitOptions.heartbeatReceiptGoalRef, which re-checks under the mutation lock.
  • readReceiptLogSnapshot decodes each complete row and the unterminated tail with one strict UTF-8 decoder (fatal: true, ignoreBOM: true). A row that cannot be decoded is recorded in firstErrorLine and skipped, so it can never become a different, valid-looking receipt.
  • readQuotaSettlement has an explicit tolerate_malformed_history request field, default false. The scheduler writeback routes (build_scheduler_followup_payload, scheduler-ack, scheduler-ack-current, scheduler-fail-current status collection) set it; settlement-authority callers keep the strict readback and still fail with malformed_settlement_state.
  • The tolerant view is read-only with respect to shared caches: for the scheduler follow-up commands --write-projection-cache returns written=false with skipped_reason: "tolerant_scheduler_followup_view" instead of publishing partial history under the key strict quota status readers consume.

Validation / 验证

  • python3 -m pytest tests/control_plane/test_scheduler_heartbeat_receipt_fence.py tests/control_plane/test_scheduler_ack_current_host_binding.py tests/control_plane/test_scheduler_host_failure_cache.py tests/control_plane/test_scheduler_compat_state_key.py: 51 passed. Real File/SQLite × ACK/failure CLI runs append a summary row with a raw 0xff after a valid receipt and still exit 0 with scheduler_state_mutated=true; the superseded-receipt case still returns heartbeat_receipt_stale.
  • New real-CLI cache case (File and SQLite): after a malformed row, strict quota status fails; scheduler-ack-current --execute --write-projection-cache reports written=false / skipped_reason and writes no cache file; quota status --use-projection-cache returns the same exit code with no cache hit.
  • node --test tests/control_plane_ts/scheduler_heartbeat_followup.test.ts tests/control_plane_ts/scheduler_heartbeat_receipt_fence.test.ts tests/control_plane_ts/scheduler_heartbeat_followup_cli.test.ts: 36 passed. New cases cover a foreign Goal instance neither authorizing nor superseding an exact-instance receipt, and the commit lock re-checking freshness inside the exact instance.
  • Earlier rounds: node --test settlement/replan suites 180 passed; Python goal attention and scheduler decision-table suites 24 passed.
  • Ruff on the changed Python files and git diff --check: passed.

Not covered locally: Windows, managed CI, and a live Trae/Codex App scheduler UI. The ordinary CLI fixture writes legacy receipts without goal_ref, so the exact-instance behaviour is proven by the TS entry and lock tests and the real CLI covers the legacy no-instance scope.

Signed-off-by: gcl-coder <gcl-coder@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 · max

本次评审对应提交 0379886ae2733c8d613faa1c1b4ad4f35b4c87f1。

[P2] 损坏的日志行会阻断仍有效的调度回报。 只追加一条 summary 含非法 UTF-8 的后继记录时,Python 仍只读到原回执,head 的实际 CLI 却返回 heartbeat_receipt_stale,两个回报均无写入。 兼容调用入口

动机 / Motivation

执行自动化调度回报的 Host 和运行兼容 CLI 的操作者。Host 用仍有效的 heartbeat 回执回报调度更新。兼容入口原先只检查回执是否存在,较新轮次已经准入时,迟到的 ACK 或失败回报仍能改写调度状态。在 File 和 SQLite 的实际 CLI 中,新版本能拒绝确实被替代的旧轮次。当前轮次可以回报,重复 ACK 能回读原结果,两个操作都没有扣额。验证覆盖本地 File、SQLite、真实兼容 CLI 和 native scheduler 测试。实际 App 自动化、已安装版本和现代 Goal 生命周期迁移未验证。一条含非法 UTF-8 的日志行可以在 native 读取时变成较新回执,阻止仍有效的 ACK 和失败回报。

改动思路 / Approach

现有 TypeScript 写入 owner 已执行 freshness 判断并在锁内复核。两端各解释一次回执会让同一日志得到不同结论。保留原 Turn 的透传,由现有 reader 严格识别可用记录,再通过同一 native fence 决定是否写入。移除新增的 Python freshness 扫描。 Python 仍负责还原原始回执绑定与当前 Host observation,包含 Codex App 的 host-match 路径。TypeScript 在收到请求后判定 freshness,提交时持锁重读。另一份 draft 的最小透传实现与本 PR 的 20 个普通场景结果相同,也能复现坏行,说明修复应落到现有 reader。 较小透传实现

具体改动 / Changes

关键代码讲解

  • build_scheduler_followup_payload 继续解析原 Turn 和 Host 参数,新加 heartbeat_receipt_status 的检查,读取同一 rollout log 第二次。其新测试覆盖较新回执、normal-run 审计、missing 和 ACK 接线。 Python 入口 测试
  • record_quota_scheduler_ack 以及 ACK / failure adapter 透传 turn_instance_id,_run_native_followup 开启 require_heartbeat_receipt。native 收到请求和持锁写入时各确认一次。 传输 锁内校验
  • readReceiptLogSnapshot 仍用替换式 UTF-8 解码读取完整行及尾行,非法字节可能通过 JSON 检查。 共享 reader

规格采用 docs/reference/protocols/host-integration-plugin-plan-v0.md 的 c49d59b24d3f7745fccf3a1f2df1e35036fe517b。Phase-3-Ordering 已证明合法较新回执阻断迟到写入。Phase-3-Replay 的新鲜 ACK 重试复用原结果。Phase-3-ReceiptValidity 尚未满足,坏行仍能改变回执资格。 协议原文

对主干的风险 / Risks

在真实 File 与 SQLite CLI 中,先写一份有效 heartbeat receipt,再插入一条只在 summary 处含非法 UTF-8 的后继日志。Python 的读取只看到原 Turn,head 调用 TypeScript 后却称原 Turn 已被替代。两种后端的 ACK 和失败回报均返回 rc=1,调度状态无写入,base 四次都正常提交。当前 main 的 GitHub 候选合并树也复现四次。建议统一按有效 UTF-8 读取,保留宽容读的坏行跳过以及严格结算读的损坏报告,再复跑真实后继和锁内竞争用例。

语义与 CI 对齐

PASS,48 项 Python、30 项 native scheduler、Ruff。真实合法后继的四个组合从错误写入变为零写入。FAIL,四个坏字节回报场景在 head 错误拒绝。标准 premerge 有一项旧 fixture 失败,精确 merge base 在同一个 safe_bypass_allowed 断言失败,包含 #8615 更新的候选合并树通过该项。未读取远端 CI。

我的整体评价 / Verdict

REQUEST_CHANGES。迟到回报的隔离可以防止调度状态被旧 Turn 覆盖。当前改动把原生 reader 的坏字节误判带到兼容入口,使有权回报的 Host 收到错误恢复提示。下一版应保留原 Turn 和锁内 fence,把 freshness 留给同一 owner,并用两种 provider 的真实回报验证。本次评审覆盖现有宿主与 CLI 行为。验证覆盖本地 File、SQLite、真实兼容 CLI 和 native scheduler 测试。实际 App 自动化、已安装版本和现代 Goal 生命周期迁移未验证。

English verdict: REQUEST_CHANGES. Head 0379886 fences real supersession but rejects valid ACKs and failure reports when a malformed UTF-8 row is treated as a newer receipt; fix the shared decoder and keep one freshness owner.

"turn_instance_id": None,
"require_heartbeat_receipt": False,
"turn_instance_id": turn_instance_id,
"require_heartbeat_receipt": bool(turn_instance_id),

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] 损坏的日志行会阻断仍有效的调度回报。 提交有效的 scoped heartbeat receipt 后,向同一日志追加一条结构近似新回执但 summary 含非法 UTF-8 的行,再执行原 Turn 的 ACK 或失败回报。只追加一条 summary 含非法 UTF-8 的后继记录时,Python 仍只读到原回执,head 的实际 CLI 却返回 heartbeat_receipt_stale,两个回报均无写入。Python 跳过坏字节,heartbeat_receipt_status 判 fresh。_run_native_followup 开启原生门后,receipt_log_snapshot.ts 用替换解码把坏行交给 JSON parser,heartbeatReceiptStatus 把其 run_id 当作真实较新回执。把完整日志行和未终止尾行的字节有效性留给现有 TypeScript reader 验证。让同一个 native fence 给出 freshness 决策并在持锁写入时复核,移除 Python 的重复分类。通过 File 和 SQLite 的真实 should-run 与 scheduler-ack-current / scheduler-fail-current 验证坏行可容忍、合法后继可拒绝、持锁后二次校验不发生写入。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 6bf625a4ece9 处理。readReceiptLogSnapshot 对完整行与未终止尾行统一按严格 UTF-8 解码,坏行只记入 firstErrorLine 并跳过,不会再被替换字符伪装成较新的回执;调度回写路径使用该容错视图,精确结算仍严格拒绝。真实 File/SQLite 的 ACK 与失败回写在坏行下均退出 0 并写入调度状态,合法较新回执仍返回 heartbeat_receipt_stale。

Strict UTF-8 decoding in the shared receipt-log snapshot keeps every
replacement-decoded row from becoming a different, valid receipt. Damaged
complete rows and unterminated tails are recorded as strict errors and skipped
by the tolerant receipt reader, while settlement readback still fails closed.

Delete the Python compatibility route's duplicate freshness scan so the native
lock-held fence answers missing/stale once, and keep passing the originating
Turn id through the ACK and host-failure transport.

Signed-off-by: ChengLong GU <88359731+gcl-coder@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 · max

本次评审对应提交 346cfa19109eaa32ea499558e8868ae7f523608b。

[P2] 让有效的 Python 调度回报不被严格结算读提前拒绝。 同一有效回执旁有一条坏日志时,Python 兼容入口仍无法回报。File 和 SQLite 的 ACK、失败回报都退出 1、零写入;省略 Turn ID 也从主干可写变成拒绝。 兼容入口

动机 / Motivation

在自动化调度更新后回报成功或失败的 Host,以及通过兼容 CLI 恢复回报的操作者。主干的兼容入口遇到真正的新 Turn 时仍会让旧回报改写调度状态;前一评审头会把坏 UTF-8 误当成新回执,挡住正常回报。当前 head 让真正被取代的回执在写入前拒绝;原生入口已能跳过坏行,干净日志下的兼容回报与 ACK 重放正常。实测隔离的 File、SQLite、Python CLI 和原生 launcher;未查托管 CI,未操作实际 App 调度。source_session_v1 的通用 CLI 在 base/head 都于读取 registry 阶段拒绝,未据此声明生命周期资格。同一有效回执旁有一条坏日志时,Python 兼容入口仍无法回报。File 和 SQLite 的 ACK、失败回报都退出 1、零写入;省略 Turn ID 也从主干可写变成拒绝。

改动思路 / Approach

调度 freshness 由现有 TypeScript owner 和同一锁域统一决定。字节校验仍归现有日志 reader;结算的严格完整性与调度回报的容错判断有不同用途。保留兼容入口原始绑定、native 提交及锁内复核;把无关的严格结算前置读取从普通调度回报分开。 保留一处 freshness owner 是正确的:Python 传原 Turn,TypeScript 在入口和持锁提交时确认。单独做 Python 分类已被移除。下一步应让普通 ACK 和失败回报使用同一容错视图;需要结算证据的分支仍读严格视图。草案 #8606 的小透传也验证了只改接线不足以处理坏字节。

具体改动 / Changes

关键代码讲解

按 docs/reference/protocols/host-integration-plugin-plan-v0.md 的 c49d59b24d3f7745fccf3a1f2df1e35036fe517b 版本核对 Phase 3 和 #8605。Phase-3-Ordering 通过:真正的较新回执让兼容 ACK 和 failure 在写入前退出。Phase-3-Replay 通过:干净日志中,同轮 ACK 复用原结果,不写第二次、不扣额。Phase-3-ReceiptValidity 仍未满足:坏行不再伪装成合法后继,但严格结算读取阻止有效调度回报。

对主干的风险 / Risks

先用真实 should-run 写入当前回执,再在同一 rollout JSONL 追加一条 summary 含原始 0xff 的记录。当前主干的两种后端、两条回报对完整行和无换行尾行均可写入;本 head 的 8 个对应调用均退出 1,返回 quota_collection_failed,调度状态与扣额无变化。带 --verbose 的 File ACK 给出 settlement readback line 2 is malformed。没有 Turn ID 的另 4 个兼容调用同样从成功变失败;带 emitted host facts 的 native launcher 对坏完整行则 4 次成功,遇真实后继 4 次拒绝,均零扣额。主干合并树复现代表性的 File ACK 与 SQLite failure。建议补实际 CLI 回归,保持严格结算读取对同一坏行仍给出诊断。

语义与 CI 对齐

85 项 Python、63 项 TypeScript、Ruff 和类型检查通过;quota_settlement_cli 在精确 head 和当前主干组合各 187 项通过。标准 premerge 的唯一失败是旧 safe_bypass_allowed 断言,精确 merge base 与 head 同一断言同一结果,当前 main 与合并树均通过该 smoke,组合 quick 通过。这里复用既有类型和诊断,没有扩展语义词表。Phase 3 的真实回报负例仍失败。未查询托管 CI。

我的整体评价 / Verdict

请求修改。真实后继的隔离已通过,坏行导致的错误分类也从 native 消除;Python 路径仍无法完成有效回报,影响恢复与失败记录。把普通调度回报的读取与严格结算分开后,复跑同一回执、坏完整行、坏尾行、无 Turn 和真正较新回执。7 个文件的范围可保留在既有 owner 内。本次评审覆盖现有宿主与 CLI 行为。实测隔离的 File、SQLite、Python CLI 和原生 launcher;未查托管 CI,未操作实际 App 调度。source_session_v1 的通用 CLI 在 base/head 都于读取 registry 阶段拒绝,未据此声明生命周期资格。

English verdict: REQUEST_CHANGES. A valid newer receipt is fenced, but malformed UTF-8 still blocks the real Python ACK and failure commands before the tolerant scheduler owner runs.

scheduler_execution_context=scheduler_context,
operator_inbox_urgency_projector=operator_inbox_urgency_projector,
before_decision=before_decision,
turn_instance_id=turn_instance_id,

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] 让有效的 Python 调度回报不被严格结算读提前拒绝。 用 should-run 写入有效回执,再追加一条 summary 含原始 0xff 的 JSONL,使用原回执执行 scheduler-ack-current 或 scheduler-fail-current。 strictUtf8 现在正确把坏行标为 firstErrorLine,但 Python 在 _build_scheduler_followup_decision 中先走 read_heartbeat_settlement,后者以 strictGoalRolloutEvents 拒绝该行,尚未到达容错的 TypeScript scheduler fence。 同一有效回执旁有一条坏日志时,Python 兼容入口仍无法回报。File 和 SQLite 的 ACK、失败回报都退出 1、零写入;省略 Turn ID 也从主干可写变成拒绝。 --verbose 返回 settlement readback line 2 is malformed;带 host-facts 的原生命令在同样输入下成功,合法的新回执仍能拒绝旧回报。保留严格结算校验。让非扣额的调度回报通过现有 TypeScript owner 使用容错回执与原身份,避免先做无关的全日志严格结算读;需要精确结算事实的特殊分支继续要求权威 readback,并保留锁内复验。 在 File 和 SQLite 夹具分别运行真实 Python should-run 与两条 scheduler follow-up,向同一 rollout JSONL 追加完整或无换行的坏行;再与 emitted launcher 及合法后继对照,读回调度文件和扣额。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 fff4d61423c06cf212e08cb01eb5e141b3b8fc43 修复。

普通调度返回不再先走严格的全日志结算读:readQuotaSettlement 增加显式 tolerate_malformed_history 请求字段(默认 false),只有 build_scheduler_followup_payload 和 scheduler-ack-current / scheduler-fail-current 的状态收集传 true,标志沿 collect_status → attention queue → effective cadence → projectSettledReplanHistory 传递。需要权威结算事实的分支保持默认严格读,仍以 malformed_settlement_state 拒绝损坏行。容错视图只跳过不可读的行,可读的回执(包括取代原 Turn 的较新回执)仍按顺序参与原生围栏,入口检查与持锁提交时的复核不变。

真实 File 与 SQLite 的 scheduler-ack-current / scheduler-fail-current 四组 CLI 场景:在有效回执后追加一条 summary 含原始 0xff 的 JSONL 行,修复前退出 1、零写入,现在均退出 0、ok=true、scheduler_state_mutated=true;合法较新回执仍返回 heartbeat_receipt_stale。tests/control_plane/test_scheduler_heartbeat_receipt_fence.py 10 passed;TS 结算与 replan 定向 180 passed,其中新用例断言默认读回仍拒绝坏行、容错读回与干净事实完全一致。Ruff 与 git diff --check 通过。远端 CI 尚未观察到全绿。

Signed-off-by: ChengLong GU <88359731+gcl-coder@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

请求修改,复审 fff4d61423c06cf212e08cb01eb5e141b3b8fc43。上轮关于损坏行阻断两个 *-current 命令的发现已经修复;本轮确认了三个不同的兼容问题。

动机 / Motivation

宿主执行定时设置后,要把结果回写给 LoopX;这包括旧版宿主和从命令行手工补写节奏的操作者。 如果旧轮次的调度回报迟到,主干仍可能接收它并覆盖新轮次的节奏。候选会拒绝确有后继轮次的回报,并允许有效回执旁边有一条坏日志时继续回写。 操作者应能用当轮回执回写成功或失败;旧轮次和错误归属的回执不得改写节奏,重复回报也不应再写一次或扣额。 这个 PR 修复本地宿主回报与读取回执的路径;不修改宿主的计划设置,也不新增 Goal、Agent 或扣额权限。 已复现的问题 #8605。

改动思路 / Approach

兼容 CLI 把原 Turn 交给既有 TypeScript 调度事务。它用日志判断该回执是否仍有效,获取状态锁后复验,最后才写 cadence。严格 UTF-8 解码避免把无法解码的坏行当成后继回执;精确结算仍应拒绝坏历史。相比维持现状,这一改动修复了确有新轮次时迟到回报仍能写入的问题。与仍为 Draft 的 #8606 相比,光传 Turn 不足以保证坏行和 Goal 实例的边界。之前的仓库经验提示应验证“原回执能否继续完成工作”;本轮采用真实写回及重放对照,没有沿用以前的结论。

回执是否属于这一轮是原生调度事务的正确性约束,兼容 CLI 只应传递身份。容错快照跨进通用 status/replan 缓存后产生了已复现的漏报,因此没有必要保留当前传播范围。 本 PR 可以完整交付原轮次和原 Goal 实例在写入前及锁内的检查,以及调度专用的容错读取;普通状态和精确结算继续使用严格读取。

具体改动 / Changes

关键代码讲解

  1. _run_native_followup 将 Turn 和是否要求回执送给现有 TS 事务;持锁检查 让期间到达的有效后继仍能挡住旧写入。Python 仍尝试按 GoalRef 读回,但删除了查不到就拒绝的分支,原生参数没有接到等价的实例范围。
  2. strictUtf8 在完整行与临时尾行验证字节;坏行不会进入可用的回执集合,并保留首个严格错误位置。缓存复用按实际前缀字节,不靠文件长度或时间戳。
  3. readQuotaSettlementSnapshot 给 rollout 与 run index 都增加容错。该开关从 live decision、replan history、attention/status 收集传入;prepare_quota_command_context 只在两个 current 命令的第一层收集启用,并照常写共享 projection cache。新测试验证坏行下两个 current 命令能运行,未覆盖其他读者消费该缓存或异实例回执。
  4. build_scheduler_followup_payload 把 ACK 和失败回报都接到同一门;普通 host-facts 原生入口、已有历史结算分支与兼容入口仍应分开验证。

规范:docs/reference/protocols/host-integration-plugin-plan-v0.md @ 167b0a1f3a3307c284b021ea9a09c7c0b5e5a11f(Phase 3);手工兼容入口见 source CLI contract。

  • Phase-3-Ordering:implemented。 真正的同 Agent 后继挡住旧 ACK 与失败回报;其他 Agent 不受影响。
  • Phase-3-ReceiptValidity:not_met。 坏字节不再充当回执,但混入旧式 registry 日志的异实例记录仍可授权。
  • Phase-3-Replay:implemented。 同一 fresh Turn 首次写入,第二次原样执行重放同一 effect;未再写或扣额。
  • Compatibility-Manual-Fallback:not_met。 手工兼容 ACK 回归;普通 status 可被容错的 ACK 预览污染。

对主干的风险 / Risks

  • [P2] 保留 Goal 实例范围。 兼容 CLI 读取 Goal 的旧式 registry,日志里只有另一 Goal 实例的同名 Goal/Agent/Turn 回执。 在真实 Trae 兼容入口,主干返回 heartbeat_receipt_missing;候选返回成功且 scheduler_commit.written=true。Python 精确查询拒绝了异实例行,但 native 请求只带 Goal slug、Agent 和 Turn,因此异实例记录取得了 cadence 写入权。这里复现的是旧式 registry 中混入已标记实例的历史,不外推到启用 source-session 的入口。 保持 TypeScript 单一 freshness owner,把期望的 GoalRef(或显式旧式无实例范围)交给它;预检与持锁写入都用同一范围排除异实例行。补充异实例不能授权、也不能使有效回执过期的入口测试。
  • [P2] 不把容错投影发布为严格缓存。 同一 Agent 在损坏 JSONL 下以 scheduler-ack-current --write-projection-cache 做预览,然后以同一参数范围执行 quota status --use-projection-cache。 同一份历史、不改任何数据,缓存未命中时 quota status 退出 1 并报告 collection failed;预览写入缓存后,status 命中同一个 key,退出 0 且没有 health_items。局部容错被其他严格读取者当成完整状态。 让 scheduler 的容错读取留在自己的窄入口;至少不要把容错投影写到严格读者使用的缓存 key,也不要无标记地把部分历史投影成健康状态。用同一运行时验证预览前后 strict status 结果一致。
  • [P2] 恢复手工 ACK 兼容性。 同一份有坏 UTF-8 行的日志,已有宿主用 quota scheduler-ack --use-current-hint 回写实际应用的节奏。 SQLite 隔离对照中,主干写入成功;候选在构建通用 status 时先报 quota_collection_failed。带原 Turn 和无 Turn 的手工兼容入口均退化,而 scheduler-ack-current 成功。同一拥有者的兼容命令不应因拼写不同走到不同的损坏处理。 从现有 scheduler follow-up 边界提供统一的容错回执读取,而非逐条命令白名单。保持普通 status、精确结算和消费额度的读取严格;有 Turn 时保持 native fence。
对照 最新主干 候选
有效原回执,随后出现同范围有效新轮次 旧失败回报仍写入 旧 ACK / 失败回报均拒绝,状态不变
只有另一实例的同名原回执 missing,未写 写入 cadence
坏行下用手工 ACK 写入成功 带或不带 Turn 均在收集状态时失败
坏行下普通 quota status,前后只做一次 ACK 预览 两次均能读;旧解码尚未拒绝该字节 先失败,后从同一 key 命中缓存并报成功

仓库源码:260 个 TypeScript 用例通过;Python 比较为候选 193 通过、4 失败,最新主干 183 通过、4 失败。4 个失败的测试 ID 相同;抽取的 SQLite 失败都出现在未指定真实交付目录的生成命令,并且都没有追加写回。 从候选源码构建、安装 wheel,在隔离的 SQLite fixture 中直接运行已安装的 loopx,重现缓存、手工 ACK 和 GoalRef 对照。 typecheck:control-plane、diff 检查和提交 DCO trailer 检查通过。上述四个测试的生成命令工作目录问题单独保留;候选变化不在交付目录判定路径上。仓库合并门仍是 Sign-off 和 merge-gate;本轮不查询或等待远程 CI。

我的整体评价 / Verdict

请求修改。正确的普通回报已恢复,但异实例回执仍能写、预览能掩盖坏历史,手工 ACK 还会被提前拦住。 回报归属由既有 native fence 判定并锁内复核;容错的 status 数据不与严格缓存共用,手工与 current 命令调用同一个调度专用读取。 复验需保持真实的同范围后继拒绝、坏行下当前回报可用、锁内复验和零额外扣额。核验范围是 Trae 兼容 CLI、JSON 结果、wheel 控制台入口及调度状态文件;无改动到渲染界面。 原生 Windows、真实 Trae/Codex 宿主 UI、source-session 重建后的真实宿主旅程及托管 CI 未在本地运行。

English verdict: REQUEST_CHANGES. The exact head rejects genuinely stale callbacks and restores valid current callbacks beside corrupt rows. It still accepts a foreign Goal-instance receipt in the legacy CLI, leaks tolerant data into strict quota status, and breaks manual ACK recovery. Verified against real File/SQLite CLI state and the installed wheel.

"turn_instance_id": None,
"require_heartbeat_receipt": False,
"turn_instance_id": turn_instance_id,
"require_heartbeat_receipt": bool(turn_instance_id),

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] 保留 Goal 实例范围 兼容 CLI 读取 Goal 的旧式 registry,日志里只有另一 Goal 实例的同名 Goal/Agent/Turn 回执。 在真实 Trae 兼容入口,主干返回 heartbeat_receipt_missing;候选返回成功且 scheduler_commit.written=true。Python 精确查询拒绝了异实例行,但 native 请求只带 Goal slug、Agent 和 Turn,因此异实例记录取得了 cadence 写入权。这里复现的是旧式 registry 中混入已标记实例的历史,不外推到启用 source-session 的入口。 保持 TypeScript 单一 freshness owner,把期望的 GoalRef(或显式旧式无实例范围)交给它;预检与持锁写入都用同一范围排除异实例行。补充异实例不能授权、也不能使有效回执过期的入口测试。 在同一隔离日志中仅保留标记成其他 Goal 实例的原 Turn 回执,执行 quota scheduler-ack-current;断言返回 blocked_receipt,调度文件与扣额无变化;再覆盖异实例后继不使正确实例过期。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

三条已在 6bf625a4ece9(本轮对应本地提交 3aececf2a)处理。

Goal 实例范围。 heartbeatReceiptStatus 增加可选精确 goalRef:首发回执查找与后续“更新回执”扫描都要求 goal_id + goal_instance_id 同时相等;没有 goal_ref 的回执归入旧式无实例范围。heartbeat_followup.ts 用 parseExactGoalRef 校验并拒绝与 host facts 的 goal_id 不一致的请求,同一 ref 同时传入入口预检与 SchedulerHeartbeatCommitOptions.heartbeatReceiptGoalRef,持锁写前复核使用同一范围。新增 TS 用例:异实例回执既不授权也不使精确实例回执过期;持锁期间追加异实例新回执,精确实例仍以 written 提交。

容错投影污染严格缓存。 tolerate_malformed_settlement_history 白名单收敛到 QUOTA_SCHEDULER_FOLLOWUP_COMMANDS(scheduler-ack、scheduler-ack-current、scheduler-fail-current)。这三个命令下 --write-projection-cache 不再写共享缓存,返回 written=false、skipped_reason: "tolerant_scheduler_followup_view"。真实 File/SQLite 用例:坏行存在时严格 quota status 失败;scheduler-ack-current --execute --write-projection-cache 不落缓存文件;随后 quota status --use-projection-cache 退出码不变且无缓存命中。

手工 ACK 兼容性。 quota scheduler-ack --use-current-hint 现在与 * -current 共用同一容错调度读,坏行下带或不带 --turn-instance-id 都能写回;普通状态、精确结算与消费读取保持严格。

验证:test_scheduler_heartbeat_receipt_fence.py 加三个相关 CLI 套件共 51 passed;TS 三个 follow-up 套件 36 passed;Ruff 与 git diff --check 通过。未覆盖:Windows、托管 CI、真实 Trae/Codex App 即时 UI。普通旧式夹具不写 goal_ref,所以精确实例行为由 TS 入口与持锁用例证明,真实 CLI 覆盖旧式无实例范围。

Comment thread loopx/cli_commands/quota_context.py Outdated
},
)
if bool(getattr(args, "write_projection_cache", False)):
cache_metadata = write_status_projection_cache(

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] 隔离调度容错与严格 status 缓存 同一 Agent 在损坏 JSONL 下以 scheduler-ack-current --write-projection-cache 做预览,然后以同一参数范围执行 quota status --use-projection-cache。 同一份历史、不改任何数据,缓存未命中时 quota status 退出 1 并报告 collection failed;预览写入缓存后,status 命中同一个 key,退出 0 且没有 health_items。局部容错被其他严格读取者当成完整状态。 让 scheduler 的容错读取留在自己的窄入口;至少不要把容错投影写到严格读者使用的缓存 key,也不要无标记地把部分历史投影成健康状态。用同一运行时验证预览前后 strict status 结果一致。 File/SQLite:真实 should-run 后追加一条含 0xff 的 JSONL。先检查 quota status,跑带 --write-projection-cache 的 ACK 预览,再重复严格 status;坏历史的错误和健康项不能消失。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 6bf625a4ece9 修复。--write-projection-cache 在三个调度 follow-up 命令(scheduler-ack、scheduler-ack-current、scheduler-fail-current)下不再把容错视图写进 status_projection_cache,返回 written=false、skipped_reason: "tolerant_scheduler_followup_view",并补齐本地未提供的 status_projection_cache_metadata 导入。真实 File 与 SQLite 用例:先写有效回执并追加summary 含原始 0xff 的行,严格 quota status 失败;scheduler-ack-current --execute --write-projection-cache 不生成缓存文件;随后 quota status --use-projection-cache 退出码与预览前一致且 hit 不为 true。普通 status 读路径保持严格。

Comment thread loopx/cli_commands/quota_context.py Outdated
# A scheduler ACK/failure return writes cadence, not settlement
# facts. One damaged audit row must not fail the whole status read
# before the tolerant native fence can answer it.
tolerate_malformed_settlement_history=command in {

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] 保留手工 ACK 的兼容性 同一份有坏 UTF-8 行的日志,已有宿主用 quota scheduler-ack --use-current-hint 回写实际应用的节奏。 SQLite 隔离对照中,主干写入成功;候选在构建通用 status 时先报 quota_collection_failed。带原 Turn 和无 Turn 的手工兼容入口均退化,而 scheduler-ack-current 成功。同一拥有者的兼容命令不应因拼写不同走到不同的损坏处理。 从现有 scheduler follow-up 边界提供统一的容错回执读取,而非逐条命令白名单。保持普通 status、精确结算和消费额度的读取严格;有 Turn 时保持 native fence。 用原始坏字节和 SQLite 对照 scheduler-ack --use-current-hint 带/不带 --turn-instance-id,以及 scheduler-ack-current 与 fail-current,检查退出码、调度读回和零扣额。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 6bf625a4ece9 修复。tolerate_malformed_settlement_history 的白名单收敛为 QUOTA_SCHEDULER_FOLLOWUP_COMMANDS,即 scheduler-ack、scheduler-ack-current、scheduler-fail-current,手工入口与 *-current 共用同一容错调度读;普通 quota status、精确结算与消费读取仍走严格读。test_real_compatibility_cli_survives_a_damaged_audit_row 扩展为真实 File/SQLite × scheduler-ack-current/scheduler-fail-current/scheduler-ack --use-current-hint(带与不带 --turn-instance-id):坏行存在时全部退出 0、ok=true、scheduler_state_mutated=true。

Keep the exact Goal instance on the scheduler follow-up request so a
same-name receipt from another lifetime can neither authorize a return nor
supersede the exact instance's receipt at preflight or inside the commit
lock. Share the tolerant scheduler read with the manual compatibility ACK,
and refuse to publish that partial view into the projection cache strict
status readers consume.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@mergify

mergify Bot commented Nov 13, 2026

Copy link
Copy Markdown

Hi @gcl-coder, the DCO Sign-off check did not pass. Please inspect
its details first: checkout, fetch, timeout or infrastructure errors
need their own recovery, not a rewrite of otherwise signed commits.

If the log confirms a missing Signed-off-by trailer, amend the
affected commit with git commit --amend -s; for multiple commits,
use an interactive rebase against the current base from the correct
base-repository remote and sign off each affected commit. Push the
rewritten PR branch with git push --force-with-lease origin HEAD.

Bring the branch up to main so the PR is no longer behind and the
scheduler follow-up fence runs against the current settlement readers.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@gcl-coder
gcl-coder force-pushed the fix/scheduler-heartbeat-receipt-fence branch from 3344fc8 to 7e58c83 Compare November 13, 2026 08:12

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.

[Bug]: the Python scheduler follow-up route accepts a heartbeat receipt a newer Turn superseded

2 participants