Skip to content

[hyper-pm] MP-CGA-006: Implement skip-if-fresh idempotency #249

Description

@sapiderman

ID: MP-CGA-006 | Priority: P0 | PRD refs: FR3, FR4 | Status: draft
Group: MP Content Generation Agent (mp-content-generation-agent)


Implement skip-if-fresh idempotency

Summary

The agent currently re-calls OpenAI on every invocation regardless of whether a newsletter was already generated for the same ticker that day. This ticket implements the skip-if-fresh precheck: before calling the LLM, the agent queries the existing newsletter store for a row matching tickerId within the current calendar day (in the configured IANA timezone). If a qualifying row exists, the agent returns a skipped outcome without calling OpenAI or creating another row.

Scope

  • In scope:
    • After config validation and the "no sources" check, add a precheck step that calls the agent-data-api (via the existing SDK) to determine whether a newsletter for tickerId already exists within the current calendar day in config.freshness.timezone.
    • The "current calendar day" window is: [start-of-day in timezone, end-of-day in timezone) evaluated at invocation time using the IANA timezone from config.
    • If a qualifying newsletter row exists, return an internal outcome with code skipped_fresh_newsletter_exists (defined in MP-CGA-005) and a descriptive message. Do not call OpenAI.
    • The precheck must use the typed SDK method (no raw HTTP paths). If agent-data-api does not yet expose a "get most recent newsletter for ticker on date" endpoint, add it as part of this ticket following the agent-data-api-endpoints skill checklist (contract → manifest → handler → SDK → tests → docs).
    • Update the existing getDataSourcesForTicker source-selection window: document clearly in a code comment whether the skip-if-fresh window and the source-selection window are aligned or differ, and why. (PRD note: today source selection uses UTC "start of day"; this ticket aligns the skip check to the configured IANA timezone. The source window alignment is out of scope — only documentation is required in v1.)
    • Unit tests for the precheck logic: newsletter exists in window → skipped, newsletter exists but outside window → not skipped, no newsletter exists → proceeds.
    • Document the delivery-agent coordination behavior in a code comment: when the content-generation step is skipped, a new newsletter row is NOT written; the delivery agent must be designed to use the existing row or skip delivery accordingly.
  • Out of scope:
    • A forceRegenerate bypass (PRD phase 2; not in v1).
    • Changing the source-selection UTC window in getDataSourcesForTicker (alignment deferred; comment only in v1).
    • Envelope-level skipped: true flag on the HTTP response (current @workspace/agent-runtime envelope does not support a skipped status; the skip is expressed in the diagnostic record written by MP-CGA-007 — no runtime contract change in this ticket).

Acceptance criteria

  • When a newsletter for tickerId was created within the current calendar day (per config.freshness.timezone), the agent returns { success: false, message: "Newsletter already generated for <tickerId> today (skipped)" } and does not call OpenAI or create a new newsletter row.
  • When no qualifying newsletter exists, the agent proceeds to the LLM call normally.
  • The freshness window boundaries are computed using config.freshness.timezone (IANA), not UTC.
  • The precheck is logged with structured fields (tickerId, timezone, windowStart, windowEnd, existingNewsletterFound: true/false) at the precheck stage; no secrets are logged.
  • If the agent-data-api endpoint needed for the precheck is new, it is implemented following the agent-data-api-endpoints skill (contract + manifest + handler + SDK + tests).
  • A code comment at the precheck site documents: (a) the freshness window formula, (b) the fact that source-selection still uses UTC start-of-day (deliberate difference in v1, not a bug), (c) delivery-agent coordination expectation.
  • Unit tests (following vitest-unit-testing skill): newsletter in window → skip, newsletter outside window → proceed, no newsletter → proceed. Use vi.useFakeTimers() to control "now" deterministically.
  • pnpm code-quality passes with no new errors.

Dependencies

  • Depends on: MP-CGA-003 (config schema with freshness group), MP-CGA-005 (outcome taxonomy — skipped_fresh_newsletter_exists code)
  • Blocks: MP-CGA-007 (writes a skipped diagnostic using this outcome)

Stacked delivery

This ticket introduces new precheck behavior. When config uses default freshness settings (calendar_day, Asia/Jakarta), the check is active immediately after this merges — any re-run of a pipeline for the same ticker on the same calendar day will be skipped. Confirm with the team that the delivery pipeline handles a "no new newsletter row" outcome correctly before merging to production.

No feature flag is required given the skip returns a clear message and the behavior is documented. However, if the team wants to roll out skip-if-fresh gradually, a freshness.enabled: boolean field can be added to the config schema — discuss before implementation.

Merge order: Ship after MP-CGA-003 and MP-CGA-005. Stack this on top of MP-CGA-005's branch (or rebase onto main after both predecessors merge). MP-CGA-007 stacks on top of this.

Notes

  • Use Temporal (if available in the runtime) or Date with Intl.DateTimeFormat to compute timezone-aware start/end of day. If a date library (e.g. date-fns-tz, luxon) is already used in the monorepo, reuse it rather than adding a new dependency.
  • The precheck query is a read, not a write — it should be fast (< 100ms in typical conditions). Log a warning if the SDK call takes longer than expected.
  • If adding a new agent-data-api endpoint for the freshness check (e.g. GET /content-generation/newsletters/latest?tickerId=X&date=YYYY-MM-DD&timezone=...), follow the full agent-data-api-endpoints checklist. Alternatively, if the existing GET /content-generation already returns enough data to filter client-side, that is acceptable — document the choice.
  • The no_sources skip (from MP-CGA-005) and skipped_fresh_newsletter_exists are both precheck-stage skips. They should produce the same internal AgentOutcome shape with different code fields, so MP-CGA-007 can log them uniformly.

PRD: https://github.com/hyperjumptech/mediapulse/blob/prds/content-generation-agent-prd.md

{
  "hyper_pm_id": "01KP1FZ605NFJ71XGYP4F8R58F",
  "type": "ticket",
  "parent_ids": {
    "epic": "01KP1JW157F7WMYR2FEN1VDRXS",
    "story": "01KP1JW2E08TA8M0CYP3KDT09Q"
  },
  "depends_on": [
    "01KP1FZ606H2RFQX2VDHHA8HQH"
  ]
}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions