Skip to content

fix: bound execute result size + fan out whale NFT fetch per-chain#39

Merged
Aliiiu merged 5 commits into
mainfrom
fix/execute-result-size-budget
Jul 4, 2026
Merged

fix: bound execute result size + fan out whale NFT fetch per-chain#39
Aliiiu merged 5 commits into
mainfrom
fix/execute-result-size-budget

Conversation

@Aliiiu

@Aliiiu Aliiiu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Closes #40

Why

Whole-portfolio queries against whale wallets (e.g. 0xd8dA…6045 / vitalik.eth) failed in the aiden-adk leaf agent, which runs execute in code mode and feeds the entire result back into gpt-5-mini (400k-token window):

  • "List token holdings across all chains" / "Show NFT holdings" / "Which approvals on Ethereum" → "input exceeds the context window" (~495k tokens, almost all the execute result payload).
  • NFT holdings specifically also returned empty — a separate, non-whale bug (see fix 3).

This PR fixes the result boundary generically, and fixes the NFT fetch strategy at its source.

What

1. Bound the execute result size (patch)

execute serialized the guest run() return value with no size limit. Added a generic budget at the result boundary (src/mcp/execute/result-budget.ts), enforced in tool.ts:

  • Under budget → returned byte-for-byte unchanged (normal path untouched).
  • Over budget → envelope metadata (ok/error/log_lines/err_lines) kept intact; the bulk result is 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.
  • Configurable via 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)

getUserAllNftList called 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:

  • Discovers the wallet's used chains and fans out the fast per-chain /user/nft_list in parallel with bounded concurrency (8) — wall-clock ≈ (chains / 8) × ~1.5 s.
  • Degrades to partial instead of all-or-nothing: a chain that errors or is too slow is dropped into 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.
  • Stamps each NFT with its chain (the per-chain endpoint often omits it).

Return-shape change (guest-facing): getUserAllNftList now resolves to { nfts, chains, partial, chains_skipped } instead of a bare UserNFT[] — mirroring the existing TokenBalanceAcrossChains partial-aggregate shape. Tool description, response schema, cookbook, and instructions updated to match.

3. Read id (not chain_id) from used_chain_list

DeBank's /user/used_chain_list returns full chain objects keyed by id (verified against the live gateway: 69 chains for vitalik, none with chain_id). The fan-out read c.chain_id → always undefined → every chain filtered out → empty NFTs for every wallet. Fixed the field read, getUserUsedChainListRaw's return type ({ chain_id }{ id }), and UserUsedChainListSchema (chain_idid, .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

  • 149/149 tests pass, tsc clean, 0 lint errors (biome).
  • New: 14 result-budget unit tests + a tool.ts integration test; 10 NFT fan-out tests (fan-out, explicit chain_ids, empty wallet, chain-stamping, partial-on-error, partial-on-soft-deadline, concurrency cap, abort propagation) + a live-shape schema fixture.
  • End-to-end (against built dist):
    • Array 819,727 → 199,900 chars, object 819,795 → 199,998 — bounded, valid JSON, metadata preserved; small results unchanged.
    • 40-chain fan-out with a hung chain → peak concurrency 8, resolves at 15.75 s (< 21 s abort), 39 NFTs + partial: true.
    • Real 69-chain { id } shape → 68 NFTs, non-empty (bug fixed).

Release / handoff

  • Two changesets: bound-execute-result-size (patch) + nft-per-chain-fanout (minor) → aggregates to 2.2.0. The NFT minor reflects the guest-facing return-shape change; downgrade/upgrade if you classify it differently.
  • aiden-adk must bump @iqai/mcp-debank to the new version (currently pins 2.1.1) to pick up the fix.
  • Follow-up: apply the same result-boundary cap to @iqai/defillama-mcp; optionally add live-shape fixture guards to the token fan-out path too.

Aliiiu added 3 commits July 3, 2026 16:15
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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/mcp/execute/result-budget.ts Outdated
Comment thread src/services/user.service.ts Outdated
…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.
@Aliiiu

Aliiiu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/services/user.service.ts Outdated
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.
@Aliiiu

Aliiiu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@Aliiiu Aliiiu self-assigned this Jul 4, 2026
@Aliiiu
Aliiiu merged commit 5e0545c into main Jul 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

execute: whale-wallet queries overflow context window / NFT holdings time out or return empty

1 participant