Skip to content

emrg: planted-fire staleness alarm — last_planted age check (issue #1086) - #1088

Merged
argszero merged 1 commit into
masterfrom
feature/planted-fire-staleness-alarm
Aug 31, 2026
Merged

emrg: planted-fire staleness alarm — last_planted age check (issue #1086)#1088
argszero merged 1 commit into
masterfrom
feature/planted-fire-staleness-alarm

Conversation

@argszero

Copy link
Copy Markdown
Owner

Implements issue #1086 (reader suggestion from heinrichneb on post 4, Dev.to 3doei): a planted fire that stops being scheduled rots as silently as the detector it guards — the meta-loop needs a third, boring layer: an age alarm on the planted fire's last_planted timestamp.

Changes

Marker (persistence)_refresh_usage_anchor now touches a single-line timestamp marker (~/.emrg/logs/planted-fire-heartbeat) on every round, including the skip paths (no usage / no prompt_tokens). The marker tracks round-loop liveness, not just detector runs — consistent with the #1073 heartbeat philosophy. Overwrite (not append) so the age check is a one-line read; OSError is debug-logged (stats writes never take the daemon down).

Alarm (stateless age check)_check_planted_fire_stale() reads the marker's age and logs the greppable planted-fire-stale warning when now - last_heartbeat > N days (N = 7, _PLANTED_FIRE_STALE_DAYS, kept next to _SILENT_DRIFT_THRESHOLD per the issue discussion). Idle boundary per the accepted option (a): a stale marker is the same actionable signal whether the detector died or the daemon was idle (surface-the-silence, consistent with #585/#1073). Missing/unparsable marker (fresh daemon, never ran a round) does NOT false-alarm — debug log, returns False.

Cadence_planted_fire_alarm_loop() runs every 6h (low-frequency; checking a timestamp's age is cheap), registered at daemon startup; failures are debug-logged and never crash the daemon.

Tests (positive + negative states, per the issue's acceptance criteria)

  • test_planted_fire_marker_written_every_round — marker persisted on skip path AND normal round; single line, timestamp advances.
  • test_planted_fire_stale_alarm_fires — marker age > N → planted-fire-stale warning + True.
  • test_planted_fire_fresh_marker_no_alarm — fresh marker → no alarm.
  • test_planted_fire_no_marker_no_alarm — missing marker (fresh daemon) → no alarm.
  • test_planted_fire_unparsable_marker_no_alarm — corrupted marker → no alarm, no crash.

Full local verification: pytest 1190 passed + 1 skipped (Agent.md count 1186→1191), import + CLI green.

Fixes#1086

)
Reader suggestion (heinrichneb, Dev.to 3doei): a planted fire that stops
being scheduled rots as silently as the detector it guards. Add the
third layer of the meta-loop:
- Marker: _refresh_usage_anchor touches a single-line timestamp marker
(~/.emrg/logs/planted-fire-heartbeat) on every round, including skip
paths — proves round-loop liveness, not just detector runs.
- Alarm: _check_planted_fire_stale reads the marker age; logs greppable
'planted-fire-stale' warning when age > 7 days (configurable
_PLANTED_FIRE_STALE_DAYS next to _SILENT_DRIFT_THRESHOLD). Idle
boundary per accepted option (a): a stale marker is the same actionable
signal whether the detector died or the daemon was idle. Missing/
unparsable marker (fresh daemon) does not false-alarm.
- Cadence: _planted_fire_alarm_loop every 6h, registered at daemon
startup, failures debug-logged (never crash the daemon).
+5 tests (positive + negative states): marker written per round incl.
skip path; stale marker alarms; fresh marker silent; missing marker
silent; unparsable marker silent. pytest 1190 passed + 1 skipped;
Agent.md count 1186→1191.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle

Reviewed the diff (3 files, +177/-2) and verified locally on branch feature/planted-fire-staleness-alarm:

  • daemon.py: _touch_planted_fire_marker() runs at the top of _refresh_usage_anchor so every round (including skip paths) persists a single-line heartbeat; _check_planted_fire_stale() is stateless (age > 7 days → greppable planted-fire-stale warning); missing/corrupt marker → debug log, no false alarm; 6h _planted_fire_alarm_loop registered at startup, exceptions never crash the daemon.
  • tests/test_daemon.py: 5 new tests covering both positive AND negative states — marker written on skip + normal paths, stale marker fires, fresh/missing/unparsable marker no-alarm. All 5 pass locally.
  • Agent.md: test count 1186 → 1191, matches pytest --collect-only (1191 collected).
  • CI: run 33376430101 — test + test-windows both SUCCESS. PR mergeable, state CLEAN.

The implementation matches issue #1086 acceptance criteria (persistent marker + age alarm, N=7 days, rides round cadence, idle boundary = same actionable signal, positive+negative tests).

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle

Re-verified on unchanged head e741c79 (1 commit, +177/-2):

  • CI run 33376430101: test + test-windows both SUCCESS.
  • Mergeable, state CLEAN.
  • Local checks (previous cycle): 5 planted-fire tests pass, pytest --collect-only = 1191 matches Agent.md.

Implementation matches issue #1086 acceptance criteria: per-round marker (incl. skip paths), stateless 7-day age check with no false-alarm on missing/corrupt marker, 6h alarm loop, positive+negative tests. LGTM 2/3.

@pm25coder

Copy link
Copy Markdown
Collaborator

I tested this PR against the acceptance criteria from issue #1086 (which I filed), and found the implementation meets all of them — including the verification-type logic in both positive and negative states.

Marker (persistence)_touch_planted_fire_marker() writes a single overwritten timestamp line to ~/.emrg/logs/planted-fire-heartbeat, called at the top of _refresh_usage_anchor BEFORE the skip-path branches, so a round that skips the detector (no usage / no prompt_tokens) still touches the marker — round-loop liveness, exactly per the issue. OSError is debug-logged (stats writes never take the daemon down).

Alarm (stateless age check)_check_planted_fire_stale() reads the marker, computes age in days, and logs the greppable planted-fire-stale warning when age > _PLANTED_FIRE_STALE_DAYS (7, kept next to _SILENT_DRIFT_THRESHOLD). Boundary check is strict >, so a marker exactly at N days does not alarm — consistent with a threshold.

Idle boundary (option a) — verified both branches: missing marker (fresh daemon, never ran a round) → debug log + False (no false alarm on a brand-new install); stale marker (detector died OR daemon idle for N days) → same actionable warning. Timezone handling is safe: marker written with datetime.now().astimezone().isoformat() (aware, with offset), read back with fromisoformat, age computed as aware-minus-aware — no naive/aware mixing.

Negative states all covered — fresh marker → no alarm; missing marker → no alarm; unparsable marker (ValueError/OSError caught) → no alarm + no crash. Positive state: 8-day-old marker → planted-fire-stale + True. The 5 new tests (test_daemon.py) assert each; Agent.md count 1186 → 1191 matches (+5). CI test + test-windows both PASS on this head.

Non-blocking observations:

  • _planted_fire_alarm_loop sleeps FIRST, so the first staleness check happens 6h after daemon start — a daemon restarted after a long downtime will report stale within 6h, not immediately. Fine for a low-frequency alarm; just worth knowing for ops.
  • The dedicated single-line marker file is a cleaner fit for a stateless age check than a JSONL event (one-line read, no parse), so the deviation from the issue's "JSONL event" suggestion is an improvement.

No functional concerns on my side.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle

Third review from a distinct cycle. Head e741c79 unchanged (1 commit, +177/-2); CI run 33376430101 test + test-windows both SUCCESS; MERGEABLE/CLEAN. Local verification (prior cycles): 5 planted-fire tests pass, pytest --collect-only = 1191 matches Agent.md. Meets issue #1086 acceptance criteria. LGTM 3/3 — merging.

@argszero
argszero merged commit dffbd6d into masterAug 31, 2026
2 checks passed
Sign up for freeto 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.

Planted-fire staleness alarm: last_planted age check (reader-driven suggestion)

2 participants

@argszero@pm25coder