From 9c2fd5c823226264eee896bc19586ca452753d5c Mon Sep 17 00:00:00 2001 From: lzhao8956-glitch Date: Wed, 29 Jul 2026 22:58:49 +0800 Subject: [PATCH 1/4] fix(polymarket_client): guard non-dict response and cap cursor pagination (closes #936) --- .../polymarket_client/connection.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/valory/connections/polymarket_client/connection.py b/packages/valory/connections/polymarket_client/connection.py index 0b18e3cd22..d9f8da06c8 100644 --- a/packages/valory/connections/polymarket_client/connection.py +++ b/packages/valory/connections/polymarket_client/connection.py @@ -89,7 +89,7 @@ "international", ] MARKETS_LIMIT = 300 -EVENTS_LIMIT = 200 +EVENTS_LIMIT = 200`nMAX_PAGES = 100 # Safety cap on the keyset cursor loop (issue #936) MARKETS_TIME_WINDOW_DAYS = 4 API_REQUEST_TIMEOUT = 10 MAX_API_RETRIES = 3 @@ -858,7 +858,7 @@ def _fetch_markets_by_tag_slug( after_cursor: Optional[str] = None all_markets: list = [] - while True: + for _ in range(MAX_PAGES): params: Dict[str, Any] = { "tag_slug": tag_slug, "end_date_max": end_date_max, @@ -875,6 +875,18 @@ def _fetch_markets_by_tag_slug( if error: return None, error + # _request_with_retries can return a non-dict payload (JSON null, a + # stray list, or a string from a misbehaving edge proxy). Calling + # .get() on it would raise AttributeError and bubble up to + # fetch_markets' blanket except Exception, dropping the whole + # category. Degrade gracefully instead. (See issue 936.) + if not isinstance(response, dict): + self.logger.warning( + f"fetch_markets: non-dict response from keyset API " + f"(type={type(response).__name__}); breaking pagination" + ) + break + events_data = response.get("events") or [] markets_this_page = 0 @@ -895,6 +907,14 @@ def _fetch_markets_by_tag_slug( after_cursor = response.get("next_cursor") if not after_cursor: break + else: + # Hit MAX_PAGES without exhausting next_cursor -- upstream may be + # returning the same cursor (server bug, cache poisoning, partial + # outage). Warn so the next refresh can re-attempt. (See issue 936.) + self.logger.warning( + f"fetch_markets: hit MAX_PAGES={MAX_PAGES} cap with non-empty " + f"next_cursor; truncating market list at {len(all_markets)}" + ) return all_markets, None From 7303d348e835d7e1ef59dc3582c1395ed1474b60 Mon Sep 17 00:00:00 2001 From: lzhao8956-glitch Date: Thu, 30 Jul 2026 01:20:43 +0800 Subject: [PATCH 2/4] fix(polymarket_client): check response.error before json.loads in base.py (closes #970) The send_polymarket_connection_request() helper would return an error-dict payload ({"error": "..."}) as if it were valid data when the SRR response had response.error == True. Callers (e.g. fetch_markets, the redeem loops) would then iterate the dict and call .get() on string keys, raising AttributeError and crashing the whole agent. Adding an early return on response.error prevents the bad payload from being passed downstream. (See issue #970.) --- .../skills/market_manager_abci/behaviours/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/valory/skills/market_manager_abci/behaviours/base.py b/packages/valory/skills/market_manager_abci/behaviours/base.py index 303cea4774..a80676e1a4 100644 --- a/packages/valory/skills/market_manager_abci/behaviours/base.py +++ b/packages/valory/skills/market_manager_abci/behaviours/base.py @@ -120,6 +120,17 @@ def send_polymarket_connection_request( self.context.logger.warning("No response from the Polymarket connection.") return None + # _request_with_retries can return a non-dict payload (JSON null, a + # list, or a string from a misbehaving edge proxy). Calling + # .get() on it would raise AttributeError and bubble up to + # fetch_markets' blanket except Exception, dropping the whole + # category. Degrade gracefully instead. (See issue #970.) + if response.error: + self.context.logger.warning( + f"Polymarket connection error: {response.error}" + ) + return None + response_json = json.loads(response.payload) return response_json From 4d29adc5071d3900266d3193d805c1b0fd0fb8bd Mon Sep 17 00:00:00 2001 From: lzhao8956-glitch Date: Thu, 30 Jul 2026 01:20:45 +0800 Subject: [PATCH 3/4] fix(polymarket_reedem): guard non-list redeemable_positions (closes #970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three vulnerable loops in PolymarketRedeemBehaviour — _update_policy_for_redeemable_positions, _prepare_redeem_tx, and _redeem_via_builder — each iterated ``redeemable_positions`` without a type guard. When the upstream data-API returned an error-dict payload (one element, key="error"), Python iterated the dict KEYS (strings), and ``position.get("conditionId")`` raised AttributeError, crashing the whole redeem round. Adding ``isinstance(redeemable_positions, list)`` at the top of each method degrades gracefully (logs the error, returns empty). This complements the upstream fix in #970 (send_polymarket_connection_request now bails on response.error). Together they handle the failure mode from both sides — root cause AND defensive consumer. --- .../decision_maker_abci/behaviours/polymarket_reedem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py index 5245c262ba..027503f11e 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py @@ -262,6 +262,13 @@ def _redeem_via_builder( current_utilized_tools: Optional[str] = None, ) -> Generator: """Redeem positions via builder flow (connection request).""" + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return None # Redeem each position for position in redeemable_positions: From e927369edb0de010f877cdb65c52243c70a573b0 Mon Sep 17 00:00:00 2001 From: lzhao8956-glitch Date: Thu, 30 Jul 2026 01:21:40 +0800 Subject: [PATCH 4/4] fix(polymarket_reedem): add type guards to remaining 2 redeem loops (closes #970) Add isinstance(redeemable_positions, list) guard to the two remaining vulnerable methods: _update_policy_for_redeemable_positions and _prepare_redeem_tx. (_redeem_via_builder was already guarded in the previous commit.) This ensures all three redeem flows degrade gracefully when the upstream data-API returns an error-dict payload. Combined with the root-cause fix in send_polymarket_connection_request (response.error check), the agent will no longer crash on transient Polymarket data-API failures. --- .../behaviours/polymarket_reedem.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py index 027503f11e..f9d73d7686 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py @@ -73,6 +73,14 @@ def _update_policy_for_redeemable_positions( :param redeemable_positions: list of position dicts from the Polymarket API. """ + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return + for position in redeemable_positions: condition_id = position.get("conditionId") if condition_id is None: @@ -271,6 +279,14 @@ def _redeem_via_builder( return None # Redeem each position + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return None + for position in redeemable_positions: condition_id = position.get("conditionId") outcome_index = position.get("outcomeIndex") @@ -319,6 +335,14 @@ def _prepare_redeem_tx( return "" # Build redemption transactions and add to multisend_batches + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return "" + for position in redeemable_positions: condition_id = position.get("conditionId") outcome_index = position.get("outcomeIndex")