Repository navigation
fix(scheduler): reject illegal calendar timestamps instead of rolling them - #8632
Conversation
… them schedulerTimestampMilliseconds parsed ISO text with Date.parse. Impossible calendar dates such as 2026-02-31 and 2026-11-31 became a later real instant, so monitor schedules and failure-cache TTL accepted them. Backoff also treated a null parse as an elapsed interval, which advanced cadence from a clock that is not a real ISO date. Use parseIsoTimestamp for this codec. Present illegal clocks fail closed. A missing or blank updated_at still counts as elapsed, and an omitted failure-cache reference time still uses the wall clock. Signed-off-by: Duang777 <duangjl007@gmail.com> Co-authored-by: DDL <duangjl007@gmail.com>
Bring the illegal calendar timestamp scheduler fix up to date with current main. Signed-off-by: Duang777 <duangjl007@gmail.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max
评审提交:80ed6f20a8d5b5af30d1a6638ad53087db0253fd。
[P2] 无效的旧时钟会阻断新的调度重置。 提前计算历史间隔 · 新增旧时钟校验
动机 / Motivation
有现有调度收据、等待边界变化后需要恢复及时唤醒的 Agent 和维护者。 既有 30 分钟调度收据带无效 updated_at,而监控到期界限将 reset token 改为新身份,需重置到 15 分钟。 真实 store 写入并读回旧收据后,基点返回 reset_required、15 分钟且需要宿主更新和 ACK;当前直接报 updated_at must be an ISO timestamp,整个调度提示无法返回。只把旧时间换为合法值的同头对照正常重置。 独立验证 Python→真实 TS worker 与隔离 File state、实际测试套件。未改动宿主自动化或查询远端 CI。
保留旧身份失效后的自动恢复。 调度状态合同 规定 token 变化回到当前 profile 的初始 RRULE,只有同身份的后续轮次才比较旧间隔。 核验项:calendar_dates_do_not_roll, obsolete_clock_cannot_block_identity_reset。
改动思路 / Approach
沿用 runtime_timestamp 和现有 TS cadence owner。把时间检查放到真正消费旧时间的分支。 合法 calendar codec 适用于当次输入和确实用到的历史时间;失效身份只用于辨认是否重置。 复用现有 calendar codec 很合适。校验当前时间与同身份旧时间应区分;旧身份在重置分支只用来确定发生了变化。
具体改动 / Changes
- 统一日期解析:实际 native 路径:2 月 31 日在基点滚到 3 月 3 日,当前 monitor 计划拒绝,失败缓存由 1 条降为 0。合法闰日 +08:00 同样得 UTC 16:40;有效旧收据保持重置。TS 基点 46/当前 51,Python 两边各 40,类型检查通过。
- backoff 提前校验:既有 30 分钟调度收据带无效 updated_at,而监控到期界限将 reset token 改为新身份,需重置到 15 分钟。 真实 store 写入并读回旧收据后,基点返回 reset_required、15 分钟且需要宿主更新和 ACK;当前直接报 updated_at must be an ISO timestamp,整个调度提示无法返回。只把旧时间换为合法值的同头对照正常重置。
对主干的风险 / Risks
错误作用于现有状态读取,不只是手工传参。现有 store 会接收和返回这个旧 updated_at,投影失败时状态字节保持原样,因此下次继续命中,无法产生新 RRULE 和 ACK。合法收据对照正常。这里无需放松当前时间或同身份时间校验。类型、签署、差异和合入模拟通过;未查询远端 CI。
我的整体评价 / Verdict
请求修改。严格校验解决月日溢出,但旧身份的坏时间不应使新身份重置失败。 按当前 cadence 分支惰性验证旧 updated_at:只有同一身份需要判断是否到间隔时才读取。新身份的初始间隔独立计算;现用非法时间仍显式拒绝。
English verdict: REQUEST_CHANGES. Head 80ed6f2. Calendar checks fix rollover and TTL retention. An obsolete persisted acknowledgement with a bad timestamp now aborts the scheduler hint before identity_reset can propose the new initial interval. Validate the old clock only in the elapsed-time branch; keep rejecting clocks that actually inform a decision.
| // Missing interval or blank updated_at stays elapsed. A present illegal | ||
| // calendar is rejected instead of rolled or treated as elapsed. | ||
| if (appliedInterval === null) return true; | ||
| const updatedAt = requireSchedulerTimestampMilliseconds( |
There was a problem hiding this comment.
[P2] 旧身份的时间不要阻断重置。 既有 30 分钟调度收据带无效 updated_at,而监控到期界限将 reset token 改为新身份,需重置到 15 分钟。 真实 store 写入并读回旧收据后,基点返回 reset_required、15 分钟且需要宿主更新和 ACK;当前直接报 updated_at must be an ISO timestamp,整个调度提示无法返回。只把旧时间换为合法值的同头对照正常重置。 按当前 cadence 分支惰性验证旧 updated_at:只有同一身份需要判断是否到间隔时才读取。新身份的初始间隔独立计算;现用非法时间仍显式拒绝。
An illegal updated_at on a previous identity was validated before cadence decided whether that clock was an input. A reset-token change then failed closed instead of returning the new profile's initial interval. Read the old clock only on the same-identity elapsed branch. Current time and a same-identity interval comparison still reject an illegal calendar. Signed-off-by: Duang777 <duangjl007@gmail.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · max
本次评审对应提交 0461def065a18d4afcf649062d8e8458377d94c7。
[P2] 保留监控 CLI 已支持的合法小写时间。 对已到期的隔离 SQLite Monitor 执行 quota monitor-poll --next-due-at 2026-12-04t01:15:00z --execute。base 退出 0,实际保存该到期时间;本提交退出 1(monitor_poll_rejected),到期时间留在原值。同一时刻的大写写法两版都能保存,11 月 31 日两版均拒绝。新 parser 的日期时间分隔符只允许大写 T 或空格;RFC 3339 §5.6 明确允许小写。在已有校验前接受/归一化这个分隔符,不放宽日历校验。
动机 / Motivation
用 --next-due-at 回写监控的用户、现有 shell/Agent 调用,以及读回调度提示的 App 宿主。基线调度时钟会把不可能的日期滚成下一月;同一 CLI 能保存合法的小写 RFC 3339 时间。上个 PR 头在旧状态验证时拦住了新调度身份重置。新头拒绝新的非法日历,且两次原生唤醒读入相同的旧坏记录,恢复 reset_required 及 ACK 提示。合法大写、有偏移和闰日对照通过。隔离 SQLite 与真实本地 CLI/TS runtime;未调用实际宿主更新、已安装环境或托管 CI。隔离 SQLite 的真实 monitor-poll 执行:合法的 2026-12-04t01:15:00z,base 退出 0 并写回原时间,head 退出 1(monitor_poll_rejected)、到期日不变;只改大小写,大写两版都写回。
改动思路 / Approach
有界修复可复用共享 codec 接受合法大小写,保持调度 owner 的原规则。不扩到通知冷却、真实宿主或更广泛的时间格式;可保留现有 reset 分支。 保留严格日历检查和已修好的身份重置,在已有 ISO codec 上接纳合法的小写分隔符;不引入另一套日期判定。
具体改动 / Changes
关键代码讲解
基线 9ce3a46bfe525aa2167de6699fc676ce8ef60247 与新提交 0461def065a18d4afcf649062d8e8458377d94c7 的真实调用对照。上个提交会在旧记录的 updated_at 校验时堵住身份重置;当前先判断是否会消费旧间隔,原生 should-run 两次都重新给出 reset_required 与 ACK。仍有独立退化:monitor_metadata.schedule 调用已换 codec 的 schedulerTimestampMilliseconds,该解析只识别大写 T 或空格。实际 CLI 小写时刻退出 1、SQLite 到期未更新;大写仅改大小写即可在两版写回;非法 11 月 31 日两版拒绝。
依据 docs/status-data-contract.md:reject_impossible_calendar、obsolete_state_identity_reset、preserve_public_monitor_timestamp_format。
对主干的风险 / Risks
长程重置已恢复,但重复调用合法小写时间的监控会丢掉下一次写回。用户体验从原本正常的 ISO 选项变成笼统拒绝。有限的修复是接纳该合法分隔符,并保持非法月日和实际历史状态的负例。
语义与 CI 对齐
标准 premerge、124 项 TS、41 项 Python 和 typecheck 通过。实际 SQLite 监控写回与旧状态 guard 正反对照已执行;未查托管 CI、未操作真实宿主调度。
我的整体评价 / Verdict
请求修改。旧坏状态的新身份重置已验证修复;请保持合法小写时间的原生监控写回,回归测同时覆盖等价大写和不可能日期。本次评审覆盖现有宿主与 CLI 行为。隔离 SQLite 与真实本地 CLI/TS runtime;未调用实际宿主更新、已安装环境或托管 CI。
English verdict: REQUEST_CHANGES. Request changes because the new scheduler codec rejects a valid lowercase RFC 3339 timestamp accepted by the existing monitor CLI, although identity reset now recovers.
| : `${text}Z`; | ||
| const parsed = Date.parse(timezoneAware); | ||
| return Number.isNaN(parsed) ? null : parsed; | ||
| const parsed = parseIsoTimestamp(text); |
There was a problem hiding this comment.
对已到期的隔离 SQLite Monitor 执行 quota monitor-poll --next-due-at 2026-12-04t01:15:00z --execute。base 退出 0,实际保存该到期时间;本提交退出 1(monitor_poll_rejected),到期时间留在原值。同一时刻的大写写法两版都能保存,11 月 31 日两版均拒绝。新 parser 的日期时间分隔符只允许大写 T 或空格;RFC 3339 §5.6 明确允许小写。在已有校验前接受/归一化这个分隔符,不放宽日历校验。
The calendar codec only matched an uppercase T or a space, so a monitor poll that already accepted 2026-12-04t01:15:00z was rejected. Normalize that separator before the existing calendar check. Impossible dates stay rejected, and the caller's original text is still what gets stored. Signed-off-by: Duang777 <duangjl007@gmail.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · max
本次评审对应提交 451246f2a02b32b70cd85b330cb7b1197c46d13c。
动机 / Motivation
使用显式监控到期日的 CLI 调用方,以及消费调度建议的 App 宿主。基线将一部分不可能的调度日历滚入下一月。候选 80ed6f2 会在旧记录损坏时阻断新身份;0461def 修复了重置,但拒绝原本可写回的合法小写 ISO 时间。合法 UTC 大写、小写和偏移均原样写回;非法月日与重复时区不写回。两次原生唤醒都恢复新身份的 reset_required 和 ACK 提示。真实源码 CLI、TypeScript 运行时与隔离 SQLite;未调用实际宿主更新、已安装环境或托管 CI。通知冷却未改。
改动思路 / Approach
在现有 owner 恢复 RFC 合法表示;严格检查只使用实际参与策略判断的事实。调度、监控与共享 ISO 语法;通知冷却、实际宿主 ACK 与历史记录迁移不属于本补丁。 复用共享 ISO 解析器做日历验证,并按现有 cadence 分支决定是否需要读旧时钟。
具体改动 / Changes
关键代码讲解
对照 9ce3a46bfe525aa2167de6699fc676ce8ef60247 与当前提交 451246f2a02b32b70cd85b330cb7b1197c46d13c。在隔离 SQLite 上走真实 quota monitor-poll --next-due-at --execute,大写 UTC、小写 t/z 和带偏移的小写时间都逐字写回,退出码都是 0;11 月 31 日的大小写写法和重复时区后缀都退出 1,权威行保持原值。此前 0461def 会拒绝同一个合法小写输入。修复仅在 RFC 3339 §5.6 允许的位置归一化分隔符,日期继续按原日历字段验证。
quota should-run --codex-app 两次读入同一条不再适用的坏记录,均返回 reset_required 和 ACK 提示,原记录未变;这覆盖了 80ed6f2 上的重置失败。仍参与同身份间隔判断的坏 updated_at 与坏 current_time 会明确拒绝,坏的缓存时间被剔除。
依据 docs/status-data-contract.md 和现有公开 ISO 参数:reject_impossible_calendar、obsolete_state_identity_reset、preserve_public_monitor_timestamp_format 已达到。
对主干的风险 / Risks
共享 codec 同时服务租约和交接。固定时钟对照确认,合法小写、等价大写与偏移得到同一到期时刻;错误 actor、lease key 或版本仍被拒绝或返回冲突,过期及非法到期也未放行。没有新增状态、配置或存储迁移。实际参与间隔判断的历史坏时间仍会显式失败;这保留了错误信号。宿主执行与通知冷却不在本次改动范围。
语义与 CI 对齐
标准 premerge 14 项选中检查和 4 项直接检查通过;124 项调度 TypeScript、基线和新头各 54 项共享时间调用测试、60 项 Python 调度测试及 typecheck 通过。复查了实际 CLI 与 SQLite 读回、连续两次原生 guard 及共享租约反例。未查询托管 CI,也未调用真实宿主更新。
我的整体评价 / Verdict
同意当前精确提交。两个历史回归都有实际路径对照,非法输入和租约边界保持预期行为。本次评审覆盖现有宿主与 CLI 行为。真实源码 CLI、TypeScript 运行时与隔离 SQLite;未调用实际宿主更新、已安装环境或托管 CI。通知冷却未改。
English verdict: APPROVE. Approve the exact head. The real monitor path preserves legal lowercase and offset inputs, rejects impossible dates, and the scheduler can reset stale identity state without weakening lease checks.
Goal And Delivered Outcome
schedulerTimestampMillisecondsaccepted impossible calendar dates viaDate.parse. The calendar codec then rejected a legal lowercase RFC 3339 separator (2026-12-04t01:15:00z) that the monitor CLI already stored. An earlyupdated_atcheck also blockedidentity_reset.tseparator is accepted and the caller's original text is stored. An obsolete illegalupdated_atdoes not block a new identity's initial interval. Same-identity interval checks andcurrent_timestill reject an illegal calendar.main.Symptom
Reproduced on scratch
main9ce3a46bfe525aa2167de6699fc676ce8ef60247.2026-02-31T00:00:00Zbecame2026-03-03T00:00:00.000Z(1772496000000). Monitor schedule, failure-cache TTL, and a null backoff parse followed that rolled or missing instant.main)2026-02-31T00:00:00Z2026-03-03T00:00:00.000Znullgenerated_at2026-02-31T00:00:00Z2026-03-03T00:30:00Zgenerated_at must be an ISO timestamp2026-02-31T12:00:00Zexplicit_next_due_at must be an ISO timestampfailed_at2026-02-31T00:00:00Zat reference2026-03-03T00:00:00Zupdated_at2026-01-32T12:00:00Zadvance_after_intervalupdated_at must be an ISO timestampcurrent_time2026-02-31T12:10:00Zor2026-11-31T12:10:00Zadvance_after_intervalcurrent_time must be an ISO timestampupdated_at, new reset token80ed6f20threw before resetidentity_reset, 15 minutes,apply_required2026-12-04t01:15:00zmainaccepted and stored that text; head0461def06rejected itnext_due_atstays2026-12-04t01:15:00z2026-11-31t10:04:00zRoot cause
Date.parseoverflows an impossible month/day. A null parse was treated as an elapsed backoff interval. The replacement codec matched only an uppercaseTor a space before the calendar check, so RFC 3339's lowercasetseparator failed closed. Validating every persistedupdated_atbefore cadence selection also rejected an obsolete clock thatidentity_resetnever reads.Minimal fix
parseIsoTimestampstill round-trips calendar fields. Before that grammar it rewrites only a lowercasetbetween the date andHH:. The stored monitor due time remains the caller's original text.updated_atis read only when the same identity is comparing the applied interval.current_time, monitorgenerated_at/explicit_next_due_at, and a present failure-cachereference_timestill fail closed when they are illegal. A missing or blankupdated_aton the elapsed branch stays elapsed. An omitted failure-cache reference time still uses the wall clock.Author Declaration
Implemented against
docs/status-data-contract.md: a changedreset_tokenreturns to the current profile's initial interval. Lowercasetfollows RFC 3339 §5.6. Reviews on80ed6f20and0461def06.parseIsoTimestamp,schedulerTimestampMillisecondsscheduler_state_store.test.tscurrent_time, same-identity elapsed checkscheduler_state_transition_rules.test.tsupdated_atmust not blockidentity_resetcadenceConsumesAppliedIntervaltseparator stays acceptable and is stored unchangedparseIsoTimestamp2026-12-04t01:15:00zschedule test2026-11-31t10:04:00z,2026-02-31t00:00:00zmainreproduced the rollover. Later heads added the elapsed-branch check and the lowercase separator. Did not re-run the isolated SQLitequota monitor-poll --executeprocess. Did not merge.Scope And Continuation
hint.tsnotification cooldown still usesDate.parseon an ISO string already taken fromrequest.timestamps.Validation
451246f2a02b32b70cd85b330cb7b1197c46d13cregression_parityfailedonmainfor the original illegal-calendar testsmain9ce3a46bf: the two scheduler files, 29 tests, 25 pass, 4 fail, exit 1.unitpassedtests/control_plane_ts/scheduler_*.test.ts, 124 pass, 0 fail.tests/control_plane_ts/monitor_metadata.test.tsincluded in a combined 42-pass run with the two scheduler files. Node v22.23.4.staticpassednpm run typecheck:control-plane, exit 0. CI pins Node 22.22.3; this run used v22.23.4 (engines.node>=22.22.3).integrationnot_runquota monitor-poll --executeand no live host heartbeat on this head.real_backendnot_applicablehold_active_initialdo not read an obsoleteupdated_at. The SQLite CLI writeback named in review was not re-executed here.See validation disclosure guidance.
Frontend / Visual Evidence
Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
N/A. This reuses
parseIsoTimestampand does not claim migration or shared Goal Authority progress.Residual risk
hint.tsnotificationCooldownstill callsDate.parseonfailed.isofromrequest.timestamps.current_timeis still rejected when it is present and illegal, including during identity reset, because failure-cache retention uses it.failed_aton a cached failure is dropped by the TTL filter.reference_timestill usesDate.now().quota monitor-poll --executecommand.Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).python3 scripts/check_rfc_bilingual.py --base origin/main --head HEADpasses, or no paired RFC changed.none.Signed-off-bytrailer (git commit -s).