Repository navigation
Eval: ingest email items at their report date, not the wall clock (#346) - #347
Conversation
The runner pushed each email item through ingest_email, which stamped it with datetime.now(UTC). That time became the alert's occurred_at, and triage's rule score adds 5 to an alert outside 06:00-18:00 UTC (agents/triage/base.py _score). The phishing_v1 cassettes were recorded at 08:23 UTC, so after 18:00 UTC phish-003/-004/-006/-008 built a triage prompt with rule confidence 100 instead of 95 and missed. (The other four were already capped at 100.) ingest_email takes an optional received_at; the runner passes the report's top-level Date header, or a fixed noon when there is none. Production still uses the arrival time. The committed cassettes replay unchanged. A new integration test replays them with the ingestion clock frozen at 03:30 and 21:00 UTC; it fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
ReviewThe fix looks right. Production behaviour is unchanged because I didn't run the tests myself, and the repo has no top-level Suggestions (non-blocking)
Security and performance
Test coverage Approving in spirit. The suggestions are optional. 🤖 Generated with Claude Code |
Closes #346
Root cause
The evaluation runner pushed each email item through
IngestionService.ingest_email, which stamped it withdatetime.now(UTC)(src/lg_sotf/ingestion/service.py:296). That time becomes the alert'soccurred_at. Triage's rule score then adds 5 to an alert that occurs outside 06:00-18:00 UTC (src/lg_sotf/agents/triage/base.py:384-386,_score).The
phishing_v1cassettes were recorded at 08:23 UTC. After 18:00 UTC, phish-003, -004, -006 and -008 built a triage prompt withrule_assessment.confidence100 instead of 95, so the input hash changed and the replay raisedCassetteMiss. phish-001, -002, -005 and -007 were already capped at 100, so they still matched.I confirmed this by diffing the dumped triage prompts with the clock shifted back to the recording time against the prompts built now. The confidence field was the only difference.
Fix
ingest_emailtakes an optionalreceived_at. It still defaults to now, so production behaviour is unchanged.EvalRunner._ingestpassesemail_received_at(mime): the report's top-levelDateheader in UTC, or a fixed weekday noon (EMAIL_FALLBACK_RECEIVED_AT) when the header is missing or unparseable. It never uses the wall clock.Tests
tests/integration/eval/test_cassettes.py::test_the_phishing_cassettes_replay_at_any_hour_of_the_wall_clockreplays the phishing_v1 cassettes with the ingestion clock frozen at 03:30 and at 21:00 UTC. Both times are after hours. Without the fix, both cases fail.tests/unit/eval/test_email_received_at.pycovers the Date parsing, the fallback, and the runner passing the report time toingest_email.make lint,make typecheck,lint-importsandscripts/changelog.py checkpass.pytest tests/unit tests/deploy tests/invariants tests/integration/eval: 5114 passed, 67 skipped.Residual
The email normalizer still clamps an
occurred_atthat is more than 15 minutes in the future against the wall clock. A dataset dated in the future would therefore still depend on when it runs. No current dataset is.🤖 Generated with Claude Code
https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc