Skip to content

audit 2026-09-05 (wave 2): artefact-keyed freshness, start sentinel, tz-aware time, anchored regex - #6

Merged
jimy-r merged 4 commits into
mainfrom
audit/2026-09-05-w2
Sep 5, 2026
Merged

audit 2026-09-05 (wave 2): artefact-keyed freshness, start sentinel, tz-aware time, anchored regex#6
jimy-r merged 4 commits into
mainfrom
audit/2026-09-05-w2

Conversation

@jimy-r

@jimy-r jimy-r commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Four fixes from the 2026-09-05 audit pass, all in deadmans.py. Two correct states the checker was reporting wrongly, two add optional config keys that stay inert unless you set them. Existing configs behave exactly as they did.

Tests: 121 pass (was 60), ruff check and ruff format --check clean, deadmans.py selftest passes. DTZ is now in the ruff select set so the naive-datetime shape cannot come back.

Read log stamps and the clock in a configurable timezone (1f0f52ff)

A log filename carries no offset, so strptime on 2026-01-15-0000 compared against a naive datetime.now() skewed every age by the gap between the producer's clock and the checker's. A UTC-stamping job read from a UTC+10 host reported ten hours fresher than it was, which is the wrong direction for a tool whose job is catching what stopped.

New optional top-level timezone key naming the clock that writes the stamps: local (default), UTC, a fixed offset like +10:00, or an IANA zone name. Both sides of the subtraction are now aware, and an unresolvable zone is a config error rather than a silent fall back to the wrong clock. A caller passing a naive now still works, read in the configured zone.

Require the sentinel to open a line (8b24219f)

check_task tested task.sentinel in body, so a log that only mentioned the string reported FRESH. A run quoting its own last failure, a summary line naming the sentinel it went looking for, a config pasted into the output: each passed the switch. That is a false FRESH, strictly worse than the false finding it avoids, and the same bug applied to failure_sentinel in reverse.

Both tests now anchor to the start of a line, tolerating the indentation, backticks, asterisks and underscores a real sentinel line picks up, and stripping a leading byte order mark first. A word boundary stops MY_JOB_OK matching MY_JOB_OK_PENDING, dropped for sentinels that do not end in a word character.

Optional start sentinel, so a hung run reports HUNG (c31eb5a4)

The state machine could not tell a job the scheduler skipped from a job that fired and wedged halfway. Both produced a log with no success string, so both reported NO_SENTINEL, and those are different problems with different fixes.

New optional per-task start_sentinel the job writes as its first act. A log carrying the start string but neither success nor failure reports HUNG, with the detail line saying how long ago the run began. Optional max_runtime_hours is the allowance before that counts as hung; inside it the task reports RUNNING, which passes, so checking while a long job is genuinely mid-flight raises nothing. Staleness still short-circuits, so an unfinished start past max_age_hours is STALE as before.

Optional artefact source, so freshness can key on what a job produces (41d7ecff)

A log line only exists if the job ran through whatever wrapper writes the log. Run the same job another way and the log stays put while the real work happens, so the switch reports a permanent false-stale on a healthy task. A permanent false-stale trains you to ignore the switch, which is the one outcome worse than not having it.

New optional per-task artefact object naming what the job produces. format: "mtime" takes the file's modification time; format: "jsonl" takes the newest timestamp_field across the records of a newline-delimited JSON file. match filters those records by field regex, so an unrelated append to a shared file cannot report the lane fresh. When the artefact is the newer signal it decides the state and the older log's sentinel is not scored, because the artefact already shows the job produced its output.

Notes

  • Config reference, the new states and each new key are documented in README.md, with timezone, start_sentinel and max_runtime_hours added to deadmans.example.json.
  • #3 (PyPI packaging) also touches README.md, in the Install section above Quickstart. This branch only edits from Config reference down, so the two should merge cleanly in either order.

🤖 Generated with Claude Code

jimy-r and others added 4 commits September 5, 2026 10:52
A log filename carries no offset, so a naive strptime of "2026-01-15-0000"
compared against a naive datetime.now() skewed every age by the gap between
the producer's clock and the checker's. A UTC-stamping job read from a UTC+10
host reported ten hours fresher than it was -- the wrong direction for a tool
whose whole job is catching what stopped.

Adds an optional top-level `timezone` key naming the clock that writes the log
stamps: "local" (default), "UTC", a fixed offset like "+10:00", or an IANA zone
name. Both sides of the age subtraction are now aware. An unresolvable zone
raises a ConfigError rather than falling back silently to the wrong clock.

A caller passing a naive `now` still works -- it is read in the configured
zone -- so existing embedders and the fixed-clock tests are unaffected.

Enables ruff's DTZ rules so the naive-datetime shape cannot come back, with
the test module exempted where its fixed naive clocks are the fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_task tested `task.sentinel in body`, so a log that only mentions the
string reported FRESH. A run quoting its own last failure, a summary line
naming the sentinel it went looking for, a config pasted into the output --
each passed the switch. That is a false FRESH, strictly worse than the false
finding it avoids, and the same bug applied to failure_sentinel in reverse.

Both tests now anchor to the start of a line, tolerating the indentation,
backticks, asterisks and underscores a real sentinel line picks up on the way
into a log, and stripping a leading byte order mark first. A word boundary
stops MY_JOB_OK matching MY_JOB_OK_PENDING, and is dropped for sentinels that
do not end in a word character.

Ported from the same fix made to this pattern's origin implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…INEL

The state machine could not tell a job the scheduler skipped from a job that
fired and wedged halfway. Both produced a log with no success string, so both
reported NO_SENTINEL, and those are different problems with different fixes.

Adds an optional per-task `start_sentinel` the job writes as its first act. A
log carrying the start string but neither success nor failure now reports HUNG,
with the detail line saying how long ago the run began. Optional
`max_runtime_hours` is the allowance before that counts as hung -- inside it
the task reports RUNNING, which passes, so checking while a long job is
genuinely mid-flight raises nothing.

Staleness still short-circuits, so an unfinished start past max_age_hours is
STALE as before. Tasks that configure neither key behave exactly as they did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…duces

A log line only exists if the job ran through whatever wrapper writes the log.
Run the same job another way -- by hand, from another host, through an agent
rather than its cron entry -- and the log stays put while the real work
happens, so the switch reports a permanent false-stale on a healthy task. The
lane this pattern came from sat at CRITICAL for ten days that way.

Adds an optional per-task `artefact` object naming what the job actually
produces. `format: "mtime"` takes the file's modification time; `format:
"jsonl"` takes the newest `timestamp_field` across the records of a
newline-delimited JSON file. `match` filters those records by field regex, so
an unrelated append to a shared file cannot report the lane fresh.

When the artefact is the newer signal it decides the state and the older log's
sentinel is not scored, because the artefact already shows the job produced its
output. When the log is newer nothing changes, and a task with no `artefact`
behaves exactly as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jimy-r
jimy-r merged commit 1af2d36 into main Sep 5, 2026
3 checks passed
@jimy-r
jimy-r deleted the audit/2026-09-05-w2 branch September 5, 2026 23:33
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.

1 participant