Repository navigation
fix(scheduler): fence Python follow-ups on superseded heartbeat receipts #8647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0379886
346cfa1
fff4d61
6bf625a
7e58c83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Mapping | ||
| from typing import Any, cast | ||
|
|
||
| import json | ||
|
|
@@ -184,14 +185,17 @@ def _run_native_followup( | |
| host_facts: dict[str, Any], | ||
| use_current_hint: bool, | ||
| reason_summary: str | None, | ||
| turn_instance_id: str | None = None, | ||
| goal_ref: Mapping[str, object] | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Decision-free compatibility transport for in-process Python callers.""" | ||
|
|
||
| request = { | ||
| "schema_version": _FOLLOWUP_REQUEST_SCHEMA, | ||
| "runtime_root": str(runtime_root), | ||
| "turn_instance_id": None, | ||
| "require_heartbeat_receipt": False, | ||
| "goal_ref": dict(goal_ref) if goal_ref is not None else None, | ||
| "turn_instance_id": turn_instance_id, | ||
| "require_heartbeat_receipt": bool(turn_instance_id), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 验证坏行可容忍、合法后继可拒绝、持锁后二次校验不发生写入。
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已在
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,调度文件与扣额无变化;再覆盖异实例后继不使正确实例过期。
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 三条已在 Goal 实例范围。 容错投影污染严格缓存。 手工 ACK 兼容性。 验证: |
||
| "before": before, | ||
| "host_facts": host_facts, | ||
| "use_current_hint": use_current_hint, | ||
|
|
@@ -298,6 +302,8 @@ def record_quota_scheduler_ack_for_decision( | |
| generated_at: str | None = None, | ||
| use_current_hint: bool = False, | ||
| host_match_observed: bool = False, | ||
| turn_instance_id: str | None = None, | ||
| goal_ref: Mapping[str, object] | None = None, | ||
| ) -> dict[str, Any]: | ||
| state_key = _followup_state_key(before, surface=surface, state_key=state_key) | ||
| safe_agent_id = normalize_todo_claimed_by(agent_id) | ||
|
|
@@ -367,6 +373,8 @@ def record_quota_scheduler_ack_for_decision( | |
| host_facts=facts, | ||
| use_current_hint=use_current_hint, | ||
| reason_summary=reason_summary, | ||
| turn_instance_id=turn_instance_id, | ||
| goal_ref=goal_ref, | ||
| ) | ||
| except ValueError as exc: | ||
| return _failure_payload( | ||
|
|
@@ -396,6 +404,8 @@ def record_quota_scheduler_failure_for_decision( | |
| observed_host_rrule: str | None = None, | ||
| failure_kind: str = "host_tool_failure", | ||
| generated_at: str | None = None, | ||
| turn_instance_id: str | None = None, | ||
| goal_ref: Mapping[str, object] | None = None, | ||
| ) -> dict[str, Any]: | ||
| state_key = _followup_state_key(before, surface=surface, state_key=state_key) | ||
| safe_agent_id = normalize_todo_claimed_by(agent_id) | ||
|
|
@@ -443,6 +453,8 @@ def record_quota_scheduler_failure_for_decision( | |
| host_facts=facts, | ||
| use_current_hint=False, | ||
| reason_summary=None, | ||
| turn_instance_id=turn_instance_id, | ||
| goal_ref=goal_ref, | ||
| ) | ||
| except ValueError as exc: | ||
| return _failure_payload( | ||
|
|
||
There was a problem hiding this comment.
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 及合法后继对照,读回调度文件和扣额。There was a problem hiding this comment.
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.py10 passed;TS 结算与 replan 定向 180 passed,其中新用例断言默认读回仍拒绝坏行、容错读回与干净事实完全一致。Ruff 与git diff --check通过。远端 CI 尚未观察到全绿。