fix: bound execute result size + fan out whale NFT fetch per-chain#39
Conversation
The execute tool serialized the guest run() return value with no size
limit, so whale-wallet whole-portfolio queries (all tokens across chains,
all NFTs, all approvals on Ethereum) produced ~500k-token payloads that
overflowed the consuming model's 400k-token context window before it could
run the summarizing call.
Enforce a generic size budget at the result boundary
(DEBANK_MCP_EXECUTE_RESULT_BUDGET_CHARS, default 200k chars ~= 50k tokens):
the envelope metadata (ok/error/log_lines/err_lines) is kept intact and the
bulk `result` is truncated -- arrays keep the first N, objects trim their
largest arrays, strings keep a prefix -- with a structured
`_truncated: { shown, total, reason, hint }` signal attached so the consumer
can refine the query instead of crashing. Results under budget are returned
byte-for-byte unchanged.
…regate
DeBank's /user/all_nft_list aggregates across every chain server-side and
takes 10-20s for active wallets, routinely timing out to nothing for whales.
getUserAllNftList now discovers the wallet's used chains and fans out the fast
per-chain /user/nft_list calls in parallel with bounded concurrency (8), so
wall-clock is ~ (chains / 8) x 1.5s rather than one long-hanging aggregate.
The method degrades to a partial result instead of failing all-or-nothing: a
chain that errors or is too slow is dropped into chains_skipped, and the fan-out
enforces its own soft deadline (~75% of the call's timeout budget) so it returns
the chains that landed before the execute client's hard abort fires. Each NFT is
stamped with its chain (the per-chain endpoint often omits it).
Return shape changes from a bare UserNFT[] to { nfts, chains, partial,
chains_skipped }; tool description, response schema, cookbook, and instructions
are updated to match, and the search-ranking test now expects the chain-less
aggregate as the top hit for "get NFTs for wallet".
DeBank's /user/used_chain_list returns full chain objects keyed by `id`
(verified against the live gateway: 69 chains for vitalik.eth, each with
`id`/`community_id`/`name`/... and no `chain_id`). The NFT fan-out read
`c.chain_id`, which was always undefined, so every chain was filtered out and
getUserAllNftList early-returned empty for EVERY wallet — not just whales.
Read `c.id` instead, and correct getUserUsedChainListRaw's return type
({ chain_id } -> { id }) and UserUsedChainListSchema (chain_id -> id, plus
passthrough to reflect the full chain object DeBank actually returns).
The fan-out unit tests had mocked used_chain_list in the wrong { chain_id }
shape, encoding the same false assumption, so they passed while the code was
broken. They now use the real { id } shape (and a schema fixture locks it), so
the "reads a field that is always undefined" regression can't ship again.
There was a problem hiding this comment.
Code Review
This pull request implements a generic size budget for execute tool results to prevent context window overflows, truncating oversized payloads while preserving metadata. Additionally, it refactors getUserAllNftList to perform a parallel, bounded-concurrency per-chain fan-out, enabling graceful degradation and partial results on timeout. The review feedback identifies a performance bottleneck in result-budget.ts due to redundant JSON.stringify calls during array sorting, and a potential race condition in user.service.ts where late-running background workers could mutate the returned skipped array after resolution.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…t late mutation Addresses two findings from PR review: - result-budget.ts: the object-truncation sort called JSON.stringify inside the comparator, re-serializing potentially large arrays on every O(M log M) comparison. Precompute each array's serialized length once (O(M)) into a Map. - user.service.ts: when the NFT fan-out's soft deadline wins the race, straggler workers keep running; a late non-abort rejection runs skipped.push(chain_id), which mutated the by-reference chains_skipped AFTER the method returned (duplicate entry). Return a clone so the handed-back array is frozen. Adds a regression test that rejects a straggler post-deadline and asserts the returned chains_skipped does not grow.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a generic size budget on the execute tool's result boundary to prevent context window overflows, truncating large payloads while preserving metadata. It also optimizes NFT fetching by replacing the slow server-side aggregate with a parallel, bounded-concurrency per-chain fan-out that degrades gracefully on timeouts or errors. Additionally, a bug in getUserUsedChainListRaw was fixed to correctly read chain IDs. Feedback on these changes suggests normalizing, trimming, and deduplicating the target chain IDs in the NFT fan-out to avoid redundant API requests and potential failures from duplicate or mixed-case inputs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Addresses a PR review finding: duplicate or mixed-case chain_ids (e.g. "ETH, eth") fanned out redundant parallel per-chain requests and double-counted NFTs, and non-lowercase ids could fail upstream. Trim + lowercase (every DeBank chain id is lowercase) + dedupe via Set, applied uniformly to both the explicit chain_ids path and the used_chain_list path. Adds a regression test.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a generic size budget on execute tool results to prevent context window overflows, truncating large payloads while attaching a structured _truncated signal. Additionally, it optimizes the retrieval of NFT holdings across chains by replacing the slow server-side /user/all_nft_list aggregate with a host-side parallel fan-out of per-chain /user/nft_list calls. This fan-out supports bounded concurrency and graceful degradation, returning partial results and skipped chains upon timeouts or failures. The response schema, documentation, and tests have been updated accordingly. No review comments were provided, so there is no feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Closes #40
Why
Whole-portfolio queries against whale wallets (e.g.
0xd8dA…6045/ vitalik.eth) failed in theaiden-adkleaf agent, which runsexecutein code mode and feeds the entire result back intogpt-5-mini(400k-token window):executeresult payload).This PR fixes the result boundary generically, and fixes the NFT fetch strategy at its source.
What
1. Bound the
executeresult size (patch)executeserialized the guestrun()return value with no size limit. Added a generic budget at the result boundary (src/mcp/execute/result-budget.ts), enforced intool.ts:ok/error/log_lines/err_lines) kept intact; the bulkresultis truncated (arrays keep first N; objects trim their largest arrays at any depth; strings keep a prefix) with a structured_truncated: { shown, total, reason, hint, fields? }signal so the model answers "showing top N of M — refine" instead of crashing.DEBANK_MCP_EXECUTE_RESULT_BUDGET_CHARS(default 200,000 chars ≈ 50k tokens). Documented in the Safety-limits table.2. Fan out whale NFT fetch per-chain (
minor)getUserAllNftListcalled DeBank's/user/all_nft_list, which aggregates server-side, takes 10–20 s for active wallets, and routinely times out to nothing. It now:/user/nft_listin parallel with bounded concurrency (8) — wall-clock ≈ (chains / 8) × ~1.5 s.chains_skipped, and a soft deadline (~75% of the call's timeout budget) abandons stragglers so the chains that landed come back before the client's hard abort fires.chain(the per-chain endpoint often omits it).Return-shape change (guest-facing):
getUserAllNftListnow resolves to{ nfts, chains, partial, chains_skipped }instead of a bareUserNFT[]— mirroring the existingTokenBalanceAcrossChainspartial-aggregate shape. Tool description, response schema, cookbook, and instructions updated to match.3. Read
id(notchain_id) fromused_chain_listDeBank's
/user/used_chain_listreturns full chain objects keyed byid(verified against the live gateway: 69 chains for vitalik, none withchain_id). The fan-out readc.chain_id→ alwaysundefined→ every chain filtered out → empty NFTs for every wallet. Fixed the field read,getUserUsedChainListRaw's return type ({ chain_id }→{ id }), andUserUsedChainListSchema(chain_id→id,.passthrough()). The fan-out tests had mocked the wrong{ chain_id }shape — they now use the real{ id }shape, plus a schema fixture that locks it, so the regression can't ship silently again.Tests & verification
tscclean, 0 lint errors (biome).tool.tsintegration test; 10 NFT fan-out tests (fan-out, explicitchain_ids, empty wallet, chain-stamping, partial-on-error, partial-on-soft-deadline, concurrency cap, abort propagation) + a live-shape schema fixture.dist):partial: true.{ id }shape → 68 NFTs, non-empty (bug fixed).Release / handoff
bound-execute-result-size(patch) +nft-per-chain-fanout(minor) → aggregates to2.2.0. The NFTminorreflects the guest-facing return-shape change; downgrade/upgrade if you classify it differently.aiden-adkmust bump@iqai/mcp-debankto the new version (currently pins 2.1.1) to pick up the fix.@iqai/defillama-mcp; optionally add live-shape fixture guards to the token fan-out path too.