Skip to content

feat(email): scheduled daily inbox briefing (off by default)#1918

Open
kovtcharov wants to merge 1 commit into
mainfrom
claudia/task-ff1d4bb5
Open

feat(email): scheduled daily inbox briefing (off by default)#1918
kovtcharov wants to merge 1 commit into
mainfrom
claudia/task-ff1d4bb5

Conversation

@kovtcharov

Copy link
Copy Markdown
Collaborator

The morning triage card existed only on demand — a user had to ask for a pre-scan every time. Now the email sidecar can generate it on a daily schedule with no prompt: each run reuses the agent's own pre_scan_inbox path (same email_pre_scan envelope, nothing re-classified), persists it with a generated_at stamp, and any surface pulls the latest run from the new additive GET /v1/email/briefing. Off by default — enabled explicitly with GAIA_EMAIL_BRIEFING_ENABLED=true (fire time GAIA_EMAIL_BRIEFING_TIME, default 08:00); an invalid value fails sidecar startup loudly instead of guessing a schedule.

The gaia schedule dispatcher (#1371) is approved but not merged, so the in-process timer is deliberately minimal and email-scoped: run_briefing_job() is the one-shot seam the #1371 dispatcher / autonomy engine (#555) will invoke directly when they land, at which point the timer can be deleted without touching the job or delivery. SCHEMA_VERSION stays 2.1 (additive, like #1887).

Closes #1608 (capability 16 in #1691).

Test plan

  • AC: scheduled job invokes pre_scan_inbox and produces the email_pre_scan envelope — test_briefing_job_produces_email_pre_scan_envelope
  • AC: disabled schedule produces no briefing (no task, no job run, nothing persisted) — test_disabled_schedule_produces_no_briefing
  • Config is explicit + fail-loud: off when unset; maybe/8am/0/101 each raise at startup — test_config_invalid_values_fail_loud
  • GET /v1/email/briefing: 404 before the first run, conforms to EmailBriefingResponse after one (tests/test_email_openapi_conformance.py::test_briefing_conforms_to_spec)
  • python -m pytest hub/agents/python/email/tests/ tests/test_email_openapi_conformance.py tests/unit/agents/email/ tests/unit/email/ — 762 passed
  • python -m gaia_agent_email.export_openapi --check (artifact regenerated) · python util/lint.py --all clean
  • Sidecar golden path: booted packaging/server.py's app with the lifespan — health OK, briefing 404 with actionable detail, invalid env aborts startup with BriefingConfigError

The pre-scan briefing content existed but only ran when a user asked;
there was no way to get the morning triage card without a prompt. The
email sidecar can now generate it on a daily timer and any surface can
pull the latest run from GET /v1/email/briefing.

- New gaia_agent_email.briefing: env-driven BriefingScheduleConfig
  (off by default, invalid values fail sidecar startup loudly),
  run_briefing_job reusing pre_scan_inbox_impl (same email_pre_scan
  envelope, nothing re-classified), and an asyncio daily scheduler
  wired into the sidecar lifespan. run_briefing_job is the seam the
  gaia schedule dispatcher (#1371) / autonomy engine (#555) will call
  directly when they land.
- Delivery: each run atomically persists the envelope with a
  generated_at stamp; new additive GET /v1/email/briefing serves the
  latest run (404 before the first one). SCHEMA_VERSION stays 2.1.
- Regenerated openapi.email.json; spec page + specification.html,
  npm README/SPEC/SKILL/CHANGELOG, and docs/guides/email.mdx updated
  together.

Closes #1608
@github-actions github-actions Bot added documentation Documentation changes tests Test changes agent::email Email agent changes labels Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Verdict: Approve

This adds a scheduled daily inbox briefing to the email sidecar: a daily timer runs the existing pre-scan (no re-classification), persists the email_pre_scan envelope, and exposes the latest run at a new additive GET /v1/email/briefing. It's off by default, opt-in via env vars, and fails startup loudly on a bad config value. The design is clean — the one-shot run_briefing_job is an explicit seam for the future gaia schedule/autonomy dispatchers, so the in-process timer can be deleted later without touching the job or delivery.

The bottom line: no blocking issues. Config is genuinely fail-loud (no silent coercion), writes are atomic, the schedule-off path is proven to produce nothing, and every doc surface (README, SPEC, SKILL, CHANGELOG, spec_html, OpenAPI, specification.html, guide) is updated in sync — exactly what CLAUDE.md requires for a contract change. One minor, non-blocking note on DST below.

🔍 Technical details

🟢 Minor

Naive-local scheduling can drift across a DST boundary (briefing.py:557, seconds_until_next_run at :484)
The loop computes seconds_until_next_run(..., datetime.now()) — a naive local timestamp — then sleeps ~24h. If a DST transition falls inside that sleep, one fire lands an hour early/late (twice a year). This is inherent to any naive daily timer and the impact is negligible for a briefing, so it's fine to ship as-is; worth a one-line comment noting the tradeoff, or leave it for the #1371 dispatcher to own real scheduling. No change required.

Verified, not issues

  • get_prescan_backend (api_routes.py:1245) is a plain function with no request-context dependency, so driving it from the scheduler's asyncio.to_thread worker is safe; _resolve_briefing_backend correctly translates its HTTPException into BriefingUnavailableError for non-HTTP callers.
  • The pre-scan envelope key set asserted in test_briefing_job_produces_email_pre_scan_envelope matches pre_scan_inbox_impl's output (read_tools.py:729), so the "byte-for-byte same envelope" claim holds.
  • GET /briefing error mapping is sound: None → 404, unreadable file → 500, shape mismatch → 500 with an actionable "delete the file" message. No silent fallback.
  • Config env opt-in reads at build_app() time (packaging/server.py:721), so an invalid value aborts startup rather than failing at the first fire — matches the fail-loud intent.

Strengths

  • Fail-loud config done rightfrom_env rejects maybe/8am/25:00/0/101 with actionable BriefingConfigError messages instead of coercing to a default, and the parametrized test_config_invalid_values_fail_loud locks that in.
  • No re-implementation — the job reuses the agent's own pre_scan_inbox_impl and the REST resolver, keeping the scheduled and on-demand cards identical; injection seams (backend, sink, run_job) make it fully testable with no live mailbox or LLM.
  • Atomic delivery — write-to-.tmp-then-replace prevents a reader ever seeing a partial file, and it's explicitly tested (test_persist_briefing_is_atomic_and_loadable).
  • Additive contract disciplineSCHEMA_VERSION stays 2.1, the endpoint is registered in the OpenAPI conformance + rest-contract path tests, and all six doc surfaces are updated together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::email Email agent changes documentation Documentation changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(email): scheduled daily inbox briefing

2 participants