Summary
A transient Polymarket data-API failure during a redeem round crashes the entire agent with an unhandled AttributeError, instead of being retried or no-op'd. This is a long-standing latent bug (predates all recent Polymarket redeem work), only triggered when the data-api.polymarket.com/positions call fails.
Observed crash
From a Pearl agent_runner.log (Polystrat, agent sc-c427c8fc...):
[ERROR] Error fetching positions: HTTPSConnectionPool(host='data-api.polymarket.com', port=443):
Max retries exceeded with url: /positions?...&redeemable=True
(Caused by ConnectTimeoutError(... connect timeout=10))
[INFO] Received Srr message: ... error=True, payload={"error": "Error fetching positions: ... timed out ..."}
[INFO] Fetched 1 redeemable positions
...
File ".../decision_maker_abci/behaviours/polymarket_reedem.py", line 77, in _update_policy_for_redeemable_positions
condition_id = position.get("conditionId")
AttributeError: 'str' object has no attribute 'get'
Error: AEA was terminated cause exception `... TraderConsensusBehaviour ...`
The agent process is terminated by the framework (uncaught AEAActException).
Root cause
Two layered defects:
-
send_polymarket_connection_request (packages/valory/skills/market_manager_abci/behaviours/base.py:100) returns json.loads(response.payload) and never checks response.error. When the connection fails it returns the dict {"error": "...timed out..."} instead of a list of positions. This affects every caller, not just redeem.
-
PolymarketRedeemBehaviour (packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py) never validates the response shape:
len({"error": ...}) is 1, so it logs the misleading Fetched 1 redeemable positions.
_update_policy_for_redeemable_positions (line 76–77) does for position in redeemable_positions: — iterating a dict yields its keys, so position == "error" (a str), and position.get("conditionId") raises AttributeError.
- The same crash exists in the
_prepare_redeem_tx and _redeem_via_builder loops (they also call position.get(...)).
A recoverable network blip becomes a fatal agent crash.
Was this always a bug?
Yes — not a regression:
- The unchecked
response.error has existed since 924f66b3 refactor: polymarket fetch rounds (#744).
d325c831 (e-greedy accuracy store at redemption) only moved the crash site earlier (line 197 → _update_policy_for_redeemable_positions); before it, the same {"error": ...} dict crashed a few lines later in the redeem loops.
35bb90d1 fix(polymarket): guard malformed positions in redeem loops does not fix this: its if condition_id is None: continue guards run after position.get(...), so they defend against a dict with missing keys, not against position being a str — and it never touched _update_policy_for_redeemable_positions.
It stayed invisible because the happy path always returns a real list; only an API failure during a redeem round exercises it.
Suggested fix
- In
send_polymarket_connection_request: check response.error (and/or validate the deserialized payload type) and surface failures distinctly so callers can retry/no-op rather than receiving an error dict typed as data.
- In
PolymarketRedeemBehaviour: if the fetched result is not a list (e.g. an {"error": ...} dict), log and treat the round as "no redeemable positions / retry" instead of iterating it. Add a type guard at the top of the three position loops.
- Regression test: connection returns
{"error": ...} → redeem round degrades gracefully, agent does not crash.
Related
Summary
A transient Polymarket data-API failure during a redeem round crashes the entire agent with an unhandled
AttributeError, instead of being retried or no-op'd. This is a long-standing latent bug (predates all recent Polymarket redeem work), only triggered when thedata-api.polymarket.com/positionscall fails.Observed crash
From a Pearl
agent_runner.log(Polystrat, agentsc-c427c8fc...):The agent process is terminated by the framework (uncaught
AEAActException).Root cause
Two layered defects:
send_polymarket_connection_request(packages/valory/skills/market_manager_abci/behaviours/base.py:100) returnsjson.loads(response.payload)and never checksresponse.error. When the connection fails it returns the dict{"error": "...timed out..."}instead of a list of positions. This affects every caller, not just redeem.PolymarketRedeemBehaviour(packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py) never validates the response shape:len({"error": ...})is1, so it logs the misleadingFetched 1 redeemable positions._update_policy_for_redeemable_positions(line 76–77) doesfor position in redeemable_positions:— iterating a dict yields its keys, soposition == "error"(astr), andposition.get("conditionId")raisesAttributeError._prepare_redeem_txand_redeem_via_builderloops (they also callposition.get(...)).A recoverable network blip becomes a fatal agent crash.
Was this always a bug?
Yes — not a regression:
response.errorhas existed since924f66b3 refactor: polymarket fetch rounds (#744).d325c831(e-greedy accuracy store at redemption) only moved the crash site earlier (line 197 →_update_policy_for_redeemable_positions); before it, the same{"error": ...}dict crashed a few lines later in the redeem loops.35bb90d1 fix(polymarket): guard malformed positions in redeem loopsdoes not fix this: itsif condition_id is None: continueguards run afterposition.get(...), so they defend against a dict with missing keys, not againstpositionbeing astr— and it never touched_update_policy_for_redeemable_positions.It stayed invisible because the happy path always returns a real list; only an API failure during a redeem round exercises it.
Suggested fix
send_polymarket_connection_request: checkresponse.error(and/or validate the deserialized payload type) and surface failures distinctly so callers can retry/no-op rather than receiving an error dict typed as data.PolymarketRedeemBehaviour: if the fetched result is not alist(e.g. an{"error": ...}dict), log and treat the round as "no redeemable positions / retry" instead of iterating it. Add a type guard at the top of the three position loops.{"error": ...}→ redeem round degrades gracefully, agent does not crash.Related
json.loads/ indefinite client hangs) — same theme (external-call resilience), different scope.