fix: check agent.timer before filing a logwatch tick-gap finding #421
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 16 minutes 43 seconds
Due date
igor
16 minutes 43 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!421
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/420-logwatch-files-false-tick-gap-tickets-when-the"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
agent.timerbefore filing a logwatch tick-gap findinglogwatch_timer_transitioned(lib/logwatch.sh) -- true when a companion*.timerunit's own journal shows a Stopped/Started transition in the reviewed window (systemd only logs these on an actual state change, never on ordinary per-minute firing)logwatch_review_unit/do_logwatch_tick(bin/tick.sh): a unit's companion timer is discovered from the samesystemd/directory listing already used for.servicediscoverylogwatch_timer_transitionedcovering all three scenarios from the issue plus two regex-precision checks (Starting/Stoppingdon't count; a transition line for a different unit doesn't count)CLAUDE.md's logwatch sectionTest plan
make testpasses (includes the newbin/test-logwatch.shcases)make lint-- no new shellcheck warnings introduced (2 pre-existing warnings in unrelated files/lines remain)bash -non all three touched shell filesjournalctl/model-call path can't be exercised outside a live host, which is why the fix is covered at the pure-function level (logwatch_timer_transitioned) per the existing test style inbin/test-logwatch.shCloses #420
agent.timerbefore filing a logwatch tick-gap finding🤖 Review —
REQUEST_CHANGES(automated)CI for
2a6462bf: successAdds
logwatch_timer_transitionedand wires a companion-*.timercheck intologwatch_review_unit/do_logwatch_tickso an operator pause explains a silent journal. The pure function is fine; the wiring introduces a new false-positive filing path that I think is a regression, plus a cadence assumption that doesn't hold for every discovered repo.Blocking
1. A pause longer than one hour now files a finding that asserts the opposite of the truth (
bin/tick.sh~2450-2470).The timer journal is fetched with the same
--since win_start --until win_endwindow as the service journal. So asystemctl stop agent.timerat 10:30 that lasts until 14:00 produces:"${timer_unit} was continuously active this window (no Stopped/Started transition observed) ... This silence is NOT explained by the timer and is itself failure-worthy."The timer was stopped the whole time. Before this PR those hours were silently skipped; now they file a ticket whose body is factually wrong. The fix for #420 makes the >1h version of #420 worse. "No transition inside this hour" is not evidence of "continuously active" — it's evidence of "no state change in this hour", which is equally consistent with continuously stopped.
Fixed looks like: consult the timer's current state rather than inferring it (
systemctl --user show -p ActiveState --value "$timer_unit", oris-active), and/or widen the timer journal lookback pastwin_startand use the last transition seen. Skip when the last known state is stopped.2. Companion
.timeris treated as "per-minute cadence" without reading the timer (bin/tick.shdo_logwatch_tick, prompt text ~2535).Discovery walks every bot-accessible repo that declares systemd units. Any repo with a daily/weekly/
OnCalendartimer + service pair now hits the empty-journal branch for ~23 of 24 hourly passes, each time telling the reviewer "for a timer-driven unit that silence is itself the failure". Nothing in the code readsOnCalendar/OnUnitActiveSec/OnBootSecto establish that the unit is actually expected to fire within the hour. Dedup limits the spam but doesn't make the first ticket correct. Either gate the new branch on an actual sub-hourly cadence parsed from the timer file, or restrict it to units whose cadence you can verify.3. Verify
set -ebehavior on the un-guarded command substitution (bin/tick.sh, discovery loop).Old:
units=$(forgejo_repo_list_dir ... | grep ... || true)— the|| trueswallowed a failing lookup.New:
dir_listing=$(forgejo_repo_list_dir "$r_name" "systemd" 2>/dev/null)with no|| true. Repos without asystemd/dir are the common case, and ifforgejo_repo_list_dirreturns nonzero there, this assignment now carries a nonzero status. Iftick.shruns underset -e/errexit, that aborts the whole logwatch tick on the first non-systemd repo instead ofcontinue-ing. I can't see the script'ssetline from the diff — please confirm, and add|| trueregardless since it costs nothing.Non-blocking
logwatch_timer_transitioned, which is the trivial part; none exercise the new branching inlogwatch_review_unit(transitioned + empty → skip/return 1; not-transitioned + empty → synthetic journal +timer_note+ return 0; transitioned + non-empty → note), nor thebase/timer_unitpairing in discovery. That branching is where both bugs above live.logwatch_timer_transitioned's regex matches any*.timer, not the specific unit; harmless today because the journal is-u "$timer_unit"-scoped, but the test labelled "a Started/Stopped line for a DIFFERENT unit does not count" actually only proves that a non-timer line doesn't count. Consider passing the unit name in if you want that guarantee.${timer_section}interpolates with leading blank lines even when empty → a couple of stray newlines in the prompt. Cosmetic.Clean
CLAUDE.mdgenuinely documents the new behavior as claimed, and the "Do NOT file / DO file" prompt additions are consistent with the section ordering.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 1 (automated)
Addressed the review on
claude-opus-5at effort high — 1 new commit(s).🤖 Review —
COMMENT(automated)CI for
367e3fee: successAdds timer-awareness to the hourly logwatch pass so a paused
*.timerexplains a silent service journal instead of producing a phantom tick-gap ticket (igor#420). Core logic reads sound and errs conservative (unknown/paused ⇒ skip), CI is green, scope is on-issue, size is within budget. Three things I want a human to weigh before merge — none of them break the #420 fix itself.1.
logwatch_timer_subhourlyaccepts exactly 3600s — that can re-create the false ticket this PR exists to kill (lib/logwatch.sh,secs -le 3600)OnUnitActiveSec=/OnUnitInactiveSec=are measured from the paired service's activation/deactivation, not from a wall clock, so an "hourly" timer's effective period is 3600 + service runtime + up toAccuracySec(default 60s) of deferral. Firings drift later, and eventually a clock hour contains zero firings — at which point the new branch turns an empty journal into a "Timer status: silence is itself failure-worthy" prompt and files.OnCalendar=hourly(fixed:00:00) is safe; the monotonic forms are not. Suggested fix:-lt 3600(or a margin like-le 1800) for theOnUnit*Secbranch only, and flip the"OnUnitActiveSec=1h -> exactly hourly, still counts"assertion inbin/test-logwatch.sh.2. The new finding path depends on
forgejo_repo_get_file, which isn't visible anywhere in this diff (bin/tick.sh~2710)timer_file=$(forgejo_repo_get_file "$r_name" "systemd/${base}.timer" 2>/dev/null || true)— I can't confirm from the diff that (a) the helper exists and (b) it returns decoded file contents rather than the Forgejo contents-API base64 blob. Either failure is masked by|| trueand silently yieldstimer_subhourly=0forever, making the entire "active timer + silent unit ⇒ file" branch dead code with no test or log to reveal it. Please confirm the helper's contract (an existing call site would satisfy me); if it can return base64, decode it.3. PR description no longer matches the diff
The checklist describes a two-state design built on
logwatch_timer_transitioned. The diff actually ships five new functions —logwatch_timer_last_transition,logwatch_timer_verdict,logwatch_timer_lookback_since,logwatch_timespan_secs,logwatch_calendar_hourly,logwatch_timer_subhourly— including a general systemd timespan andOnCalendarparser, and the "empty journal ⇒ Timer status note" bullet is now additionally gated onis-activeand declared cadence. Nothing checked is fabricated (every box maps to real code), but the description under-describes the change; a human trusting the checklist would not know a calendar-expression parser is in here. Worth updating so the CLAUDE.md text and the description agree.Smaller notes, non-blocking:
Started|Stopped <unit>matching assumes systemd ≥ v250, which logs the unit ID; older systemd logs the Description instead, so the in-window signal would silently never fire there. Theprior/is-activesignals still cover it, so this degrades safely — just noting the test fixtures only cover the new format.logwatch_timer_subhourlytreats everyOnCalendar=/OnUnit*Sec=line as additive; systemd resets the list on an empty assignment (OnCalendar=), soOnCalendar=hourly+OnCalendar=+OnCalendar=dailywould be judged sub-hourly. Obscure, but it's the one direction that produces a false ticket.LOGWATCH_TIMER_LOOKBACK_HOURS'sSC2034comment says "read only by bin/tick.sh" — it's read bylogwatch_timer_lookback_sincein the same file.bin/test-logwatch.shruns underset -uo pipefail(no-e), so a mistyped/undefined assertion helper would print to stderr and still exit 0.yesis provably defined (CI would hang on coreutilsyes); worth a glance thateqandnoare too, since both are new usages here.Test coverage on the pure functions is genuinely good (verdict matrix, lookback direction, cadence parsing, regex precision). What's untested is the wiring in
logwatch_review_unit/do_logwatch_tick— the discovery→get_file→subhourlychain in item 2 is exactly the part with no coverage and the part most likely to be silently inert.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.