Skip to content

Task-email quality: 2026-07-03 audit follow-ups (duplicate schedules, health-check false alarms, missing source links, stale metrics) #1846

Description

@sweetmantech

Tracking issue for the quality fixes coming out of the 2026-07-03 task-email audit (recoup-internal-task-email-audit, window 2026-07-02T19:00Z → 2026-07-03T19:00Z). Every one of the 13 delivered customer emails was audited for data provenance (per-run tool-call traces), link quality (all URLs curl-checked), and prompt fulfillment. Scope: the emails that DID deliver. Delivery failures (sandbox key expiry → 401, accounts with no account_emails row, 403 recipient restriction) are deliberately out of scope here — the working hypothesis (sweetman, 2026-07-03) is that fixing the quality bugs on the delivering tasks also raises the floor for the rest.

Goal

Tomorrow's scheduled task emails (2026-07-04 batches at 09:00 and 13:00 UTC) ship with: no duplicate sends (one enabled scheduled_actions row per job, plus an idempotency guard at the send layer), no false-alarm subject lines (health-check task no longer declares its own sandbox "DOWN"), clickable evidence in every trend report (publisher source links, not zero-link "✔ Verified" badges or Google News redirect shells), no stale metrics presented as live, and schedules that fire at the prompt's stated local time. Surfaces involved: scheduled_actions rows (Supabase, prompts + crons), POST /api/emails (api/lib/emails/sendEmailHandler.ts), and the task-agent send helper.

PRs (updated 2026-07-03)

PR Item Base State
none yet — items below are in priority order; prompt/schedule fixes need no PR (data changes in scheduled_actions)

Merge sequencing for the two code items: docs (OpenAPI contract for dry_run + idempotency on POST /api/emails) → api. Prompt/schedule fixes are independent and can land today.

What shipped (context — the audit itself)

  • 63 customer-prompt-task Trigger.dev runs across 36 accounts in the window; 15 customer sends delivered to 11 recipients; 33 non-test rejections (see out-of-scope note).
  • 0 hallucinated metrics in all 13 delivered emails. Every metric/timestamp traced to a real fetch in the run's chat_messages trace; sources independently re-verified (e.g. Ashnikko's "Broke 08:17 UTC" = soapcentral pubDate Fri, 03 Jul 2026 08:17:30 GMT; LA EQUIS +89,479 streams = exact Jun 23 → Jul 3 two-snapshot delta in song_measurements). The post-Task emails must contain real, sourced data — eliminate hallucinated metrics #1833 guardrails are demonstrably working.
  • 0 dead links across ~70 checked — but 3 of 13 emails ship zero source links while claiming verification.
  • Verdicts: 9 valuable, 3 marginal, 1 actively harmful (health-check false "[ALERT] … FAILURE (Web Server Down)").

Decision 2026-07-03 (sweetman): account 24994fe8 belongs to a former dev no longer working with us — both of its "Daily systems check" tasks were disabled outright rather than deduped-to-one, and the planned health-check prompt rewrite is dropped as moot. If he returns he can re-enable from the UI (a fresh Trigger schedule gets minted on enable). The on-blocker-email principle from that item is preserved under Architecture decisions.

Done

  • September Mourning (223062ee): pause the old brief, rework + retime the new one.
    ✅ Shipped 2026-07-04 (direct prod changes; no PR — scheduled_actions + Trigger.dev only).
    Decision (sweetman): keep b5582012 "Daily X Trends + Tweet Drafts", pause (not delete) 565b0169 "Daily Social Media Trends" so the artist isn't surprised in their UI — Trigger schedule sched_ppyg6n7nfgmyogywqoy95 deleted, row set enabled=false, prompt untouched.
    b5582012 reworked in place, preserving the customer's Goal/taxonomy/tweet-mix verbatim and adding: a grounding step 0 (fetch socials + last-7-days posts, rank by engagement from fresh scrape output; Mondays only fetch Spotify per-track playcounts via research endpoints, totals-with-capture-date until two snapshots exist); freshness rules (event <24h not article, no repeats from yesterday, verify-or-drop every trend); one publisher source link + one-line description per aligned trend; content pushes link the measured top-performing assets with verified canonical fallbacks (correct-cased Spotify URL, live YouTube handle); a "Your assets this week" table capped at 5 rows (posts daily, streams Mondays), every number from a this-run fetch with capture date; inline-CSS/plain-text/no-paren-URL formatting rules; the corrected X handle @SeptmbrMourning. Cron 0 9 * * *0 14 * * * (6-7 AM PT, matching the prompt's PT framing) on both the row and Trigger schedule sched_pj7vqaabzwn4wywf9brn9.
    Prereq artist-data fixes (dead X/YouTube links re-pointed, Spotify URL casing, fresh scrapes + 12 IG posts stored) are in the 2026-07-04 issue comment.
    Verified: row shows schedule='0 14 * * *', prompt_len 5085; Trigger.dev GET confirms cron "0 14 * * *", nextRun 2026-07-05T14:00Z, active. First reworked send: 2026-07-05 14:00 UTC; first Monday streams table: 2026-07-06.
  • Disable both 24994fe8 "Daily systems check" tasks (was: dedupe the duplicate pair).
    ✅ Shipped 2026-07-03 (direct prod change; no PR — data-layer only).
    Both rows (4b1a8fd6-c175-407e-87a9-c029a8b6fec8, df8a6224-0eb1-49a7-86da-805d77f96b96) were enabled duplicates on cron 0 13 * * * producing contradictory daily health alerts (2026-07-03 13:06 + 13:10). PATCH /api/tasks was not usable (our org keys get Access denied to specified account_id for this non-member account), so the handler's exact behavior was replicated manually per api/lib/tasks/updateTask.tssyncTriggerSchedule: deleted Trigger.dev schedules sched_nbg4wf4urw1matww3fybc and sched_ed3ruobsucmj7us83nz3r via the Management API (both 200), then update scheduled_actions set enabled=false, trigger_schedule_id=null on both rows.
    Verified: UPDATE's returning shows both rows enabled=false, trigger_schedule_id=null; Trigger.dev DELETE responses acknowledged both schedule ids. Expect zero customer-prompt-task runs for this account from 2026-07-04 onward (checkable in the daily audit).

Open — prompt & schedule fixes (no PR needed; edit scheduled_actions)

  • De-collide the remaining scheduled actions. (Scope corrected 2026-07-03 after row-level inspection of scheduled_actions; the true duplicate (24994fe8) and the September Mourning collision are both resolved under Done.)
    • acct bc73354e (JJ Lin) — already clean, no active bug: 7 near-identical rows but only 1e46a9a3… is enabled (cron 0 1 * * *, live trigger); the other 6 (61bc6ed8, da8bee05, ab4a4348, 430a37ce, f39392b6, 7af3c392) are disabled, trigger-less leftovers from Oct–Nov 2025 prompt iteration. The 2026-07-03 01:28 duplicate attempts were the ephemeral-key retry storm, not a second schedule. Optional hygiene: delete the 6 dead rows.
    • acct 7ce7341d (LV8R) — self-heal verified 2026-07-03: exactly one enabled row remains (fec6ae4b…, 0 13 * * *); the duplicate 8aeb06e0… was disabled by the 13:11 run itself. Nothing to do except confirm a single 2026-07-04 13:00 UTC send.
    • Done when: 2026-07-04 audit shows exactly one send per job for both accounts, and 2026-07-05 shows one September Mourning send at 14:00 UTC.
  • Migrate the Ashnikko prompt (acct 26cba4e3, last updated 2025-07-08) to the current trends template.
    • Why: the one pre-link-mandate prompt left. Result: every card stamped "✔ Verified" with zero clickable sources, a footer name-dropping outlets (BBC, Official Charts, Billboard) not all present in the trace, <details>-toggle layout that breaks in Outlook, footer on deprecated recoupable.com.
    • Fix: port the per-topic Sources-line mandate, inline-CSS-only rule, and recoupable.dev footer from the current template (the PinkPantheress prompt on acct 7b54a0c7 is the reference).
    • Done when: next Ashnikko email has ≥1 working source link per topic and no <details> elements.
  • Add the freshness + sourcing rubric to all trend prompts.
    • Why: two emails gamed the 24 h window (a syndicated Jul-2 repost of a Jul-1 NME story; follow-up coverage of a Jun-28 injury story); the Rostrum report passed Instagram's own newsroom (first-party PR) and a $19.99 affiliate listing as "verified" sources, and ~50 % of its bullets were recycled verbatim-in-substance from the previous day's email to the same recipient (trace even shows the agent querying yesterday's topics by name: "Sophie Cunningham" meme when:2d).
    • Fix: append to each trends prompt: (1) "the underlying event, not the latest article about it, must be <24 h old — otherwise say so"; (2) "two independent sources means no first-party PR and no affiliate/aggregator pages"; (3) "exclude topics featured in the previous report unless there is a material new development"; (4) "cite the final publisher URL, not the Google News redirect".
    • Done when: 2026-07-04 Rostrum report (acct d3b964c6) has 0 repeated topics vs 07-03 and no single-source bullets.
  • Ruby Velle weekly playbook (acct 83ef8335): live data only, links mandatory.
    • Why: email claimed metrics "pulled directly from Ruby's connected accounts" while reporting 12,155 IG followers — the run's own fresh scrape stored 11,994 (stale research doc won over live data). Spotify figures had no backing fetch this run. 33 KB "playbook" contains zero URLs. No live trend research occurred despite "Analyze current music industry trends" in the prompt.
    • Fix: prompt additions — report only numbers fetched this run (anything older labeled "as of "); every claimed stat and every recommended example links out; require ≥1 live trend fetch or an honest "formats are evergreen, not researched today" line.
    • Done when: next send's follower counts match the same run's account_socials scrape values and body contains working links.
  • iAMLYRIC (acct 2b3aafc5): fix the timezone and the dead-end CTAs; audit all crons for the local-vs-UTC bug.
    • Why: prompt specifies 09:00 America/New_York; cron fired 09:01 UTC → customer gets a "morning" brief at 05:06 ET (the email honestly prints it). All 30 CTA buttons (OPEN TEMPLATE / SCHEDULE POST) point at the bare recoupable.com homepage. Plain-text fallback wraps URLs in parens → clients linkify a trailing ) → broken.
    • Fix: cron → 0 13 * * *; sweep every scheduled_actions row whose prompt names a timezone and check the cron matches; CTAs → real deep links or drop the buttons; unwrap parens around plain-text URLs.
    • Done when: 2026-07-04 send lands 13:00–13:15 UTC and no href resolves to the bare homepage.

Open — send-layer guards (code; docs → api)

  • Idempotency key on task-email sends.
    • Why: duplicate schedules (above) reached inboxes as duplicate/contradictory emails (LV8R twice under the identical subject 3 min apart, health check twice with opposite verdicts). Prompt hygiene alone can't guarantee this never recurs — Resend supports idempotency keys natively.
    • Fix: send helper passes task-email/<scheduled_action_id>/<ISO-date> as the Resend idempotency key through POST /api/emails; processAndSendEmail forwards it.
    • Done when: two identical sends for the same action+date result in one delivered email (second returns the original response), covered by a unit test.
  • dry_run mode on POST /api/emails + helper instruction to use it for diagnostics.
    • Why: the JJ Lin retry run (acct bc73354e, 2026-07-03 01:28) debugged auth by POSTing emails with subjects "probe"/"probe2"/"probe4" addressed to the customer — they were only stopped because auth happened to be down. Same pattern on acct d15d5708 ("test auth check - ignore" ×2 to the customer's address). One junk email to a real customer erases the trust ten good reports build.
    • Fix: { "dry_run": true } runs full validation/auth/recipient checks and returns the verdict without sending or logging sent; sandbox send-helper docs instruct agents to diagnose with dry_run, never with probe sends to real recipients.
    • Done when: dry-run request returns the same 4xx an actual send would, delivers nothing, and the helper's guidance names it; no "probe"-subject rows with customer recipients in email_send_log after rollout.

Architecture decisions

  • Fix quality at the prompt layer first; add platform guards only where a prompt can't prevent customer-visible harm. The audit showed prompts with explicit guardrails ("only from content fetched THIS RUN", "honest line if fewer") produced 0 hallucinations — prompt fixes are the highest-leverage, zero-deploy lever. The two code items (idempotency, dry_run) cover the failure modes prompts can't: double-fires and agent debugging behavior.
  • The on-blocker email is the sanctioned fallback for unconfigurable tasks. When a task lacks the connection/config it needs, the agent sends one short "action needed" email with a working link and stops (reference: connect-YouTube send, acct d7ad378c). It does not improvise a target, and it does not re-nag daily without a "already notified?" check.

Source references

  • Audit method: recoup-internal-task-email-audit skill (skills repo). Sources: Trigger.dev customer-prompt-task run list, email_send_log, chats/chat_messages traces, scheduled_actions (Supabase godremdqwajrwazhbrue).
  • Prior trackers: Task emails: make scheduled-task emails accurate, trustworthy & valuable #1829 (empty-email delivery, resolved 2026-07-01), Task emails must contain real, sourced data — eliminate hallucinated metrics #1833 (hallucinated data, resolved 2026-07-02). This issue is the quality follow-up now that content is verified-real.
  • Out-of-scope (candidate follow-up tracker): delivery failures — sandbox ephemeral-key expiry → 401 retry storms (~16 accounts), 8 accounts with no account_emails row running undeliverable email tasks daily, 403 recipient-restriction discovered only at send time, and email_send_log recording rejections without a reason (api/lib/emails/sendEmailHandler.ts L59).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions