Skip to content

fix(history): 按 UTC instant 排序 Canonical Run History - #3347

Merged
huangruiteng merged 2 commits into
huangruiteng:mainfrom
yuefengw:codex/3346-canonical-history-utc-order
Aug 19, 2026
Merged

fix(history): 按 UTC instant 排序 Canonical Run History#3347
huangruiteng merged 2 commits into
huangruiteng:mainfrom
yuefengw:codex/3346-canonical-history-utc-order

Conversation

@yuefengw

Copy link
Copy Markdown
Contributor

变更概述

Closes #3346

collect_history() 之前直接按 generated_at 原始字符串倒序。不同本地时区的 ISO-8601 offset 会导致字典序与真实时间轴不一致,从而选错 latest_status_run、semantic history 或跨 Goal 的 top-level run。

本 PR:

  • 复用 control_plane.runtime.time.parse_timestamp(),按 UTC instant 排序;
  • 合法时间始终优先于缺失、畸形或 UTC 转换溢出的 legacy 值;
  • 保留原始时间文本,并为相同 instant 保留确定性 tie-break;
  • 同时修复单 Goal runs 与跨 Goal all_runs
  • 不修改持久化 schema,也不扩大到其他独立 run-index 读取路径。

验证

  • 3517 passed, 10 skipped 全量 pytest;
  • 新增跨 offset、跨 Goal limit=1、semantic history、缺失/畸形/Overflow timestamp 回归测试;
  • 两个 run-history smoke 通过;
  • Ruff、标准 mypy 通过;
  • loopx canary premerge --from-git-diff 通过:4/4 catalog canaries;
  • changed files public/private boundary scan clean。

兼容性

旧的 index 行不会被重写;无法解析的历史时间仍会被保留,但不会压过合法时间戳。

Signed-off-by: yuefengw <60574042+yuefengw@users.noreply.github.com>
Signed-off-by: yuefengw <60574042+yuefengw@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Exact head: 4582acd98cdde0fdd1cccf349dc6fa5dad5420f3

动机

这个 PR 修复的是 Canonical Run History 的真实时间顺序错误。旧实现直接比较 generated_at 原始字符串;当两个合法 ISO-8601 时间使用不同 offset 时,字典序并不等于 UTC 时间轴顺序。受影响的不只是历史列表展示:collect_history() 排序后的结果还会进入 latest_status_run、agent semantic history,以及跨 Goal 的 top-level runs,所以错误排序可能让状态、语义上下文和 limit=1 查询选中较旧的 run。

PR 选择复用已有 control_plane.runtime.time.parse_timestamp(),没有引入第二套时间解析协议,也没有重写旧 index 或扩大持久化 schema。相较于只修某一个消费者,这个落点更合适:排序错误的共同根因就在 collect_history() 的 canonical read model,单点修复可以覆盖同一 Goal 和跨 Goal 两条路径。

改动思路

入口仍是 collect_history():它从每个 Goal 的 index.jsonl 读取并去重行,然后用新的 _chronology_key()generated_at 解析为 UTC-aware datetime。排序键先区分“合法时间”和“缺失/畸形/UTC 转换溢出时间”,因此任何 legacy 坏值都不会压过合法 run;合法值再按 UTC instant 排序,并保留原始时间文本作为同一 instant 的确定性 tie-break。单 Goal 路径还保留原 index 位置作为最后 tie-break,跨 Goal 聚合则依赖已排序 Goal id 与 Python stable sort 保持确定性。

正向路径是:带 +08:00 的较早 run 与带 Z 的较晚 run 被 parse_timestamp() 归一化,collect_history() 将后者排在前面,latest_status_runlatest_runs_with_agent_context()goal_semantic_history_from_runs() 和 top-level runs[:limit] 都消费同一正确顺序。负向路径是:缺失、无法解析或在 astimezone(UTC) 时溢出的时间返回 invalid key;它们仍保留在历史里,但落到所有合法时间之后,读取不会失败,也不会把坏行误当成 latest。

具体改动

  • loopx/history.py:新增 27 行、删除 3 行。生产行为集中在 _chronology_key() 以及 collect_history() 的两处排序调用;没有修改写入路径或外部 schema。
  • tests/test_history_chronology.py:新增 148 行,建立最小 index fixture,覆盖同 Goal offset 顺序、跨 Goal limit=1、semantic history,以及缺失/畸形/overflow 时间。

关键代码讲解

  1. _chronology_key(value):输入任意 legacy generated_at,调用既有 parse_timestamp() 得到 UTC-aware 时间;OverflowError 被收敛成 invalid,而不是让整个 history read model 失败。返回 (validity, instant, raw),其中 validity 明确保证合法值优先,raw 仅承担同 instant 或 invalid 集合内的确定性排序。
  2. collect_history() 的 per-goal sort:在 chronology key 后追加原始 index 位置并倒序,保证相同 key 的重复/legacy 行仍有稳定顺序。这个排序先于 latest_status_run、agent context retention 和 semantic history,所以三个消费者得到一致的 canonical chronology。
  3. collect_history()all_runs.sort(...):跨 Goal 聚合复用同一个 helper,修复 top-level limit 以前仍按字符串排序的遗漏;没有复制第二套判断。
  4. parse_timestamp()(未改动的共享调用点):naive 时间继续按既有协议视为 UTC,带 offset 的时间转换为 UTC。PR 没有偷偷改变解析规则,只改变排序消费方式。

类型/状态规则方面,这里没有新增 substring denylist、散落布尔状态或领域词汇;合法/非法时间通过结构化排序键表达。行为变化在 PR 描述和回归测试中明确说明,属于 canonical read model 的 bug fix,不涉及 must_attempt_work 一类 guidance/obligation 契约。

对主干的风险

最强回归场景是 legacy 时间输入导致 history 命令异常,或同一 instant 的多种文本表示出现非确定顺序。实现通过捕获 UTC 转换溢出、给 invalid 值固定的 aware sentinel,并保留 raw/index tie-break 来封闭这两个风险。blast radius 是所有依赖 collect_history() 的状态、operator gate、summary、dreaming 和 CLI history 读取;好处是它们统一修复,风险也集中在一个 read-model helper,回滚只需恢复原排序逻辑且不会迁移数据。

我在 exact head 上独立运行了:

  • pytest -q tests/test_history_chronology.py:3 passed;
  • run-history-readmodel-smoke.py:通过;
  • run-history-agent-context-retention-smoke.py:通过;
  • Ruff(changed files):通过;
  • 仓库标准 python -m mypy kernel contracts:通过;
  • GitHub checks:7/7 成功。

测试同时包含正向 offset 归一化和负向 malformed/missing/overflow 路径。剩余风险主要是其他独立 run-index 读取器仍可能有自己的字符串排序;PR 明确将其列为 non-goal,因此不阻塞本 PR,但后续若发现同类 caller 应复用同一 typed helper,而不是再写局部字符串规则。

我的整体评价

结论:APPROVE。改动体量与问题匹配,生产代码只有一个可复用排序 helper 和两处真实调用点;测试虽然比实现长,但覆盖了 canonical history 的多个实际消费者与关键失败路径,不是一次性样例。未发现阻塞性正确性、兼容性、typed-state、domain-neutrality、行为披露或 guidance/obligation 问题。exact head 未发生变化,远端检查和独立 focused validation 均通过。

English verdict: APPROVE at exact head 4582acd98cdde0fdd1cccf349dc6fa5dad5420f3. The shared UTC-aware chronology key fixes both per-goal and cross-goal ordering, keeps malformed legacy rows readable but below valid timestamps, and passed focused chronology tests, two run-history smokes, Ruff, the repository-standard mypy kernel check, and all GitHub checks. No blocking finding.

@huangruiteng
huangruiteng merged commit 6558308 into huangruiteng:main Aug 19, 2026
7 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]: Canonical Run History 跨时区偏移可能选错最新 Run

2 participants