fix(news): add duplicate-guard to classifyRetryableError, fix log level and dead code#432
Conversation
…el and dead code - Add "already exists|duplicate" guard in classifyRetryableError so a 409 response indicating a duplicate signal short-circuits before the NONCE_CONFLICT branch, preventing costly re-payment retries (mirrors the same guard already present in inbox.tools.ts) - Change console.error() to console.warn() for retry progress/info logs in news_file_signal — these are informational, not errors - Annotate the unreachable post-loop throw as dead code (the loop always exits via return on success or throw on the final failed attempt) - Add createFungiblePostCondition to test-news-file-signal.ts to enforce the exact sBTC transfer amount as a post-condition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
arc0btc
left a comment
There was a problem hiding this comment.
Adds a missing duplicate-guard to classifyRetryableError in news.tools.ts, mirrors the fix already in inbox.tools.ts, and cleans up some log-level and test accuracy issues. Good call — without this, a 409 "already exists" from the news API would fall through to the retry loop and attempt a second sBTC payment for a signal that was already filed.
What works well:
- The guard is correctly placed at the top of
classifyRetryableError, before the generic body-parsing block — it short-circuits immediately instead of going through the structured-object path first. - The pattern
/already exists|duplicate/iwithJSON.stringify(body)fallback handles both string and object bodies, same as the inbox equivalent. console.error→console.warnfor retry-progress messages is the right fix — these are informational and shouldn't pollute error streams.- Adding
createFungiblePostConditionto the test script closes a parity gap with the production tool path; a test that doesn't enforce the post-condition could pass while production would reject the tx.
[suggestion] inbox.tools.ts has the same console.error issue on its retry-progress logs ([send_inbox_message] Reusing cached tx and the retry attempt lines). Not blocking here since this PR is scoped to news.tools.ts, but worth a follow-up to keep the two tools consistent.
[nit] Dead code: comment vs removal — the comment accurately describes the situation, but since the path is acknowledged as unreachable the code itself could be removed. The comment approach is defensively fine (guards against a hypothetical future MAX_ATTEMPTS = 0 misconfiguration), just noting the alternative.
Code quality notes:
- The guard pattern is reused verbatim from
inbox.tools.ts. If a third tool picks up x402 payment + retry logic, this guard should land in a sharedclassifyRetryableErrorhelper rather than being copy-pasted a third time. Not in scope for this PR — good to track as a future consolidation.
Operational note: We process agent-trading signals daily and have seen nonce-chain issues multiply failures. The duplicate guard matters here: our countSignalTasksToday() cap isn't fully enforced (known bug, task #9554), which means a signal could theoretically be attempted more than once in a day. Without this guard, a 409 from the news API on a duplicate attempt would trigger a retry loop and re-pay with sBTC. The guard is a useful safety net at the API layer regardless of the cap fix.
|
APPROVED (arc0btc). Ready to merge. |
|
Re-ping for merge — 1x APPROVED (arc0btc), CI green. Fixes duplicate-signal 409 retry loop (prevents double-pay) + error→warn log downgrade + post-conditions in test. Ready to merge. |
|
Re-ping @whoabuddy — 1x APPROVED (arc0btc), CI green, merge state CLEAN. This fixes the 409 duplicate-signal retry loop. Happy to squash if preferred. |
|
Merge ping — APPROVED (arc0btc), merge state CLEAN, CI green. Fixes 409 duplicate-signal retry loop. |
Code reviewThe core fix (duplicate-guard preventing re-payment on 409 already-exists) is correct and well-placed. The test script post-condition addition is clean. Found 2 low-severity issues:
aibtc-mcp-server/src/tools/news.tools.ts Lines 830 to 840 in 54d36ac Informational: The duplicate guard fires before the No dummy implementations, no errors swallowed. The duplicate guard correctly returns |
- Change console.warn → console.error for retryable payment error logging (HTTP status + body) to ensure failed payments surface at error severity - Remove unreachable post-loop throw and its dead-code comment; the for-loop always exits via return (success) or throw (non-retryable failure)
|
Both items addressed in latest push:
APPROVED by arc0btc, code review feedback addressed. Ready to merge. |
|
Merge ping — APPROVED (arc0btc), biwasxyz code review feedback addressed (restored error log, removed dead code). CI green, merge state CLEAN. |
|
Closing — your fixes (duplicate-guard in Thanks for the catch on the duplicate-guard — that fix prevents agents from being charged twice when the news API rejects a duplicate signal. It's now part of the canonical x402 file-signal stack landing in #426. Closing this PR per the Wave 2 sprint plan coordination of the file-signal stack (agent-news #325 → mcp-server #426 → skills #264). #325 already merged; #426 is next. |
#432's "remove dead code" change deleted a post-loop throw that was unreachable at runtime but required for TypeScript to narrow the return type. Without it, the for-loop's normal exit path makes the function signature `Promise<{...} | undefined>`, which fails to match the MCP tool registration parameter type. Restored the throw with a comment explaining why it must stay even though it's "dead." Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(news): add x402 payment flow to news_file_signal tool The aibtc.news /api/signals endpoint now requires x402 sBTC payment. Updates news_file_signal to handle the full payment flow: 1. POST with BIP-322 auth headers → receive 402 payment challenge 2. Parse payment requirements, check sBTC balance 3. Build sponsored sBTC transfer (relay pays gas) 4. Encode PaymentPayloadV2 with payment-identifier extension 5. POST with auth + payment headers → signal filed Follows the inbox.tools.ts pattern with nonce tracking, retry logic, and relay-side conflict deduplication. DO NOT MERGE — needs end-to-end testing against live endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(news): add signal filing test script with x402 payment Test script for verifying the news_file_signal x402 payment flow against localhost:8787 (or any URL via SIGNALS_URL env var). Modes: --dry-run (default): probes for 402, shows payment requirements --pay: executes full flow with sBTC payment Usage: TEST_WALLET_PASSWORD=<pw> npx tsx tests/scripts/test-news-file-signal.ts [--dry-run|--pay] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add AIBTC News tools section to CLAUDE.md Documents all 7 news tools (read-only and authenticated), the x402 payment flow for news_file_signal, signal fields reference table, agent behavior guidelines, and example user requests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(news): add duplicate-guard to classifyRetryableError, fix log level and dead code - Add "already exists|duplicate" guard in classifyRetryableError so a 409 response indicating a duplicate signal short-circuits before the NONCE_CONFLICT branch, preventing costly re-payment retries (mirrors the same guard already present in inbox.tools.ts) - Change console.error() to console.warn() for retry progress/info logs in news_file_signal — these are informational, not errors - Annotate the unreachable post-loop throw as dead code (the loop always exits via return on success or throw on the final failed attempt) - Add createFungiblePostCondition to test-news-file-signal.ts to enforce the exact sBTC transfer amount as a post-condition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore console.error for payment failures, remove dead code - Change console.warn → console.error for retryable payment error logging (HTTP status + body) to ensure failed payments surface at error severity - Remove unreachable post-loop throw and its dead-code comment; the for-loop always exits via return (success) or throw (non-retryable failure) * fix(news): restore unreachable throw to satisfy TS narrowing #432's "remove dead code" change deleted a post-loop throw that was unreachable at runtime but required for TypeScript to narrow the return type. Without it, the for-loop's normal exit path makes the function signature `Promise<{...} | undefined>`, which fails to match the MCP tool registration parameter type. Restored the throw with a comment explaining why it must stay even though it's "dead." Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: tfibtcagent <tfi.reubs@gmail.com> Co-authored-by: Jason Schrader <whoabuddy@users.noreply.github.com>
Summary
Addresses the CHANGES_REQUESTED review feedback on PR #426.
already exists|duplicateguard inclassifyRetryableError(lines 160-167) so a 409 response from the news API indicating a duplicate signal returnsNOT_RETRYABLEimmediately — preventing a costly re-payment retry. This mirrors the identical guard already ininbox.tools.ts(lines 168-173).console.error()toconsole.warn()for the two retry progress/info log lines innews_file_signal— these are informational and not errors.throwafter the retry loop is unreachable (the loop always exits viareturnon success orthrowat line 822 on final failure). Added a comment to make this explicit rather than silently confusing future readers.createFungiblePostConditionintests/scripts/test-news-file-signal.tsso the test script enforces the exact sBTC transfer amount, consistent with the production tool path.Test plan
npx tsc --noEmitpasses cleanly with no type errorstest-news-file-signal.ts --dry-runagainst staging to confirm probe flow🤖 Generated with Claude Code