Skip to content

docs-seo-audit: never post the Slack summary twice in one run - #654

Open
warp-agent-staging[bot] wants to merge 2 commits into
mainfrom
factory/seo-audit-slack-dedupe
Open

docs-seo-audit: never post the Slack summary twice in one run#654
warp-agent-staging[bot] wants to merge 2 commits into
mainfrom
factory/seo-audit-slack-dedupe

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

An SEO audit run posted two Slack summaries ~11s apart for the same run (PR #652): a rough first draft, then a "cleaned up" re-send. Root cause: the docs-seo-audit skill's Slack notification section had no "post at most once" contract — just a bare curl example with no success check — so nothing stopped an agent from revising the wording and re-sending. Not a duplicate schedule and not two agent runs; only one SEO schedule exists and both messages came from the same Oz run.

Changes

.agents/skills/docs-seo-audit/SKILL.md

  • Mandates exactly one Slack post per run: revise wording before sending, never after a successful (ok: true) post.
  • Requires checking the channel for an existing top-level *SEO Audit — <date>* message before posting, and skipping if one exists.
  • Replaces the freeform curl example with the one-shot poster script below, matching the aeo_crosslink_audit pattern.

.agents/skills/docs-seo-audit/scripts/notify_seo_slack.py (new)

Small helper that performs the same-day dedupe check and the post in a single invocation, so an agent can't casually re-invoke curl to "fix" wording after a successful post. The dedupe check paginates Slack history through the start of the target date rather than a fixed recent-message window, and fails closed (skips the post, non-zero exit) when the history check itself can't be verified. Exits 0 on post/skip, non-zero on a post failure or an unverified history check.

.agents/skills/docs-seo-audit/test_notify_seo_slack.py (new)

Regression test for the dedupe logic (find_existing_post / is_top_level / fetch_messages_for_date): same-day top-level post detected regardless of exact wording, a match on a later pagination page is still found, pagination stops when there's no next cursor, different date not matched, thread replies not mistaken for a top-level post, thread-parent still counts, empty history doesn't block the first post, and a failed history check (Slack API error or network error) fails closed without calling chat.postMessage.

Rework changes

Addressed both [IMPORTANT] findings from the code-review pass:

  • Fixed 50-message history window could miss a same-day post. fetch_recent_messages (single conversations.history call, limit=50) is replaced with fetch_messages_for_date, which paginates the API with oldest set to the start of the target date and follows response_metadata.next_cursor until Slack reports no more pages. Added check_pagination_finds_later_page_match (a same-day post that only appears on a simulated second page is still detected) and check_fetch_stops_pagination_when_no_next_cursor regression coverage.
  • History-check failure posted anyway. main() previously caught the history-fetch exception, logged a warning, and fell through to treat history as empty (posting proceeds). It now returns a non-zero exit and skips the post on any conversations.history failure — a Slack API error (RuntimeError) or a network error (URLError) — instead of risking a duplicate on the exact ambiguous-retry path this script exists to prevent. Added check_main_fails_closed_on_history_error and check_main_fails_closed_on_url_error regression coverage (both assert a non-zero exit and that post_message is never called).

Also updated the "Sending the notification" section of SKILL.md to describe the new fail-closed exit behavior.

Verification

  • Updated regression test: python3 .agents/skills/docs-seo-audit/test_notify_seo_slack.py — all 11 cases pass (7 original + 4 new: later-page pagination match, pagination-stop, and two fail-closed cases).
  • python3 -m py_compile on both scripts — no syntax errors.
  • notify_seo_slack.py --help — CLI parses as expected.
  • SKILL.md frontmatter validated (name: / description: present) per the docs repo's skill-authoring conventions.
  • trunk check / trunk fmt are not runnable in this sandbox (Trunk CLI isn't vendored in the repo, per create_pr's note); no other repo-documented checks apply to .agents/skills/ content.
  • The SKILL.md prose change itself is skill-doc-only (testing-exempt per factory-verification) — the dedupe/fail-closed logic is genuinely testable code, so it got regression tests instead of relying on the exemption.

Originating thread: https://warpdev.slack.com/archives/C09BVK0PL3Y/p1787900556500469

Harden the Slack notification section against double-posting: mandate
exactly one post per run, check the channel for an existing same-day
SEO Audit message before posting, and replace the freeform curl example
with a one-shot Python poster (notify_seo_slack.py) that performs the
dedupe check and post together so there is no window to re-send after
revising the wording.

Fixes JAS-3: an agent revised its summary mid-run and posted twice
(~11s apart) for the same audit run.

Co-Authored-By: Warp <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 28, 2026
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 28, 2026 1:47pm

Request Review

@warp-for-oss

warp-for-oss Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@warp-agent-staging[bot]

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@warp-agent-staging warp-agent-staging Bot added the warpy-factory Opened by the Warp factory agents label Aug 28, 2026

@warp-for-oss warp-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

This PR adds a one-shot Slack poster and updates the docs SEO audit skill to avoid duplicate same-day Slack summaries. The direction matches the reported failure, but the helper still has paths that can miss an existing same-day post and send another message.

Concerns

  • The history lookup only inspects the latest 50 channel messages, so a busy channel can push an earlier same-day SEO Audit summary out of the dedupe window.
  • The helper posts anyway when conversations.history fails, which bypasses the dedupe check on exactly the ambiguous retry path this PR is meant to make safe.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

return False


def fetch_recent_messages(token: str, channel: str, limit: int = 50) -> list:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Limiting the dedupe scan to the latest 50 messages can miss an earlier same-day SEO Audit post in a busy channel, causing a rerun to post a duplicate. Query from the start of --date with pagination or another same-day bound instead of relying on a fixed recent-message window.

try:
recent = fetch_recent_messages(token, args.channel)
except (urllib.error.URLError, RuntimeError) as exc:
print(f"warning: could not check channel history ({exc}); posting anyway", file=sys.stderr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Posting when conversations.history fails bypasses the dedupe guarantee: a retry after an ambiguous earlier success can still send a second summary. Treat an unverified history check as a skipped or failed notification and write the message to run output rather than calling chat.postMessage.

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The PR updates the SEO audit skill to mandate a one-shot Python Slack poster and adds direct regression coverage for top-level same-day message recognition. CI is green, the reviewer request read-back includes rachaelrenk, and the helper test is meaningful; however, the notification path remains fail-open and only reads 50 messages, so it can still post a duplicate summary. Found: 0 critical, 2 important, 0 suggestions, 0 nits. Request changes.

Required changes

  1. Paginate Slack history through the target date before deciding no matching top-level summary exists.
  2. Fail closed when the history request cannot be verified; do not call chat.postMessage.

Verification

CI: all 8 checks passed. Local: python3 -m py_compile and the 7-case dedupe regression test passed. ./script/presubmit is not present in this repository, and trunk is not installed in this sandbox. The current reviewRequests read-back includes rachaelrenk.

Verdict

Found: 0 critical, 2 important, 0 suggestions, 0 nits

Request changes

return False


def fetch_recent_messages(token: str, channel: str, limit: int = 50) -> list:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] The one-page conversations.history query does not satisfy the same-day dedupe requirement in a busy channel: an existing SEO Audit post can fall outside the newest 50 messages, and this helper will then post a duplicate. Paginate history and stop only after reaching messages older than the target date (with regression coverage for a matching post on a later page).

try:
recent = fetch_recent_messages(token, args.channel)
except (urllib.error.URLError, RuntimeError) as exc:
print(f"warning: could not check channel history ({exc}); posting anyway", file=sys.stderr)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] If conversations.history fails after an earlier ambiguous or successful attempt, this exception handler treats history as empty and calls chat.postMessage, bypassing the no-duplicate contract. Fail closed: report the history-check failure and return non-zero or a skipped result without posting.

Rework for JAS-3 review: the same-day dedupe check now paginates conversations.history through the target date instead of relying on a fixed 50-message window, so an earlier same-day summary in a busy channel is still found. A failed or unverifiable history check now fails closed (skips the post, non-zero exit) instead of treating history as empty and posting anyway.

Co-Authored-By: Warp <agent@warp.dev>

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Overview

This rework paginates Slack history and fails closed on lookup errors. It cannot be accepted because the pagination request corrupts valid opaque Slack cursor values.

Verdict

Checks: CI ✅ · Python compile and 11 targeted regression cases ✅ · docs build unavailable locally (Node 25 is outside the repository’s supported engines; CI build ✅)
Found: 0 critical, 1 important, 0 suggestions

Prior concerns: pagination traversal and history-failure handling are addressed; cursor encoding remains outstanding.

Request changes

Review run

https://oz.staging.warp.dev/runs/01a0489e-91f4-76cb-aea4-13017d8e7179

cursor = None
while True:
url = (
f"{SLACK_API}/conversations.history?channel={channel}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] URL-encode the opaque next_cursor value (and assert the encoded follow-up URL in the pagination regression test). Slack cursors can contain query-reserved characters: a + becomes a space and an & starts a new parameter here, so the next request can use an invalid cursor, miss later same-day messages, and allow a duplicate summary. Build the query with urllib.parse.urlencode instead of interpolating the cursor.

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

Labels

cla-signed warpy-factory Opened by the Warp factory agents

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants