Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ First release as a standalone engine, extracted from [Agent-Notification](https:
- The monitor's repository-root lookup no longer mangles directory names with trailing whitespace (B129): `_fingerprints` read `git rev-parse --show-toplevel` with `text=True` and applied `strip()` - universal-newline translation rewrote a real carriage return at the end of the repository directory name on top of Git's record separator, and `strip()` cut off genuine trailing spaces, tabs and newlines - so the sampled root landed on the trimmed sibling path. When that trimmed path did not exist, every status path was recorded as missing and a quiet executor rewriting the same non-ignored file stopped refreshing the activity clock (false stall); when it did exist, the sampler watched the wrong directory and real activity in this repository did not count. The `rev-parse` probe now captures bytes and removes exactly the one trailing `\n` that the Git output protocol appends; all other bytes decode into the root path verbatim, with no guessing, no fallback to a similarly named directory and no scanning of neighbouring directories, and with `cwd` inside a subdirectory `--show-toplevel` still resolves the exact root. B128's file-metadata sampling, `-z` and `-uall` status parsing, the all-intermediate-components and last-component symlink boundaries, symlink-itself activity and missing-file handling are preserved; no recursive scans, no reading file contents for hashing, no loosened `.git` or ignored-directory sampling. `run_monitored` and `PiHost` public signatures and return values, natural-exit `ok`/`error` and returncode, deadline/stop priority and the process-group termination policy are unchanged, and no configuration, dependencies or event fields are added. **Migration:** none.
- Post-executor parsing no longer crashes on a top-level non-object JSON line in the event stream (B130): `PiHost.parse`, `parse_observability` and `parse_context` (through `_last_assistant_text`) called `event.get` directly after a successful `json.loads`, so one line of `null`, an array, boolean, number or bare string - the real host can emit one after otherwise normal events, with the subprocess already exited successfully - raised `AttributeError` out of `Dispatcher.run_executor`'s post-processing, and out of the standalone context/observability entries (which some callers downgrade to `unknown` or zero counts). Such values are not protocol events: all three parse entries now skip the whole line and continue with the surrounding object events, never expanding, re-parsing or trusting forged events or context markers embedded in strings/arrays. Model, token/cost usage, guard-denial reasons and tool-call counts of legal object events are unchanged, and the last legal assistant completion message remains the context authority - later non-object noise cannot overwrite, clear or forge it (a final message with no marker still reports `unknown`, an invalid report still `invalid`, tool-output markers are still not trusted, and an older report never stands in for the last message). A stream of only non-object noise returns the existing no-valid-event defaults: `parse` → `("", {}, {})`, context → `unknown`/empty items/`no_marker`, observability → zero tool counts/empty `missing_context`. Broken-JSON lines and unknown-typed objects are handled exactly as before, file read errors still propagate instead of being swallowed into empty results, and no nested field validation is added; `_context_item`/`_safe_context_ref`, context categories/short IDs/privacy filtering, guard decisions, usage/cost computation, the valid-event and report formats and the `PiHost`/`RunResult` public signatures are untouched. No configuration keys. **Migration:** none.
- The `events` and `trace` queries now order events by the actual instant instead of the timestamp text (B131): both entry points and the segment list sorted by the raw `ts` string, so mixing legal ISO forms reordered history - `2026-10-16T10:00:00+08:00` (really 02:00Z) landed after `02:30:00Z` and `trace` misreported the later event as the first failure, second-precision and fractional-second Zulu strings could invert, and segment order plus segment start/end bounds followed the wrong order. Both queries now sort by the parsed actual moment (naive strings keep the existing UTC reading; aware values are compared as instants without re-normalizing the representation, so legal extreme-year stamps such as `0001-01-01T00:00:00+01:00` and `9999-12-31T23:59:59-01:00` - whose UTC conversion would leave `datetime`'s native range - order correctly instead of overflowing, and are neither dropped nor demoted to unparseable), with ties broken deterministically by `source` and then `seq`. Legacy timestamps that no longer parse sort after all parseable ones by `source`/`seq`, keep their original text verbatim and raise nothing. Segments are still assembled by the existing source/run-record window rules (attempt, head and count attribution unchanged; a moment that cannot be represented in UTC safely misses every representable window); the segment list is ordered by each segment's actual first instant, and `start`/`end` keep the original strings while representing the segment's real first and last events. First-failure detection still distinguishes `fail`/`deny`/`error`, and the longest stage, filter sets, count/total, every JSON field, the text layout, the stored timestamp format and the database/chain/export/import behaviour are unchanged; queries append no events, invalid arguments still exit 2 and empty queries exit 0. **Migration:** none.
- `events --since` no longer crashes with a traceback on values `datetime` parses natively but that cannot be represented in UTC (B132): an extreme-year stamp with an offset (`0001-01-01T00:00:00+01:00`, `9999-12-31T23:59:59-01:00` - legal ISO whose conversion to UTC would leave `datetime`'s native year range) raised `OverflowError` out of `astimezone`, and an oversized non-negative relative duration raised it out of the `timedelta` construction (`1000000000d` exceeds the `timedelta` cap) or out of the subtraction from the current moment (`999999999d` underflows below year 1) - all four escaped `events_main` as a crash instead of the documented argument error. The parser now converts exactly those input-parsing/time-arithmetic overflows into `ValueError` (one guard around the duration construction and subtraction, one around the UTC normalization, cause chained away), so the existing parameter-error path applies unchanged: text and `--json` both exit 2, stderr names `--since` and echoes the value, stdout stays empty, and the rejection happens before `events_io.query` runs. Unrepresentable values are rejected outright - no clamping, no substitution of `datetime` extremes, no silent fall-back to an unfiltered or empty query. Everything else keeps its interpretation: relative `d/h/m/s` durations, ISO with `Z`/naive/positive-or-negative offsets/surrounding whitespace, and the representable UTC boundaries of years 0001 and 9999 still filter as before with the boundary included, filter sets and count/total/stage/status counts are unchanged, `events_io`'s accepted range is not widened, and malformed inputs keep exiting 2. For valid input, downstream `OSError`/`OverflowError` from storage still propagate as-is (no broader exception swallowing, storage failures are not disguised as argument errors); B131's actual-instant ordering, first-failure reporting and segmentation, the stored timestamp text, every JSON field and the text layout, and the event/database/hash-chain/export/import behaviour are untouched, and no CLI parameter or public signature changes. **Migration:** none.

- On the `pilot` branch only, `docs/backlog.md` and reserved `tests/test_pilot_*.py` files are exempt from Code Owner review so eligible pilot changes can merge through the approved host gate. Ordinary tests, workflow files, and CODEOWNERS itself retain owner review; executor edits to tests remain forbidden and task repro bytes remain checked against the owner-provided task base. **Migration:** none for engine users; this is a pilot repository policy change.

Expand Down
1 change: 1 addition & 0 deletions docs/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,4 @@ B34–B41 的处理建议(B35 与 B36 先做、B41 放弃等)用户 2026-09-
| B129 | 监测采样对仓库根路径的空白不保真(root-path-whitespace-false-stall):`_fingerprints` 用 `text=True` 读 `git rev-parse --show-toplevel` 再 `strip()`——通用换行转换会把目录名末尾真实的回车连同 Git 追加的记录分隔符一起改写,`strip()` 则把名字末尾的空格、制表符、换行一并截掉,采样根随之落到被截短的同名路径上:截短路径不存在时每条状态路径都按缺失记,安静执行方持续改写同一非忽略文件不再刷新活动时钟,被误判 stall;截短路径存在时采的是别的目录,本仓库的真实活动也不计入。修法:`rev-parse` 改按字节取输出,只去掉 Git 输出协议在末尾追加的那一个 `\n` 记录分隔符,其余字节(含名字末尾空白与回车)原样解码为根路径;不猜测、不回退名字相似的目录、不扫描旁边目录修复,cwd 在仓库子目录时 `--show-toplevel` 照旧定位真实根。B128 已有的文件元数据采样、`-z` 与 `-uall`、全部中间目录与末分量符号链接边界、链接自身活动、文件消失按缺失记均保留,仍不递归扫描、不读文件正文做哈希、不放宽 .git 与忽略目录的采样;`run_monitored` 与 `PiHost` 公开签名/返回值、自然退出 ok/error 与 returncode、deadline/stop 优先级及进程组终止策略不变,不加配置、依赖或事件字段。验证:截短名字的旁边目录存在时,其中同名文件持续变化不计入本仓库活动,本仓库安静不动仍照常 stall | 2026-10-16,#237(宿主回填),pilot 分支试点 TASK-20261016-01 |
| B130 | 顶层非对象 JSON 让已完成的执行在后处理崩溃(nonobject-json-postprocess-crash):`PiHost.parse`、`parse_observability` 与 `parse_context`(经 `_last_assistant_text`)在 `json.loads` 成功后直接 `event.get`,事件流夹一行顶层 null、数组、布尔、数字或裸字符串(真实宿主输出正常事件后可以以此结束,子进程已正常退出)时抛 AttributeError——`Dispatcher.run_executor` 在执行成功后的解析阶段崩掉;单独调用上下文/观察入口同样抛异常,部分调用方降级 unknown 或零计数。修法:顶层非对象 JSON 值不是协议事件,三个解析入口统一整行忽略、继续处理其余对象事件;字符串/数组内伪造的事件或上下文标记不展开、不二次解析、不采信。合法对象事件的模型、usage/费用、守卫拒绝理由与工具调用计数不变;最后一条合法 assistant 完成消息仍是上下文权威来源,后续非对象噪声不覆盖、不清空、不伪造它(末条无标记仍 unknown、真实报告无效仍 invalid、工具输出标记仍不采信、不以旧报告代替)。全是非对象噪声时按无有效事件默认值返回:parse 为 `("", {}, {})`、context 为 unknown/空 items/no_marker、observability 为零工具计数/空 missing_context;坏 JSON 与未知类型对象的处理保持,文件读取异常原样上抛、不以吞异常的空值代替解析,不新增嵌套字段校验,不改其他错误处理;`_context_item`/`_safe_context_ref`、上下文类别/短 ID/隐私过滤、守卫判定、usage/费用计算、有效事件与报告格式、`PiHost`/`RunResult` 公开签名均不变。验证:真实 run_executor 在成功子进程输出混入五类非对象及空数组/伪造 assistant/伪造 tool_execution_end 数组噪声后仍返回 ok/0,且合法事件的 usage、guard 计数与 missing_context 保持 | 2026-10-16,#240(宿主回填),pilot 分支试点 TASK-20261016-02 |
| B131 | events/trace 按时间戳原文排序使合法 ISO 时区/精度混用排错(trace-iso-time-order):events_main 与 _timeline 的排序键、_segment_events 的分段序都是 ts 原文的字典序,实际 02:00Z 的 `2026-10-16T10:00:00+08:00` 被排到 `02:30Z` 之后,trace 把后者误报为首个失败;秒精度 Z 与小数秒 Z(`02:00:00Z` 对 `02:00:00.100Z`)也会倒序,分段列表顺序与段首尾随之错乱。修法:两个查询入口统一按解析后的实际时刻排序(naive 沿用既有解释按 UTC 对待;aware 直接按时刻比较、不规范化表示——0001/9999 极端年份带偏移换算到 UTC 会越过 datetime 原生范围,时刻比较不受影响),同一时刻沿用 source、seq 确定性次序;无法解析的历史旧串排在可解析之后、按 source/seq 定序,原文逐字保留、不抛异常。分段仍按既有来源/运行记录窗口规则组装(attempt/head/count 归属不变),分段列表改按段内实际起始时刻排序,start/end 因事件已按实际时刻排序而天然是该段实际首尾;运行记录窗口解析对不能表示到 UTC 的时刻安全返回无命中(不可能命中可表示的有效窗口),正常窗口归属不变。首个失败仍区分 fail/deny/error,最长阶段、筛选集合、count/total、JSON 字段与文本布局、时间戳保存格式、数据库/哈希链/导出/导入行为均不变,查询不追加事件、不触网,非法参数退出 2、空查询退出 0 保持。验证:+08:00 与 Z 混排的 events/trace 均按实际时刻排序且首失败取较早一条;同段内不同表示的段首尾取实际首尾;极端年份 0001-01-01T00:00:00+01:00、9999-12-31T23:59:59-01:00 的显示排序不溢出、不丢弃、保留 source/seq 与原文;合法串与多个不可解析历史串混排按合法在前、未知按 source/seq 定序 | 2026-10-16,#242(宿主回填),pilot 分支试点 TASK-20261016-03 |
| B132 | events `--since` 对 datetime 原生可读但换算到 UTC 越界的输入崩溃(since-input-overflow):`_parse_since` 对极端年份带偏移的时间戳直接 `astimezone(UTC)`(`0001-01-01T00:00:00+01:00`、`9999-12-31T23:59:59-01:00`,datetime 能解析、换算越过原生 1–9999 年边界),对过大的非负相对时长直接构造 `timedelta` 并与当前时刻相减(`1000000000d` 溢出 timedelta 上限、`999999999d` 构造成功但相减下溢到年份 0 以下),四类输入的 OverflowError 都逃出 `events_main`,命令以 traceback 崩溃而不是按参数错误返回 2。修法:仅 `_parse_since` 内把输入解析/时间计算的溢出转 ValueError——相对时长的 timedelta 构造与相减一处、ISO 的 UTC 换算一处,`from None` 保留「直接原因」语义,沿用 `events_main` 既有参数错误路径(文本与 --json 均退出 2、stderr 写明 --since 与原输入、stdout 为空、无 traceback),且失败发生在 `events_io.query` 之前(不被调用的筛选不落库);明确拒绝不可表示值,不钳制、不替换为 datetime 极值、不回退成无筛选或空结果。有效输入解释不变:d/h/m/s 相对时长、ISO 的 Z/naive/正负偏移/首尾空白、可表示的 0001/9999 UTC 边界(`0001-01-01T00:00:00Z`、`0001-01-01T01:00:00+01:00`、`9999-12-31T23:59:59Z`、`9999-12-31T22:59:59-01:00`)含义与含边界的筛选集合、count/total/stage/status 计数不变,不扩大 events_io 接受范围;畸形输入(not-a-time、1.5d、1w、2026-02-30)照旧退出 2;有效输入的下游 OSError/OverflowError 原样上抛,不新增宽泛吞异常、不把存储异常伪装成输入错误;B131 的显示排序/首失败/分段、时间戳原文、字段与文本布局、事件/数据库/哈希链/导出导入及只读查询保持,CLI 参数与公共签名不变。验证:四类超界输入文本与 --json 均退出 2 且 stderr 含 --since、无 traceback、`events_io.query` 未被调用、解析器抛 ValueError;1d/12h/60m/30s 含边界筛选与 8 事件计数、五种 ISO 表示的 deny 筛选集合与计数、可表示极端年份边界的命中/不命中、畸形输入退出 2、拒绝参数前后导出包与链核对不变、query 的 OSError/OverflowError 传播保持 | 2026-10-16,#243(宿主回填),pilot 分支试点 TASK-20261016-04 |
17 changes: 14 additions & 3 deletions engine/reports/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,26 @@


def _parse_since(text: str) -> datetime:
"""--since 的取值:<n><d|h|m|s> 相对时长或 ISO 时间戳;不合法抛 ValueError(命令转退出码 2)。"""
"""--since 的取值:<n><d|h|m|s> 相对时长或 ISO 时间戳;不合法或超出 UTC 可表示范围抛
ValueError(命令转退出码 2),不钳制、不回退。

datetime 原生可读但换算到 UTC 越界的值(0001/9999 极端年份带偏移)与过大的相对时长
(timedelta 构造溢出、与当前时刻相减下溢)同样按参数错误拒绝。
"""
match = _DURATION_RE.fullmatch(text.strip())
if match:
return datetime.now(UTC) - timedelta(seconds=int(match[1]) * _UNITS[match[2]])
try:
return datetime.now(UTC) - timedelta(seconds=int(match[1]) * _UNITS[match[2]])
except OverflowError:
raise ValueError(f"--since 时长超出可表示的 UTC 范围:{text}") from None
try:
parsed = datetime.fromisoformat(text.strip())
except ValueError:
raise ValueError(f"--since 无法解析为时长或 UTC 时间:{text}") from None
return parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed.astimezone(UTC)
try:
return parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed.astimezone(UTC)
except OverflowError:
raise ValueError(f"--since 时间换算到 UTC 超出可表示范围:{text}") from None


def _parse_ts(value) -> datetime | None:
Expand Down
Loading
Loading